From 17e8d4f656c09b485f680711b20c39899b566211 Mon Sep 17 00:00:00 2001 From: Jack Gordley Date: Mon, 15 Sep 2025 07:07:50 -0700 Subject: [PATCH 1/8] feat: Add AgentCore Memory Checkpointer --- .../langgraph_checkpoint_aws/__init__.py | 10 + .../checkpoint/agentcore_memory/constants.py | 29 ++ .../checkpoint/agentcore_memory/helpers.py | 307 +++++++++++ .../checkpoint/agentcore_memory/models.py | 89 ++++ .../checkpoint/agentcore_memory/saver.py | 279 ++++++++++ samples/memory/checkpointer-demo.ipynb | 486 ++++++++++++++++++ 6 files changed, 1200 insertions(+) create mode 100644 libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/checkpoint/agentcore_memory/constants.py create mode 100644 libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/checkpoint/agentcore_memory/helpers.py create mode 100644 libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/checkpoint/agentcore_memory/models.py create mode 100644 libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/checkpoint/agentcore_memory/saver.py create mode 100644 samples/memory/checkpointer-demo.ipynb diff --git a/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/__init__.py b/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/__init__.py index c3058802a..828801800 100644 --- a/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/__init__.py +++ b/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/__init__.py @@ -2,5 +2,15 @@ LangGraph Checkpoint AWS - A LangChain checkpointer implementation using Bedrock Session Management Service. """ +from langgraph_checkpoint_aws.checkpoint.agentcore_memory.saver import ( + AgentCoreMemorySaver, +) + __version__ = "0.1.1" SDK_USER_AGENT = f"LangGraphCheckpointAWS#{__version__}" + +# Expose the saver class at the package level +__all__ = [ + "AgentCoreMemorySaver", + "SDK_USER_AGENT", +] diff --git a/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/checkpoint/agentcore_memory/constants.py b/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/checkpoint/agentcore_memory/constants.py new file mode 100644 index 000000000..6374989f2 --- /dev/null +++ b/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/checkpoint/agentcore_memory/constants.py @@ -0,0 +1,29 @@ +""" +Constants and exceptions for AgentCore Memory Checkpoint Saver. +""" + +EMPTY_CHANNEL_VALUE = "_empty" + + +class AgentCoreMemoryError(Exception): + """Base exception for AgentCore Memory errors.""" + + pass + + +class EventDecodingError(AgentCoreMemoryError): + """Raised when event decoding fails.""" + + pass + + +class InvalidConfigError(AgentCoreMemoryError): + """Raised when configuration is invalid.""" + + pass + + +class EventNotFoundError(AgentCoreMemoryError): + """Raised when expected event is not found.""" + + pass diff --git a/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/checkpoint/agentcore_memory/helpers.py b/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/checkpoint/agentcore_memory/helpers.py new file mode 100644 index 000000000..5361fcd12 --- /dev/null +++ b/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/checkpoint/agentcore_memory/helpers.py @@ -0,0 +1,307 @@ +""" +Helper classes for AgentCore Memory Checkpoint Saver. +""" + +from __future__ import annotations + +import base64 +import json +import logging +from collections import defaultdict +from datetime import UTC, datetime +from typing import Any, Dict, List, Union + +import boto3 +from langgraph.checkpoint.base import CheckpointTuple, SerializerProtocol + +from langgraph_checkpoint_aws.checkpoint.agentcore_memory.constants import ( + EMPTY_CHANNEL_VALUE, + EventDecodingError, +) +from langgraph_checkpoint_aws.checkpoint.agentcore_memory.models import ( + ChannelDataEvent, + CheckpointerConfig, + CheckpointEvent, + WriteItem, + WritesEvent, +) + +logger = logging.getLogger(__name__) + +# Union type for all events +EventType = Union[CheckpointEvent, ChannelDataEvent, WritesEvent] + + +class EventSerializer: + """Handles serialization and deserialization of events to store in AgentCore Memory.""" + + def __init__(self, serde: SerializerProtocol): + self.serde = serde + + def serialize_value(self, value: Any) -> Dict[str, Any]: + """Serialize a value using the serde protocol.""" + type_tag, binary_data = self.serde.dumps_typed(value) + return {"type": type_tag, "data": base64.b64encode(binary_data).decode("utf-8")} + + def deserialize_value(self, serialized: Dict[str, Any]) -> Any: + """Deserialize a value using the serde protocol.""" + try: + type_tag = serialized["type"] + binary_data = base64.b64decode(serialized["data"]) + return self.serde.loads_typed((type_tag, binary_data)) + except Exception as e: + raise EventDecodingError(f"Failed to deserialize value: {e}") + + def serialize_event(self, event: EventType) -> str: + """Serialize an event to JSON string.""" + + # Create a custom serializer for Pydantic models + def custom_serializer(obj): + if hasattr(obj, "model_dump"): + return obj.model_dump() + raise TypeError(f"Object of type {type(obj)} is not JSON serializable") + + # Get the base dictionary + event_dict = event.model_dump(exclude_none=True) + + # Handle special serialization for specific fields + if isinstance(event, CheckpointEvent): + event_dict["checkpoint_data"] = self.serialize_value(event.checkpoint_data) + event_dict["metadata"] = self.serialize_value(event.metadata) + + elif isinstance(event, ChannelDataEvent): + if event.value != EMPTY_CHANNEL_VALUE: + event_dict["value"] = self.serialize_value(event.value) + + elif isinstance(event, WritesEvent): + # The writes field is already properly serialized by model_dump() + # We just need to serialize the value field in each write + for write in event_dict["writes"]: + val = write.get("value", EMPTY_CHANNEL_VALUE) + write["value"] = self.serialize_value(val) + + return json.dumps(event_dict, default=custom_serializer) + + def deserialize_event(self, data: str) -> EventType: + """Deserialize JSON string to event.""" + try: + event_dict = json.loads(data) + event_type = event_dict.get("event_type") + + if event_type == "checkpoint": + # Deserialize checkpoint data and metadata + event_dict["checkpoint_data"] = self.deserialize_value( + event_dict["checkpoint_data"] + ) + event_dict["metadata"] = self.deserialize_value(event_dict["metadata"]) + return CheckpointEvent(**event_dict) + + elif event_type == "channel_data": + # Deserialize channel value if not empty + if "value" in event_dict and isinstance(event_dict["value"], dict): + event_dict["value"] = self.deserialize_value(event_dict["value"]) + return ChannelDataEvent(**event_dict) + + elif event_type == "writes": + # Deserialize write values + for write in event_dict["writes"]: + if isinstance(write["value"], dict): + write["value"] = self.deserialize_value(write["value"]) + return WritesEvent(**event_dict) + + else: + raise EventDecodingError(f"Unknown event type: {event_type}") + + except json.JSONDecodeError as e: + raise EventDecodingError(f"Failed to parse JSON: {e}") + except Exception as e: + raise EventDecodingError(f"Failed to deserialize event: {e}") + + +class CheckpointEventClient: + """Handles low-level event storage and retrieval from AgentCore Memory for checkpoints.""" + + def __init__(self, memory_id: str, serializer: EventSerializer, **boto3_kwargs): + self.memory_id = memory_id + self.serializer = serializer + self.client = boto3.client("bedrock-agentcore", **boto3_kwargs) + + def store_event(self, event: EventType, session_id: str, actor_id: str) -> None: + """Store an event in AgentCore Memory.""" + serialized = self.serializer.serialize_event(event) + + self.client.create_event( + memoryId=self.memory_id, + actorId=actor_id, + sessionId=session_id, + eventTimestamp=datetime.now(UTC), + payload=[{"blob": serialized}], + ) + + def store_events_batch( + self, events: List[EventType], session_id: str, actor_id: str + ) -> None: + """Store multiple events in a single API call to AgentCore Memory.""" + # Serialize all events into payload blobs + payload = [] + timestamp = datetime.now(UTC) + + for event in events: + serialized = self.serializer.serialize_event(event) + payload.append({"blob": serialized}) + + # Store all events in a single create_event call + self.client.create_event( + memoryId=self.memory_id, + actorId=actor_id, + sessionId=session_id, + eventTimestamp=timestamp, + payload=payload, + ) + + def get_events( + self, session_id: str, actor_id: str, max_results: int = 100 + ) -> List[EventType]: + """Retrieve events from AgentCore Memory.""" + all_events = [] + next_token = None + + while len(all_events) < max_results: + params = { + "memoryId": self.memory_id, + "actorId": actor_id, + "sessionId": session_id, + "maxResults": min(100, max_results - len(all_events)), + "includePayloads": True, + } + + if next_token: + params["nextToken"] = next_token + + response = self.client.list_events(**params) + + for event in response.get("events", []): + for payload_item in event.get("payload", []): + blob = payload_item.get("blob") + if blob: + try: + parsed_event = self.serializer.deserialize_event(blob) + all_events.append(parsed_event) + except EventDecodingError as e: + logger.warning(f"Failed to decode event: {e}") + + next_token = response.get("nextToken") + if not next_token or len(all_events) >= max_results: + break + + return all_events[:max_results] + + def delete_events(self, session_id: str, actor_id: str) -> None: + """Delete all events for a session.""" + params = { + "memoryId": self.memory_id, + "actorId": actor_id, + "sessionId": session_id, + "maxResults": 100, + "includePayloads": False, + } + + while True: + response = self.client.list_events(**params) + events = response.get("events", []) + + if not events: + break + + for event in events: + self.client.delete_event( + memoryId=self.memory_id, + sessionId=session_id, + eventId=event["eventId"], + actorId=actor_id, + ) + + next_token = response.get("nextToken") + if not next_token: + break + params["nextToken"] = next_token + + +class EventProcessor: + """Processes events into checkpoint data structures.""" + + @staticmethod + def process_events( + events: List[EventType], + ) -> tuple[ + Dict[str, CheckpointEvent], + Dict[str, List[WriteItem]], + Dict[tuple[str, str], Any], + ]: + """Process events into organized data structures.""" + checkpoints = {} + writes_by_checkpoint = defaultdict(list) + channel_data_by_version = {} + + for event in events: + if isinstance(event, CheckpointEvent): + checkpoints[event.checkpoint_id] = event + + elif isinstance(event, WritesEvent): + writes_by_checkpoint[event.checkpoint_id].extend(event.writes) + + elif isinstance(event, ChannelDataEvent): + if event.value != EMPTY_CHANNEL_VALUE: + channel_data_by_version[(event.channel, event.version)] = ( + event.value + ) + + return checkpoints, writes_by_checkpoint, channel_data_by_version + + @staticmethod + def build_checkpoint_tuple( + checkpoint_event: CheckpointEvent, + writes: List[WriteItem], + channel_data: Dict[tuple[str, str], Any], + config: CheckpointerConfig, + ) -> CheckpointTuple: + """Build a CheckpointTuple from processed data.""" + # Build pending writes + pending_writes = [ + (write.task_id, write.channel, write.value) for write in writes + ] + + # Build parent config + parent_config = None + if checkpoint_event.parent_checkpoint_id: + parent_config = { + "configurable": { + "thread_id": config.thread_id, + "checkpoint_ns": config.checkpoint_ns, + "checkpoint_id": checkpoint_event.parent_checkpoint_id, + } + } + + # Build checkpoint with channel values + checkpoint = checkpoint_event.checkpoint_data.copy() + channel_values = {} + + for channel, version in checkpoint.get("channel_versions", {}).items(): + if (channel, version) in channel_data: + channel_values[channel] = channel_data[(channel, version)] + + checkpoint["channel_values"] = channel_values + + return CheckpointTuple( + config={ + "configurable": { + "thread_id": config.thread_id, + "checkpoint_ns": config.checkpoint_ns, + "checkpoint_id": checkpoint_event.checkpoint_id, + } + }, + checkpoint=checkpoint, + metadata=checkpoint_event.metadata, + parent_config=parent_config, + pending_writes=pending_writes, + ) diff --git a/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/checkpoint/agentcore_memory/models.py b/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/checkpoint/agentcore_memory/models.py new file mode 100644 index 000000000..f7c60d8b8 --- /dev/null +++ b/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/checkpoint/agentcore_memory/models.py @@ -0,0 +1,89 @@ +""" +Data models for AgentCore Memory Checkpoint Saver. +""" + +from typing import Any, Dict, List, Optional + +from pydantic import BaseModel, Field + + +class CheckpointerConfig(BaseModel): + """Configuration for checkpoint operations.""" + + thread_id: str + actor_id: str + checkpoint_ns: str = "" + checkpoint_id: Optional[str] = None + + @property + def session_id(self) -> str: + """Generate session ID from thread_id and checkpoint_ns.""" + return ( + f"{self.thread_id}#{self.checkpoint_ns}" + if self.checkpoint_ns + else self.thread_id + ) + + @classmethod + def from_runnable_config(cls, config: Dict[str, Any]) -> "CheckpointerConfig": + """Create CheckpointerConfig from RunnableConfig.""" + from .constants import InvalidConfigError + + configurable = config.get("configurable", {}) + + if not configurable.get("thread_id"): + raise InvalidConfigError( + "RunnableConfig must contain 'thread_id' for AgentCore Checkpointer" + ) + + if not configurable.get("actor_id"): + raise InvalidConfigError( + "RunnableConfig must contain 'actor_id' for AgentCore Checkpointer" + ) + + return cls( + thread_id=configurable["thread_id"], + actor_id=configurable["actor_id"], + checkpoint_ns=configurable.get("checkpoint_ns", ""), + checkpoint_id=configurable.get("checkpoint_id"), + ) + + +class WriteItem(BaseModel): + """Individual write operation.""" + + task_id: str + channel: str + value: Any + task_path: str = "" + + +class CheckpointEvent(BaseModel): + """Event representing a checkpoint.""" + + event_type: str = Field(default="checkpoint") + checkpoint_id: str + checkpoint_data: Dict[str, Any] + metadata: Dict[str, Any] + parent_checkpoint_id: Optional[str] = None + thread_id: str + checkpoint_ns: str = "" + + +class ChannelDataEvent(BaseModel): + """Event representing channel data.""" + + event_type: str = Field(default="channel_data") + channel: str + version: str + value: Any + thread_id: str + checkpoint_ns: str = "" + + +class WritesEvent(BaseModel): + """Event representing pending writes.""" + + event_type: str = Field(default="writes") + checkpoint_id: str + writes: List[WriteItem] diff --git a/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/checkpoint/agentcore_memory/saver.py b/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/checkpoint/agentcore_memory/saver.py new file mode 100644 index 000000000..e56427333 --- /dev/null +++ b/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/checkpoint/agentcore_memory/saver.py @@ -0,0 +1,279 @@ +""" +AgentCore Memory Checkpoint Saver implementation. +""" + +from __future__ import annotations + +import random +from collections.abc import AsyncIterator, Iterator, Sequence +from typing import Any, Dict + +from langchain_core.runnables import RunnableConfig +from langgraph.checkpoint.base import ( + BaseCheckpointSaver, + ChannelVersions, + Checkpoint, + CheckpointMetadata, + CheckpointTuple, + SerializerProtocol, + get_checkpoint_id, + get_checkpoint_metadata, +) + +from langgraph_checkpoint_aws.checkpoint.agentcore_memory.constants import ( + EMPTY_CHANNEL_VALUE, + InvalidConfigError, +) +from langgraph_checkpoint_aws.checkpoint.agentcore_memory.helpers import ( + CheckpointEventClient, + EventProcessor, + EventSerializer, +) +from langgraph_checkpoint_aws.checkpoint.agentcore_memory.models import ( + ChannelDataEvent, + CheckpointerConfig, + CheckpointEvent, + WriteItem, + WritesEvent, +) + + +class AgentCoreMemorySaver(BaseCheckpointSaver[str]): + """ + AgentCore Memory checkpoint saver. + + This checkpoint saver stores checkpoints in Bedrock AgentCore memory + """ + + def __init__( + self, + memory_id: str, + *, + serde: SerializerProtocol | None = None, + **boto3_kwargs: Any, + ) -> None: + super().__init__(serde=serde) + + self.memory_id = memory_id + self.serializer = EventSerializer(self.serde) + self.checkpoint_event_client = CheckpointEventClient( + memory_id, self.serializer, **boto3_kwargs + ) + self.processor = EventProcessor() + + def get_tuple(self, config: RunnableConfig) -> CheckpointTuple | None: + """Get a checkpoint tuple from Bedrock AgentCore Memory.""" + + # TODO: There is room for caching here on the client side + + checkpoint_config = CheckpointerConfig.from_runnable_config(config) + + events = self.checkpoint_event_client.get_events( + checkpoint_config.session_id, checkpoint_config.actor_id + ) + + checkpoints, writes_by_checkpoint, channel_data = self.processor.process_events( + events + ) + + if not checkpoints: + return None + + # Find the specific checkpoint if `checkpoint_id` is provided or return the latest one + if checkpoint_config.checkpoint_id: + checkpoint_event = checkpoints.get(checkpoint_config.checkpoint_id) + if not checkpoint_event: + return None + else: + latest_checkpoint_id = max(checkpoints.keys()) + checkpoint_event = checkpoints[latest_checkpoint_id] + + # Build and return checkpoint tuple + writes = writes_by_checkpoint.get(checkpoint_event.checkpoint_id, []) + return self.processor.build_checkpoint_tuple( + checkpoint_event, writes, channel_data, checkpoint_config + ) + + def list( + self, + config: RunnableConfig | None, + *, + filter: dict[str, Any] | None = None, + before: RunnableConfig | None = None, + limit: int | None = None, + ) -> Iterator[CheckpointTuple]: + """List checkpoints from Bedrock AgentCore Memory.""" + + # TODO: There is room for caching here on the client side + + checkpoint_config = CheckpointerConfig.from_runnable_config(config) + config_checkpoint_id = get_checkpoint_id(config) if config else None + + events = self.checkpoint_event_client.get_events( + checkpoint_config.session_id, checkpoint_config.actor_id + ) + + checkpoints, writes_by_checkpoint, channel_data = self.processor.process_events( + events + ) + + # Build and yield CheckpointTuples + count = 0 + before_checkpoint_id = get_checkpoint_id(before) if before else None + + # Sort checkpoints by ID in descending order (most recent first) + for checkpoint_id in sorted(checkpoints.keys(), reverse=True): + checkpoint_event = checkpoints[checkpoint_id] + # Apply filters + if config_checkpoint_id and checkpoint_id != config_checkpoint_id: + continue + + if before_checkpoint_id and checkpoint_id >= before_checkpoint_id: + continue + + if limit is not None and count >= limit: + break + + writes = writes_by_checkpoint.get(checkpoint_id, []) + + yield self.processor.build_checkpoint_tuple( + checkpoint_event, writes, channel_data, checkpoint_config + ) + + count += 1 + + def put( + self, + config: RunnableConfig, + checkpoint: Checkpoint, + metadata: CheckpointMetadata, + new_versions: ChannelVersions, + ) -> RunnableConfig: + """Save a checkpoint to AgentCore Memory.""" + checkpoint_config = CheckpointerConfig.from_runnable_config(config) + + # Extract channel values + checkpoint_copy = checkpoint.copy() + channel_values: Dict[str, Any] = checkpoint_copy.pop("channel_values", {}) + + # Create all events to be stored in a single batch + events_to_store = [] + + # Create channel data events + for channel, version in new_versions.items(): + channel_event = ChannelDataEvent( + channel=channel, + version=version, + value=channel_values.get(channel, EMPTY_CHANNEL_VALUE), + thread_id=checkpoint_config.thread_id, + checkpoint_ns=checkpoint_config.checkpoint_ns, + ) + events_to_store.append(channel_event) + + checkpoint_event = CheckpointEvent( + checkpoint_id=checkpoint["id"], + checkpoint_data=checkpoint_copy, + metadata=get_checkpoint_metadata(config, metadata), + parent_checkpoint_id=checkpoint_config.checkpoint_id, + thread_id=checkpoint_config.thread_id, + checkpoint_ns=checkpoint_config.checkpoint_ns, + ) + events_to_store.append(checkpoint_event) + + self.checkpoint_event_client.store_events_batch( + events_to_store, checkpoint_config.session_id, checkpoint_config.actor_id + ) + + return { + "configurable": { + "thread_id": checkpoint_config.thread_id, + "checkpoint_ns": checkpoint_config.checkpoint_ns, + "checkpoint_id": checkpoint["id"], + } + } + + def put_writes( + self, + config: RunnableConfig, + writes: Sequence[tuple[str, Any]], + task_id: str, + task_path: str = "", + ) -> None: + """Save pending writes to AgentCore Memory.""" + checkpoint_config = CheckpointerConfig.from_runnable_config(config) + + if not checkpoint_config.checkpoint_id: + raise InvalidConfigError("checkpoint_id is required for put_writes") + + # Create write items + write_items = [ + WriteItem( + task_id=task_id, + channel=channel, + value=value, + task_path=task_path, + ) + for channel, value in writes + ] + + writes_event = WritesEvent( + checkpoint_id=checkpoint_config.checkpoint_id, + writes=write_items, + ) + + self.checkpoint_event_client.store_event( + writes_event, checkpoint_config.session_id, checkpoint_config.actor_id + ) + + def delete_thread(self, thread_id: str, actor_id: str) -> None: + """Delete all checkpoints and writes associated with a thread.""" + self.checkpoint_event_client.delete_events(thread_id, actor_id) + + # ===== Async methods ( TODO: NOT IMPLEMENTED YET ) ===== + async def aget_tuple(self, config: RunnableConfig) -> CheckpointTuple | None: + return self.get_tuple(config) + + async def alist( + self, + config: RunnableConfig | None, + *, + filter: dict[str, Any] | None = None, + before: RunnableConfig | None = None, + limit: int | None = None, + ) -> AsyncIterator[CheckpointTuple]: + for item in self.list(config, filter=filter, before=before, limit=limit): + yield item + + async def aput( + self, + config: RunnableConfig, + checkpoint: Checkpoint, + metadata: CheckpointMetadata, + new_versions: ChannelVersions, + ) -> RunnableConfig: + return self.put(config, checkpoint, metadata, new_versions) + + async def aput_writes( + self, + config: RunnableConfig, + writes: Sequence[tuple[str, Any]], + task_id: str, + task_path: str = "", + ) -> None: + return self.put_writes(config, writes, task_id, task_path) + + async def adelete_thread(self, thread_id: str, actor_id: str) -> None: + return self.delete_thread(thread_id, actor_id) + + def get_next_version(self, current: str | None, channel: None) -> str: + """Generate next version string.""" + if current is None: + current_v = 0 + elif isinstance(current, int): + current_v = current + else: + current_v = int(current.split(".")[0]) + + next_v = current_v + 1 + next_h = random.random() + return f"{next_v:032}.{next_h:016}" diff --git a/samples/memory/checkpointer-demo.ipynb b/samples/memory/checkpointer-demo.ipynb new file mode 100644 index 000000000..7f3ade097 --- /dev/null +++ b/samples/memory/checkpointer-demo.ipynb @@ -0,0 +1,486 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "8208f7d6-391d-4054-9e67-ac0f85878dcd", + "metadata": {}, + "source": [ + "# Bedrock AgentCore Memory Checkpointer Walkthrough\n", + "\n", + "This sample notebook walks through setup and usage of the Bedrock AgentCore Memory Checkpointer with LangGraph. This approach enables saving of conversations and state data to the Memory API for persistent storage, fault tolerance, and human-in-the-loop workflows.\n", + "\n", + "### Setup\n", + "For this notebook you will need:\n", + "1. An Amazon Web Services development account\n", + "2. Bedrock Model Access (i.e. Claude 3.7 Sonnet)\n", + "3. An AgentCore Memory Resource configured (see below section for details)\n", + "\n", + "### AgentCore Memory Resource\n", + "\n", + "Either in the AWS developer portal or using the boto3 library you must create an [AgentCore Memory Resource](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/bedrock-agentcore-control/client/create_memory.html). For just using the `AgentCoreMemorySaver` checkpointer in this notebook, you do not need to specify any specific long-term memory strategies. However, it may be beneficial to supplement this approach with the `AgentCoreMemoryStore` to save and extract conversational insights, so you may want to enable strategies for that use case.\n", + "\n", + "Once you have the Memory enabled and in a `ACTIVE` state, take note of the `memoryId`, we will need it later." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "8eff8706-6715-4981-983b-934561ee0a19", + "metadata": {}, + "outputs": [], + "source": [ + "# Install general dependencies\n", + "from typing import Annotated, Any, Dict, List\n", + "\n", + "from langchain.chat_models import init_chat_model\n", + "from langchain.tools import tool\n", + "\n", + "# Import LangGraph and LangChain components\n", + "from langchain_core.messages import AIMessage, HumanMessage, SystemMessage\n", + "from langchain_core.runnables import RunnableConfig\n", + "from langgraph.graph import END, START, StateGraph\n", + "from langgraph.graph.message import add_messages\n", + "from langgraph.prebuilt import ToolNode, tools_condition\n", + "from typing_extensions import TypedDict" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "fc12fd92-b84a-4d2f-b96d-83d3da08bf70", + "metadata": {}, + "outputs": [], + "source": [ + "# Import the AgentCoreMemorySaver that we will use as a checkpointer\n", + "# from langgraph_checkpoint_aws.checkpoint.agentcore_memory.saver import AgentCoreMemorySaver\n", + "from agentcore_memory.saver import AgentCoreMemorySaver\n", + "\n", + "import logging\n", + "logging.getLogger().setLevel(logging.DEBUG)" + ] + }, + { + "cell_type": "markdown", + "id": "51d91a14-541c-426c-a52c-5fdcf29f8953", + "metadata": {}, + "source": [ + "## AgentCore Memory Configuration\n", + "- `REGION` corresponds to the AWS region that your resources are present in, these are passed to the `AgentCoreMemorySaver`.\n", + "- `MEMORY_ID` corresponds to your top level AgentCore Memory resource. Within this resource we will store checkpoints for multiple actors and sessions\n", + "- `MODEL_ID` this is the bedrock model that will power our LangGraph agent through Bedrock Converse.\n", + "\n", + "We will use the `MEMORY_ID` and any additional boto3 client keyword args (in our case, `REGION`) to instantiate our checkpointer." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "226d094c-a05d-4f88-851d-cc42ff63ef11", + "metadata": {}, + "outputs": [], + "source": [ + "REGION = \"ap-southeast-2\"\n", + "MEMORY_ID = \"memory_tnpk0-AMDRU75vYn\"\n", + "MODEL_ID = \"apac.anthropic.claude-3-7-sonnet-20250219-v1:0\"\n", + "\n", + "# Initialize checkpointer for state persistence\n", + "checkpointer = AgentCoreMemorySaver(MEMORY_ID, region_name=REGION)\n", + "\n", + "# Initialize LLM\n", + "llm = init_chat_model(MODEL_ID, model_provider=\"bedrock_converse\", region_name=REGION)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "a8a1e31f-137a-4bd6-b959-4dc7d8176949", + "metadata": {}, + "outputs": [], + "source": [ + "# Define agent state and LangGraph graph builder\n", + "class State(TypedDict):\n", + " messages: Annotated[list, add_messages]\n", + "\n", + "\n", + "graph_builder = StateGraph(State)" + ] + }, + { + "cell_type": "markdown", + "id": "c56c77aa-4b19-49a5-af3c-db2b4798384b", + "metadata": {}, + "source": [ + "## Define our tools\n", + "\n", + "For this demo notebook we will only give our agent access to two simple tools, adding and multiplying." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "dd81ba1c-3fb8-474c-ae40-0ee7c62ca234", + "metadata": {}, + "outputs": [], + "source": [ + "@tool\n", + "def add(a: int, b: int):\n", + " \"\"\"Add two integers and return the result\"\"\"\n", + " return a + b\n", + "\n", + "\n", + "@tool\n", + "def multiply(a: int, b: int):\n", + " \"\"\"Multiply two integers and return the result\"\"\"\n", + " return a * b\n", + "\n", + "\n", + "tools = [add, multiply]\n", + "\n", + "# Bind the tools to our LLM so it can understand their structure\n", + "llm_with_tools = llm.bind_tools(tools)" + ] + }, + { + "cell_type": "markdown", + "id": "651b6e3f-8b41-4c07-8a32-680666b2661e", + "metadata": {}, + "source": [ + "## Build our LangGraph agent graph\n", + "\n", + "Our agent will have a few simple nodes, mainly a chatbot node and a tool node. This will enable our chatbot to use the add and multiply tools as much as it needs and then return a response. We will visualize this graph below." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "9e63decd-2e15-4f12-9e82-6a0aac3f1892", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "✅ Agent with checkpointing compiled successfully\n" + ] + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Our chatbot node will contain the LLM invocation\n", + "def chatbot(state: State):\n", + " return {\"messages\": [llm_with_tools.invoke(state[\"messages\"])]}\n", + "\n", + "\n", + "graph_builder.add_node(\"chatbot\", chatbot)\n", + "tool_node = ToolNode(tools=tools)\n", + "graph_builder.add_node(\"tools\", tool_node)\n", + "\n", + "graph_builder.add_conditional_edges(\n", + " \"chatbot\",\n", + " tools_condition,\n", + ")\n", + "\n", + "# Finish off the other edges\n", + "graph_builder.add_edge(START, \"chatbot\")\n", + "graph_builder.add_edge(\"tools\", \"chatbot\")\n", + "graph_builder.add_edge(\"chatbot\", END)\n", + "\n", + "# Compile the graph with our AgentCoreMemorySaver as the checkpointer\n", + "graph = graph_builder.compile(checkpointer=checkpointer)\n", + "\n", + "print(\"✅ Agent with checkpointing compiled successfully\")\n", + "graph" + ] + }, + { + "cell_type": "markdown", + "id": "b6cbbb3a-f85c-42f5-bbfb-40f5f6b8b48c", + "metadata": {}, + "source": [ + "## IMPORTANT: State and Config\n", + "\n", + "\n", + "### LangGraph RuntimeConfig\n", + "In LangGraph, config is a `RuntimeConfig` that contains attributes that are necessary at invocation time, for example user IDs or session IDs. For the `AgentCoreMemorySaver`, `thread_id` and `actor_id` must be set in the config. For instance, your AgentCore invocation endpoint could assign this based on the identity or user ID of the caller. Additional documentation here: [https://langchain-ai.github.io/langgraphjs/how-tos/configuration/](https://langchain-ai.github.io/langgraphjs/how-tos/configuration/)\n", + "\n", + "### LangGraph Invocation State\n", + "The state that is passed to the `graph.invoke` method is an instance of our `State` we defined earlier: \n", + "```\n", + "class State(TypedDict):\n", + " messages: Annotated[list, add_messages]\n", + "```\n", + "As you can see, we have defined messages with the `add_messages` annotation. This means that each node (or invocation) state can return a `messages` key that will simply add the value to the latest value of the `State`. So for example, our chatbot node may return a response such as `messages=[\"Hi of course I can help you with that\"]` and it will be appended to our state messages below instead of overwriting it. For more information on State, see the docs here: [https://langchain-ai.github.io/langgraph/concepts/low_level/#working-with-messages-in-graph-state](https://langchain-ai.github.io/langgraph/concepts/low_level/#working-with-messages-in-graph-state)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "859fee36-1eff-4713-9c3b-387b702c0301", + "metadata": {}, + "outputs": [], + "source": [ + "# For our demo we will have dummy thread and actor IDs\n", + "config = {\n", + " \"configurable\": {\n", + " \"thread_id\": \"thread-0\",\n", + " \"actor_id\": \"user-0\",\n", + " }\n", + "}\n", + "\n", + "# This invocation state is where you would fill in a query prompt from the user at /invocations\n", + "invocation_state = {\n", + " \"messages\": [HumanMessage(\"What is 1337 times 200 + 17? Follow pemdas.\")]\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "96044de0-2d32-4811-ac30-43f4416f4303", + "metadata": {}, + "source": [ + "### Run the agent" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "c2bc4869-58cd-4914-9a7a-8d39ecd16226", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'messages': [HumanMessage(content='What is 1337 times 200 + 17? Follow pemdas.', additional_kwargs={}, response_metadata={}, id='b48c5506-2d06-4ba8-8da5-e315ad5cc654'),\n", + " AIMessage(content=[{'type': 'text', 'text': \"I'll solve this step by step following PEMDAS (Parentheses, Exponents, Multiplication/Division, Addition/Subtraction).\\n\\nAccording to PEMDAS, I should perform multiplication before addition. So I'll first multiply 1337 by 200, and then add 17 to that result.\\n\\nLet me calculate 1337 × 200:\"}, {'type': 'tool_use', 'name': 'multiply', 'input': {'a': 1337, 'b': 200}, 'id': 'tooluse_zZPvbVtOR2u2uxjQMtAq_A'}], additional_kwargs={}, response_metadata={'ResponseMetadata': {'RequestId': 'e1c53591-ec66-4bcc-851e-74e43091ff2f', 'HTTPStatusCode': 200, 'HTTPHeaders': {'date': 'Mon, 15 Sep 2025 13:45:15 GMT', 'content-type': 'application/json', 'content-length': '691', 'connection': 'keep-alive', 'x-amzn-requestid': 'e1c53591-ec66-4bcc-851e-74e43091ff2f'}, 'RetryAttempts': 0}, 'stopReason': 'tool_use', 'metrics': {'latencyMs': [3046]}, 'model_name': 'apac.anthropic.claude-3-7-sonnet-20250219-v1:0'}, id='run--5727d585-bf6b-4ca8-9b15-9e9275e112e6-0', tool_calls=[{'name': 'multiply', 'args': {'a': 1337, 'b': 200}, 'id': 'tooluse_zZPvbVtOR2u2uxjQMtAq_A', 'type': 'tool_call'}], usage_metadata={'input_tokens': 472, 'output_tokens': 153, 'total_tokens': 625, 'input_token_details': {'cache_creation': 0, 'cache_read': 0}}),\n", + " ToolMessage(content='267400', name='multiply', id='25e90eed-b36e-474a-b9f6-74a6294814c6', tool_call_id='tooluse_zZPvbVtOR2u2uxjQMtAq_A'),\n", + " AIMessage(content=[{'type': 'text', 'text': \"Now I'll add 17 to this result:\"}, {'type': 'tool_use', 'name': 'add', 'input': {'a': 267400, 'b': 17}, 'id': 'tooluse_RkwUNdsXT16Yh8dktOyMRQ'}], additional_kwargs={}, response_metadata={'ResponseMetadata': {'RequestId': 'a9c3bb70-784b-4550-a162-d0380023e801', 'HTTPStatusCode': 200, 'HTTPHeaders': {'date': 'Mon, 15 Sep 2025 13:45:17 GMT', 'content-type': 'application/json', 'content-length': '425', 'connection': 'keep-alive', 'x-amzn-requestid': 'a9c3bb70-784b-4550-a162-d0380023e801'}, 'RetryAttempts': 0}, 'stopReason': 'tool_use', 'metrics': {'latencyMs': [1589]}, 'model_name': 'apac.anthropic.claude-3-7-sonnet-20250219-v1:0'}, id='run--5b2347b4-54e9-4c72-b9ff-ca3b111688ac-0', tool_calls=[{'name': 'add', 'args': {'a': 267400, 'b': 17}, 'id': 'tooluse_RkwUNdsXT16Yh8dktOyMRQ', 'type': 'tool_call'}], usage_metadata={'input_tokens': 638, 'output_tokens': 82, 'total_tokens': 720, 'input_token_details': {'cache_creation': 0, 'cache_read': 0}}),\n", + " ToolMessage(content='267417', name='add', id='b5a708d3-5f32-4f34-93c5-1383dcc11479', tool_call_id='tooluse_RkwUNdsXT16Yh8dktOyMRQ'),\n", + " AIMessage(content='Following PEMDAS, the answer to 1337 × 200 + 17 is 267,417.', additional_kwargs={}, response_metadata={'ResponseMetadata': {'RequestId': '079b938e-969a-44ad-a8b9-c9d936fcc238', 'HTTPStatusCode': 200, 'HTTPHeaders': {'date': 'Mon, 15 Sep 2025 13:45:19 GMT', 'content-type': 'application/json', 'content-length': '354', 'connection': 'keep-alive', 'x-amzn-requestid': '079b938e-969a-44ad-a8b9-c9d936fcc238'}, 'RetryAttempts': 0}, 'stopReason': 'end_turn', 'metrics': {'latencyMs': [1397]}, 'model_name': 'apac.anthropic.claude-3-7-sonnet-20250219-v1:0'}, id='run--bdb9b4ac-1f5a-489a-a956-d879aac4d942-0', usage_metadata={'input_tokens': 733, 'output_tokens': 31, 'total_tokens': 764, 'input_token_details': {'cache_creation': 0, 'cache_read': 0}}),\n", + " HumanMessage(content='Create a LaTeX formula in markdown that shows the previous calculation and results', additional_kwargs={}, response_metadata={}, id='ad93df1b-1eac-4e08-8805-b42ae9f59462'),\n", + " AIMessage(content=\"Here's a LaTeX formula in markdown that shows the previous calculation and results:\\n\\n```math\\n1337 \\\\times 200 + 17 = 267400 + 17 = 267417\\n```\\n\\nThis displays the calculation step by step, showing the multiplication of 1337 by 200 first (following PEMDAS - order of operations), which equals 267400, then the addition of 17 to get the final result of 267417.\", additional_kwargs={}, response_metadata={'ResponseMetadata': {'RequestId': 'baa5c348-5b8b-40e0-8b69-6465193ce041', 'HTTPStatusCode': 200, 'HTTPHeaders': {'date': 'Mon, 15 Sep 2025 13:45:44 GMT', 'content-type': 'application/json', 'content-length': '657', 'connection': 'keep-alive', 'x-amzn-requestid': 'baa5c348-5b8b-40e0-8b69-6465193ce041'}, 'RetryAttempts': 0}, 'stopReason': 'end_turn', 'metrics': {'latencyMs': [2624]}, 'model_name': 'apac.anthropic.claude-3-7-sonnet-20250219-v1:0'}, id='run--1faf142a-b246-4870-9552-09b5f6b9d05b-0', usage_metadata={'input_tokens': 781, 'output_tokens': 106, 'total_tokens': 887, 'input_token_details': {'cache_creation': 0, 'cache_read': 0}}),\n", + " HumanMessage(content='What is 1337 times 200 + 17? Follow pemdas.', additional_kwargs={}, response_metadata={}, id='e6f0b5e6-a8c6-4a7a-96cc-0a1b6f11931f'),\n", + " AIMessage(content=[{'type': 'text', 'text': \"I'll solve this step by step following PEMDAS (Parentheses, Exponents, Multiplication/Division, Addition/Subtraction).\\n\\nAccording to PEMDAS, I should perform multiplication before addition. So I'll first multiply 1337 by 200, and then add 17 to that result.\\n\\nLet me calculate 1337 × 200:\"}, {'type': 'tool_use', 'name': 'multiply', 'input': {'a': 1337, 'b': 200}, 'id': 'tooluse_20HotERoStqxI3CgbvcQMg'}], additional_kwargs={}, response_metadata={'ResponseMetadata': {'RequestId': '51736bf1-9e9e-483a-8b60-a00b99c0a4ff', 'HTTPStatusCode': 200, 'HTTPHeaders': {'date': 'Mon, 15 Sep 2025 13:50:16 GMT', 'content-type': 'application/json', 'content-length': '692', 'connection': 'keep-alive', 'x-amzn-requestid': '51736bf1-9e9e-483a-8b60-a00b99c0a4ff'}, 'RetryAttempts': 0}, 'stopReason': 'tool_use', 'metrics': {'latencyMs': [2453]}, 'model_name': 'apac.anthropic.claude-3-7-sonnet-20250219-v1:0'}, id='run--0ca8262a-6d70-4a50-b13b-d6df5f478149-0', tool_calls=[{'name': 'multiply', 'args': {'a': 1337, 'b': 200}, 'id': 'tooluse_20HotERoStqxI3CgbvcQMg', 'type': 'tool_call'}], usage_metadata={'input_tokens': 909, 'output_tokens': 153, 'total_tokens': 1062, 'input_token_details': {'cache_creation': 0, 'cache_read': 0}}),\n", + " ToolMessage(content='267400', name='multiply', id='a43f5f72-d03f-4953-b748-f882c3b98b95', tool_call_id='tooluse_20HotERoStqxI3CgbvcQMg'),\n", + " AIMessage(content=[{'type': 'text', 'text': \"Now I'll add 17 to this result:\"}, {'type': 'tool_use', 'name': 'add', 'input': {'a': 267400, 'b': 17}, 'id': 'tooluse_dXOCeJXhS1e-p3DGxhUeFg'}], additional_kwargs={}, response_metadata={'ResponseMetadata': {'RequestId': '618612aa-4795-4e4b-9c4d-850d87ccd805', 'HTTPStatusCode': 200, 'HTTPHeaders': {'date': 'Mon, 15 Sep 2025 13:50:18 GMT', 'content-type': 'application/json', 'content-length': '427', 'connection': 'keep-alive', 'x-amzn-requestid': '618612aa-4795-4e4b-9c4d-850d87ccd805'}, 'RetryAttempts': 0}, 'stopReason': 'tool_use', 'metrics': {'latencyMs': [1722]}, 'model_name': 'apac.anthropic.claude-3-7-sonnet-20250219-v1:0'}, id='run--755de27f-2d2d-4804-b8b8-2d2fddc2f86e-0', tool_calls=[{'name': 'add', 'args': {'a': 267400, 'b': 17}, 'id': 'tooluse_dXOCeJXhS1e-p3DGxhUeFg', 'type': 'tool_call'}], usage_metadata={'input_tokens': 1075, 'output_tokens': 82, 'total_tokens': 1157, 'input_token_details': {'cache_creation': 0, 'cache_read': 0}}),\n", + " ToolMessage(content='267417', name='add', id='118bff77-abcf-4517-96e4-f60b80d5604b', tool_call_id='tooluse_dXOCeJXhS1e-p3DGxhUeFg'),\n", + " AIMessage(content='Following PEMDAS, the answer to 1337 × 200 + 17 is 267,417.', additional_kwargs={}, response_metadata={'ResponseMetadata': {'RequestId': '3abe3820-54cf-456f-95a0-61fcef15dcc4', 'HTTPStatusCode': 200, 'HTTPHeaders': {'date': 'Mon, 15 Sep 2025 13:50:19 GMT', 'content-type': 'application/json', 'content-length': '356', 'connection': 'keep-alive', 'x-amzn-requestid': '3abe3820-54cf-456f-95a0-61fcef15dcc4'}, 'RetryAttempts': 0}, 'stopReason': 'end_turn', 'metrics': {'latencyMs': [1186]}, 'model_name': 'apac.anthropic.claude-3-7-sonnet-20250219-v1:0'}, id='run--d653f490-c780-423a-ab5e-24130110daaf-0', usage_metadata={'input_tokens': 1170, 'output_tokens': 31, 'total_tokens': 1201, 'input_token_details': {'cache_creation': 0, 'cache_read': 0}})]}" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "graph.invoke(invocation_state, config=config)" + ] + }, + { + "cell_type": "markdown", + "id": "f0d28db8-c9cc-4fa1-922d-89476e9f71c5", + "metadata": {}, + "source": [ + "## Inspect the current state with AgentCoreMemory\n", + "\n", + "Under the hood when you call `graph.get_state(config)` it calls the checkpointer to retrieve the latest checkpoint saved for our actor and session. We can take a look at the values saved at this point in the conversation from `messages`." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "60042ec5-cd64-48f3-bd7d-cb7ca9e21b39", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "human: What is 1337 times 200 + 17? Follow pemdas.\n", + "=========================================\n", + "ai: I'll solve this step by step following PEMDAS (Parentheses, Exponents, Multiplication/Division, Addition/Subtraction).\n", + "\n", + "According to PEMDAS, I should perform multiplication before addition. So I'll first multiply 1337 by 200, and then add 17 to that result.\n", + "\n", + "Let me calculate 1337 × 200:\n", + "=========================================\n", + "tool: 267400\n", + "=========================================\n", + "ai: Now I'll add 17 to this result:\n", + "=========================================\n", + "tool: 267417\n", + "=========================================\n", + "ai: Following PEMDAS, the answer to 1337 × 200 + 17 is 267,417.\n", + "=========================================\n", + "human: Create a LaTeX formula in markdown that shows the previous calculation and results\n", + "=========================================\n", + "ai: Here's a LaTeX formula in markdown that shows the previous calculation and results:\n", + "\n", + "```math\n", + "1337 \\times 200 + 17 = 267400 + 17 = 267417\n", + "```\n", + "\n", + "This displays the calculation step by step, showing the multiplication of 1337 by 200 first (following PEMDAS - order of operations), which equals 267400, then the addition of 17 to get the final result of 267417.\n", + "=========================================\n", + "human: What is 1337 times 200 + 17? Follow pemdas.\n", + "=========================================\n", + "ai: I'll solve this step by step following PEMDAS (Parentheses, Exponents, Multiplication/Division, Addition/Subtraction).\n", + "\n", + "According to PEMDAS, I should perform multiplication before addition. So I'll first multiply 1337 by 200, and then add 17 to that result.\n", + "\n", + "Let me calculate 1337 × 200:\n", + "=========================================\n", + "tool: 267400\n", + "=========================================\n", + "ai: Now I'll add 17 to this result:\n", + "=========================================\n", + "tool: 267417\n", + "=========================================\n", + "ai: Following PEMDAS, the answer to 1337 × 200 + 17 is 267,417.\n", + "=========================================\n" + ] + } + ], + "source": [ + "for message in graph.get_state(config).values.get(\"messages\"):\n", + " print(f\"{message.type}: {message.text()}\")\n", + " print(\"=========================================\")" + ] + }, + { + "cell_type": "markdown", + "id": "3aa0c9bd-6fb6-467d-b51a-84696238b4fa", + "metadata": {}, + "source": [ + "## Look at a previous checkpoints during execution\n", + "Using the `graph.get_state_history(config)` we will see the checkpoints that were saved, then inspect a previous checkpoint's values for `messages`. Checkpoints are listed so the most recent checkpoints appear first." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "c2232c2b-154d-4b50-93b0-925eb88e67c8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(Checkpoint ID: 1f0923ae-b162-61aa-800f-bba4b057543e) # of messages in state: 14\n", + "(Checkpoint ID: 1f0923ae-a40f-61e2-800e-bc65f256d255) # of messages in state: 13\n", + "(Checkpoint ID: 1f0923ae-a3ff-6c38-800d-505d7ed13c25) # of messages in state: 12\n", + "(Checkpoint ID: 1f0923ae-918a-6f26-800c-7b2ebf1316a2) # of messages in state: 11\n", + "(Checkpoint ID: 1f0923ae-9173-629a-800b-b53ad08339fe) # of messages in state: 10\n", + "(Checkpoint ID: 1f0923ae-72c7-6ab2-800a-5b5912be455a) # of messages in state: 9\n", + "(Checkpoint ID: 1f0923ae-72bd-6e0e-8009-33e40701ee42) # of messages in state: 8\n", + "(Checkpoint ID: 1f0923a4-6e20-6324-8008-16ed6f369bd9) # of messages in state: 8\n", + "(Checkpoint ID: 1f0923a4-5323-6b48-8007-e1247fbfc0cb) # of messages in state: 7\n", + "(Checkpoint ID: 1f0923a4-530f-6a62-8006-4b60a52adca0) # of messages in state: 6\n", + "(Checkpoint ID: 1f0923a3-7efb-649c-8005-acd1eb738210) # of messages in state: 6\n", + "(Checkpoint ID: 1f0923a3-6fa2-62a2-8004-a44cd9675857) # of messages in state: 5\n", + "(Checkpoint ID: 1f0923a3-6f9b-69d4-8003-6100b95bc700) # of messages in state: 4\n", + "(Checkpoint ID: 1f0923a3-5e70-6da8-8002-40a909cdb9bd) # of messages in state: 3\n", + "(Checkpoint ID: 1f0923a3-5e61-6858-8001-256b044cf577) # of messages in state: 2\n", + "(Checkpoint ID: 1f0923a3-3f5c-6f5c-8000-7ce3a145a6ed) # of messages in state: 1\n", + "(Checkpoint ID: 1f0923a3-3f4f-646a-bfff-6d736ba7793a) # of messages in state: 0\n" + ] + } + ], + "source": [ + "for checkpoint in graph.get_state_history(config):\n", + " print(\n", + " f\"(Checkpoint ID: {checkpoint.config['configurable']['checkpoint_id']}) # of messages in state: {len(checkpoint.values.get('messages'))}\"\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "ab07194f-49b4-4fb9-a74b-d86b970cace6", + "metadata": {}, + "source": [ + "## Continue the conversation\n", + "By using the same config with our session and actor from before, we can continue the conversation by invoking our LangGraph agent with a new invocation state. The checkpointer in the background will take care of loading in context so that all the previous messages from the first interaction are loaded in." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "b7117c6a-d1d5-4866-aee9-2b7229fd73fd", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "AIMessage(content=\"Here's a LaTeX formula in markdown that shows the previous calculation and results:\\n\\n```math\\n1337 \\\\times 200 + 17 = 267400 + 17 = 267417\\n```\\n\\nThis displays the calculation step by step, showing the multiplication of 1337 by 200 first (following PEMDAS - order of operations), which equals 267400, then the addition of 17 to get the final result of 267417.\", additional_kwargs={}, response_metadata={'ResponseMetadata': {'RequestId': 'cf97747e-d4d4-49bf-b801-56253cc788c2', 'HTTPStatusCode': 200, 'HTTPHeaders': {'date': 'Mon, 15 Sep 2025 13:50:36 GMT', 'content-type': 'application/json', 'content-length': '659', 'connection': 'keep-alive', 'x-amzn-requestid': 'cf97747e-d4d4-49bf-b801-56253cc788c2'}, 'RetryAttempts': 0}, 'stopReason': 'end_turn', 'metrics': {'latencyMs': [2207]}, 'model_name': 'apac.anthropic.claude-3-7-sonnet-20250219-v1:0'}, id='run--88043c79-6656-47e0-b489-ad0219b553f0-0', usage_metadata={'input_tokens': 1218, 'output_tokens': 106, 'total_tokens': 1324, 'input_token_details': {'cache_creation': 0, 'cache_read': 0}})" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# This invocation state is where you would fill in a query prompt from the user at /invocations\n", + "invocation_state = {\n", + " \"messages\": [\n", + " HumanMessage(\n", + " \"Create a LaTeX formula in markdown that shows the previous calculation and results\"\n", + " )\n", + " ]\n", + "}\n", + "graph.invoke(invocation_state, config=config)\n", + "\n", + "# Display the response\n", + "graph.get_state(config).values.get(\"messages\")[-1]" + ] + }, + { + "cell_type": "markdown", + "id": "6fa7bd51-0ece-47d6-aae3-2a9041d98645", + "metadata": {}, + "source": [ + "## Wrapping Up\n", + "\n", + "As you can see, the AgentCore checkpointer is very powerful for persisting conversational and graph state in the background. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "eda8649a-d48f-467c-94b9-99dd8fb2c078", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.11.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 2a8a920cfce95b55a30acc8cc3f1b9eabc36097c Mon Sep 17 00:00:00 2001 From: Jack Gordley Date: Wed, 17 Sep 2025 16:40:49 -0700 Subject: [PATCH 2/8] Moving agentcore saver files to new directory --- .../langgraph_checkpoint_aws/__init__.py | 2 +- .../constants.py | 0 .../agentcore_memory => agentcore}/helpers.py | 14 +- .../agentcore_memory => agentcore}/models.py | 0 .../agentcore_memory => agentcore}/saver.py | 8 +- .../agentcore_memory_checkpointer.ipynb | 465 +++++++++++++++++ samples/memory/checkpointer-demo.ipynb | 486 ------------------ 7 files changed, 477 insertions(+), 498 deletions(-) rename libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/{checkpoint/agentcore_memory => agentcore}/constants.py (100%) rename libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/{checkpoint/agentcore_memory => agentcore}/helpers.py (96%) rename libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/{checkpoint/agentcore_memory => agentcore}/models.py (100%) rename libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/{checkpoint/agentcore_memory => agentcore}/saver.py (97%) create mode 100644 samples/memory/agentcore_memory_checkpointer.ipynb delete mode 100644 samples/memory/checkpointer-demo.ipynb diff --git a/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/__init__.py b/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/__init__.py index 828801800..a022b97cf 100644 --- a/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/__init__.py +++ b/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/__init__.py @@ -2,7 +2,7 @@ LangGraph Checkpoint AWS - A LangChain checkpointer implementation using Bedrock Session Management Service. """ -from langgraph_checkpoint_aws.checkpoint.agentcore_memory.saver import ( +from langgraph_checkpoint_aws.agentcore.saver import ( AgentCoreMemorySaver, ) diff --git a/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/checkpoint/agentcore_memory/constants.py b/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/constants.py similarity index 100% rename from libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/checkpoint/agentcore_memory/constants.py rename to libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/constants.py diff --git a/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/checkpoint/agentcore_memory/helpers.py b/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/helpers.py similarity index 96% rename from libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/checkpoint/agentcore_memory/helpers.py rename to libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/helpers.py index 5361fcd12..bd601aabc 100644 --- a/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/checkpoint/agentcore_memory/helpers.py +++ b/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/helpers.py @@ -14,11 +14,11 @@ import boto3 from langgraph.checkpoint.base import CheckpointTuple, SerializerProtocol -from langgraph_checkpoint_aws.checkpoint.agentcore_memory.constants import ( +from langgraph_checkpoint_aws.agentcore.constants import ( EMPTY_CHANNEL_VALUE, EventDecodingError, ) -from langgraph_checkpoint_aws.checkpoint.agentcore_memory.models import ( +from langgraph_checkpoint_aws.agentcore.models import ( ChannelDataEvent, CheckpointerConfig, CheckpointEvent, @@ -160,18 +160,18 @@ def store_events_batch( ) def get_events( - self, session_id: str, actor_id: str, max_results: int = 100 + self, session_id: str, actor_id: str, max_results: int = None ) -> List[EventType]: """Retrieve events from AgentCore Memory.""" all_events = [] next_token = None - while len(all_events) < max_results: + while True: params = { "memoryId": self.memory_id, "actorId": actor_id, "sessionId": session_id, - "maxResults": min(100, max_results - len(all_events)), + "maxResults": 100, "includePayloads": True, } @@ -191,10 +191,10 @@ def get_events( logger.warning(f"Failed to decode event: {e}") next_token = response.get("nextToken") - if not next_token or len(all_events) >= max_results: + if not next_token or (max_results and len(all_events) >= max_results): break - return all_events[:max_results] + return all_events def delete_events(self, session_id: str, actor_id: str) -> None: """Delete all events for a session.""" diff --git a/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/checkpoint/agentcore_memory/models.py b/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/models.py similarity index 100% rename from libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/checkpoint/agentcore_memory/models.py rename to libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/models.py diff --git a/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/checkpoint/agentcore_memory/saver.py b/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/saver.py similarity index 97% rename from libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/checkpoint/agentcore_memory/saver.py rename to libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/saver.py index e56427333..e8100f281 100644 --- a/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/checkpoint/agentcore_memory/saver.py +++ b/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/saver.py @@ -20,16 +20,16 @@ get_checkpoint_metadata, ) -from langgraph_checkpoint_aws.checkpoint.agentcore_memory.constants import ( +from langgraph_checkpoint_aws.agentcore.constants import ( EMPTY_CHANNEL_VALUE, InvalidConfigError, ) -from langgraph_checkpoint_aws.checkpoint.agentcore_memory.helpers import ( +from langgraph_checkpoint_aws.agentcore.helpers import ( CheckpointEventClient, EventProcessor, EventSerializer, ) -from langgraph_checkpoint_aws.checkpoint.agentcore_memory.models import ( +from langgraph_checkpoint_aws.agentcore.models import ( ChannelDataEvent, CheckpointerConfig, CheckpointEvent, @@ -110,7 +110,7 @@ def list( config_checkpoint_id = get_checkpoint_id(config) if config else None events = self.checkpoint_event_client.get_events( - checkpoint_config.session_id, checkpoint_config.actor_id + checkpoint_config.session_id, checkpoint_config.actor_id, limit ) checkpoints, writes_by_checkpoint, channel_data = self.processor.process_events( diff --git a/samples/memory/agentcore_memory_checkpointer.ipynb b/samples/memory/agentcore_memory_checkpointer.ipynb new file mode 100644 index 000000000..e83223e2d --- /dev/null +++ b/samples/memory/agentcore_memory_checkpointer.ipynb @@ -0,0 +1,465 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "8208f7d6-391d-4054-9e67-ac0f85878dcd", + "metadata": {}, + "source": [ + "# Bedrock AgentCore Memory Checkpointer Walkthrough\n", + "\n", + "This sample notebook walks through setup and usage of the Bedrock AgentCore Memory Checkpointer with LangGraph. This approach enables saving of conversations and state data to the Memory API for persistent storage, fault tolerance, and human-in-the-loop workflows.\n", + "\n", + "### Setup\n", + "For this notebook you will need:\n", + "1. An Amazon Web Services development account\n", + "2. Bedrock Model Access (i.e. Claude 3.7 Sonnet)\n", + "3. An AgentCore Memory Resource configured (see below section for details)\n", + "\n", + "### AgentCore Memory Resource\n", + "\n", + "Either in the AWS developer portal or using the boto3 library you must create an [AgentCore Memory Resource](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/bedrock-agentcore-control/client/create_memory.html). For just using the `AgentCoreMemorySaver` checkpointer in this notebook, you do not need to specify any specific long-term memory strategies. However, it may be beneficial to supplement this approach with the `AgentCoreMemoryStore` to save and extract conversational insights, so you may want to enable strategies for that use case.\n", + "\n", + "Once you have the Memory enabled and in a `ACTIVE` state, take note of the `memoryId`, we will need it later." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e6acbf4e", + "metadata": {}, + "outputs": [], + "source": [ + "%pip install langchain" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "8eff8706-6715-4981-983b-934561ee0a19", + "metadata": {}, + "outputs": [], + "source": [ + "# Import LangGraph and LangChain components\n", + "from langchain.chat_models import init_chat_model\n", + "from langchain.tools import tool\n", + "from langgraph.prebuilt import create_react_agent" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "fc12fd92-b84a-4d2f-b96d-83d3da08bf70", + "metadata": {}, + "outputs": [], + "source": [ + "# Import the AgentCoreMemorySaver that we will use as a checkpointer\n", + "from langgraph_checkpoint_aws import AgentCoreMemorySaver\n", + "\n", + "import logging\n", + "logging.getLogger().setLevel(logging.DEBUG)" + ] + }, + { + "cell_type": "markdown", + "id": "51d91a14-541c-426c-a52c-5fdcf29f8953", + "metadata": {}, + "source": [ + "## AgentCore Memory Configuration\n", + "- `REGION` corresponds to the AWS region that your resources are present in, these are passed to the `AgentCoreMemorySaver`.\n", + "- `MEMORY_ID` corresponds to your top level AgentCore Memory resource. Within this resource we will store checkpoints for multiple actors and sessions\n", + "- `MODEL_ID` this is the bedrock model that will power our LangGraph agent through Bedrock Converse.\n", + "\n", + "We will use the `MEMORY_ID` and any additional boto3 client keyword args (in our case, `REGION`) to instantiate our checkpointer." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "226d094c-a05d-4f88-851d-cc42ff63ef11", + "metadata": {}, + "outputs": [], + "source": [ + "REGION = \"us-west-2\"\n", + "MEMORY_ID = \"YOUR_MEMORY_ID\"\n", + "MODEL_ID = \"us.anthropic.claude-3-7-sonnet-20250219-v1:0\"\n", + "\n", + "# Initialize checkpointer for state persistence\n", + "checkpointer = AgentCoreMemorySaver(MEMORY_ID, region_name=REGION)\n", + "\n", + "# Initialize LLM\n", + "llm = init_chat_model(MODEL_ID, model_provider=\"bedrock_converse\", region_name=REGION)" + ] + }, + { + "cell_type": "markdown", + "id": "c56c77aa-4b19-49a5-af3c-db2b4798384b", + "metadata": {}, + "source": [ + "## Define our tools\n", + "\n", + "For this demo notebook we will only give our agent access to two simple tools, adding and multiplying." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "dd81ba1c-3fb8-474c-ae40-0ee7c62ca234", + "metadata": {}, + "outputs": [], + "source": [ + "@tool\n", + "def add(a: int, b: int):\n", + " \"\"\"Add two integers and return the result\"\"\"\n", + " return a + b\n", + "\n", + "\n", + "@tool\n", + "def multiply(a: int, b: int):\n", + " \"\"\"Multiply two integers and return the result\"\"\"\n", + " return a * b\n", + "\n", + "\n", + "tools = [add, multiply]\n", + "\n", + "# Bind the tools to our LLM so it can understand their structure\n", + "llm_with_tools = llm.bind_tools(tools)" + ] + }, + { + "cell_type": "markdown", + "id": "651b6e3f-8b41-4c07-8a32-680666b2661e", + "metadata": {}, + "source": [ + "## Build our LangGraph agent graph\n", + "\n", + "Our agent will have a few simple nodes, mainly a chatbot node and a tool node. This will enable our chatbot to use the add and multiply tools as much as it needs and then return a response. We will visualize this graph below." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "a116a57f", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "graph = create_react_agent(\n", + " model=llm_with_tools,\n", + " tools=[add, multiply],\n", + " prompt=\"You are a helpful assistant\",\n", + " checkpointer=checkpointer,\n", + ")\n", + "\n", + "graph" + ] + }, + { + "cell_type": "markdown", + "id": "b6cbbb3a-f85c-42f5-bbfb-40f5f6b8b48c", + "metadata": {}, + "source": [ + "## IMPORTANT: Input and Config\n", + "\n", + "### Graph Invoke Input\n", + "We only need to pass the newest user message in as an argument `inputs`. This could include other state variables too but for the simple `create_react_agent`, messages are all that's required.\n", + "\n", + "### LangGraph RuntimeConfig\n", + "In LangGraph, config is a `RuntimeConfig` that contains attributes that are necessary at invocation time, for example user IDs or session IDs. For the `AgentCoreMemorySaver`, `thread_id` and `actor_id` must be set in the config. For instance, your AgentCore invocation endpoint could assign this based on the identity or user ID of the caller. Additional documentation here: [https://langchain-ai.github.io/langgraphjs/how-tos/configuration/](https://langchain-ai.github.io/langgraphjs/how-tos/configuration/)\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "859fee36-1eff-4713-9c3b-387b702c0301", + "metadata": {}, + "outputs": [], + "source": [ + "config = {\n", + " \"configurable\": {\n", + " \"thread_id\": \"session-1\", # REQUIRED: This maps to Bedrock AgentCore session_id under the hood\n", + " \"actor_id\": \"react-agent-1\", # REQUIRED: This maps to Bedrock AgentCore actor_id under the hood\n", + " }\n", + "}\n", + "\n", + "inputs = {\"messages\": [{\"role\": \"user\", \"content\": \"What is 1337 times 515321? Then add 412 and return the value to me.\"}]}" + ] + }, + { + "cell_type": "markdown", + "id": "96044de0-2d32-4811-ac30-43f4416f4303", + "metadata": {}, + "source": [ + "### Run the agent" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "c2bc4869-58cd-4914-9a7a-8d39ecd16226", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'agent': {'messages': [AIMessage(content=[{'type': 'text', 'text': \"I'll solve this step-by-step using the available tools.\\n\\nFirst, I'll multiply 1337 by 515321:\"}, {'type': 'tool_use', 'name': 'multiply', 'input': {'a': 1337, 'b': 515321}, 'id': 'tooluse_ufONDYV9T_GDk9uYGKrUBA'}], additional_kwargs={}, response_metadata={'ResponseMetadata': {'RequestId': 'c3d3db9c-985f-4c2a-88f0-4aaa5c673900', 'HTTPStatusCode': 200, 'HTTPHeaders': {'date': 'Wed, 17 Sep 2025 20:37:20 GMT', 'content-type': 'application/json', 'content-length': '497', 'connection': 'keep-alive', 'x-amzn-requestid': 'c3d3db9c-985f-4c2a-88f0-4aaa5c673900'}, 'RetryAttempts': 0}, 'stopReason': 'tool_use', 'metrics': {'latencyMs': [2280]}, 'model_name': 'us.anthropic.claude-3-7-sonnet-20250219-v1:0'}, id='run--364254f3-1cd7-4394-8d27-b52355454b40-0', tool_calls=[{'name': 'multiply', 'args': {'a': 1337, 'b': 515321}, 'id': 'tooluse_ufONDYV9T_GDk9uYGKrUBA', 'type': 'tool_call'}], usage_metadata={'input_tokens': 482, 'output_tokens': 100, 'total_tokens': 582, 'input_token_details': {'cache_creation': 0, 'cache_read': 0}})]}}\n", + "{'tools': {'messages': [ToolMessage(content='688984177', name='multiply', id='db6b78b8-165d-44cd-96b5-cd3b21bdc4a6', tool_call_id='tooluse_ufONDYV9T_GDk9uYGKrUBA')]}}\n", + "{'agent': {'messages': [AIMessage(content=[{'type': 'text', 'text': \"Now I'll add 412 to the result:\"}, {'type': 'tool_use', 'name': 'add', 'input': {'a': 688984177, 'b': 412}, 'id': 'tooluse_PuRt_QohS-2ZW_-Xjp1brQ'}], additional_kwargs={}, response_metadata={'ResponseMetadata': {'RequestId': '8177a4ec-802a-44ca-9323-936e91f3aa69', 'HTTPStatusCode': 200, 'HTTPHeaders': {'date': 'Wed, 17 Sep 2025 20:37:22 GMT', 'content-type': 'application/json', 'content-length': '429', 'connection': 'keep-alive', 'x-amzn-requestid': '8177a4ec-802a-44ca-9323-936e91f3aa69'}, 'RetryAttempts': 0}, 'stopReason': 'tool_use', 'metrics': {'latencyMs': [1642]}, 'model_name': 'us.anthropic.claude-3-7-sonnet-20250219-v1:0'}, id='run--80836015-5332-4d24-aeb6-3ad1cebf826f-0', tool_calls=[{'name': 'add', 'args': {'a': 688984177, 'b': 412}, 'id': 'tooluse_PuRt_QohS-2ZW_-Xjp1brQ', 'type': 'tool_call'}], usage_metadata={'input_tokens': 596, 'output_tokens': 83, 'total_tokens': 679, 'input_token_details': {'cache_creation': 0, 'cache_read': 0}})]}}\n", + "{'tools': {'messages': [ToolMessage(content='688984589', name='add', id='422ab45d-0a3e-4643-9c0b-0cc0073775fc', tool_call_id='tooluse_PuRt_QohS-2ZW_-Xjp1brQ')]}}\n", + "{'agent': {'messages': [AIMessage(content='The final answer is 688,984,589.\\n\\nFirst I multiplied 1337 by 515321, which equals 688,984,177. Then I added 412 to that result, giving us the final value of 688,984,589.', additional_kwargs={}, response_metadata={'ResponseMetadata': {'RequestId': '7a9cbf35-301a-43e4-97dd-fbb8dd64bcfb', 'HTTPStatusCode': 200, 'HTTPHeaders': {'date': 'Wed, 17 Sep 2025 20:37:23 GMT', 'content-type': 'application/json', 'content-length': '465', 'connection': 'keep-alive', 'x-amzn-requestid': '7a9cbf35-301a-43e4-97dd-fbb8dd64bcfb'}, 'RetryAttempts': 0}, 'stopReason': 'end_turn', 'metrics': {'latencyMs': [1785]}, 'model_name': 'us.anthropic.claude-3-7-sonnet-20250219-v1:0'}, id='run--c1484fdb-6b49-4d6b-8248-6fb901808436-0', usage_metadata={'input_tokens': 693, 'output_tokens': 61, 'total_tokens': 754, 'input_token_details': {'cache_creation': 0, 'cache_read': 0}})]}}\n" + ] + } + ], + "source": [ + "for chunk in graph.stream(inputs, stream_mode=\"updates\", config=config):\n", + " print(chunk)" + ] + }, + { + "cell_type": "markdown", + "id": "f0d28db8-c9cc-4fa1-922d-89476e9f71c5", + "metadata": {}, + "source": [ + "## Inspect the current state with AgentCoreMemory\n", + "\n", + "Under the hood when you call `graph.get_state(config)` it calls the checkpointer to retrieve the latest checkpoint saved for our actor and session. We can take a look at the values saved at this point in the conversation from `messages`." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "60042ec5-cd64-48f3-bd7d-cb7ca9e21b39", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "human: What is 1337 times 515321? Then add 412 and return the value to me.\n", + "=========================================\n", + "ai: I'll solve this step-by-step using the available tools.\n", + "\n", + "First, I'll multiply 1337 by 515321:\n", + "=========================================\n", + "tool: 688984177\n", + "=========================================\n", + "ai: Now I'll add 412 to the result:\n", + "=========================================\n", + "tool: 688984589\n", + "=========================================\n", + "ai: The final answer is 688,984,589.\n", + "\n", + "First I multiplied 1337 by 515321, which equals 688,984,177. Then I added 412 to that result, giving us the final value of 688,984,589.\n", + "=========================================\n" + ] + } + ], + "source": [ + "for message in graph.get_state(config).values.get(\"messages\"):\n", + " print(f\"{message.type}: {message.text()}\")\n", + " print(\"=========================================\")" + ] + }, + { + "cell_type": "markdown", + "id": "3aa0c9bd-6fb6-467d-b51a-84696238b4fa", + "metadata": {}, + "source": [ + "## Look at a previous checkpoints during execution\n", + "Using the `graph.get_state_history(config)` we will see the checkpoints that were saved, then inspect a previous checkpoint's values for `messages`. Checkpoints are listed so the most recent checkpoints appear first." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "c2232c2b-154d-4b50-93b0-925eb88e67c8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(Checkpoint ID: 1f094061-dc46-6a54-8005-daf53773fe70) # of messages in state: 6\n", + "(Checkpoint ID: 1f094061-ca8e-63b6-8004-f69f7debce02) # of messages in state: 5\n", + "(Checkpoint ID: 1f094061-ca73-6598-8003-770972f8d2e2) # of messages in state: 4\n", + "(Checkpoint ID: 1f094061-ba19-6efe-8002-94a1cb0a0063) # of messages in state: 3\n", + "(Checkpoint ID: 1f094061-ba06-6f20-8001-ffd58cf51b40) # of messages in state: 2\n", + "(Checkpoint ID: 1f094061-a168-61b2-8000-8a5fb60f2073) # of messages in state: 1\n", + "(Checkpoint ID: 1f094061-a145-62de-bfff-db79a2d38f86) # of messages in state: 0\n" + ] + } + ], + "source": [ + "for checkpoint in graph.get_state_history(config):\n", + " print(\n", + " f\"(Checkpoint ID: {checkpoint.config['configurable']['checkpoint_id']}) # of messages in state: {len(checkpoint.values.get('messages'))}\"\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "ab07194f-49b4-4fb9-a74b-d86b970cace6", + "metadata": {}, + "source": [ + "## Continue the conversation\n", + "By using the same config with our session and actor from before, we can continue the conversation by invoking our LangGraph agent with a new invocation state. The checkpointer in the background will take care of loading in context so that all the previous messages from the first interaction are loaded in." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "b7117c6a-d1d5-4866-aee9-2b7229fd73fd", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'agent': {'messages': [AIMessage(content='The first calculations you asked me to do were to multiply 1337 by 515321, and then add 412 to the result.\\n\\nSpecifically:\\n1. Multiply: 1337 × 515321 = 688,984,177\\n2. Add: 688,984,177 + 412 = 688,984,589\\n\\nI performed these calculations using the multiply and add tools as requested in your initial query.', additional_kwargs={}, response_metadata={'ResponseMetadata': {'RequestId': 'd6d1a259-9195-4d8b-8f3f-92b2043a1f62', 'HTTPStatusCode': 200, 'HTTPHeaders': {'date': 'Wed, 17 Sep 2025 20:38:06 GMT', 'content-type': 'application/json', 'content-length': '605', 'connection': 'keep-alive', 'x-amzn-requestid': 'd6d1a259-9195-4d8b-8f3f-92b2043a1f62'}, 'RetryAttempts': 0}, 'stopReason': 'end_turn', 'metrics': {'latencyMs': [2263]}, 'model_name': 'us.anthropic.claude-3-7-sonnet-20250219-v1:0'}, id='run--1f8ee6ce-ed7f-46fc-b7f2-a97cf5440884-0', usage_metadata={'input_tokens': 768, 'output_tokens': 100, 'total_tokens': 868, 'input_token_details': {'cache_creation': 0, 'cache_read': 0}})]}}\n" + ] + } + ], + "source": [ + "inputs = {\"messages\": [{\"role\": \"user\", \"content\": \"What were the first calculations I asked you to do?\"}]}\n", + "\n", + "for chunk in graph.stream(inputs, stream_mode=\"updates\", config=config):\n", + " print(chunk)" + ] + }, + { + "cell_type": "markdown", + "id": "dc602c5b-e6b3-4099-820a-a82266039849", + "metadata": {}, + "source": [ + "## Start a new chat by using a new session ID\n", + "\n", + "By providing a new thread_id, a new conversation will be started (while still persisting the old conversation in AgentCore Memory)." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "8ce88359-ee1e-44bd-9095-ad22b880dda8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'agent': {'messages': [AIMessage(content=\"I don't see that you've asked me to multiply or add any specific values yet. While I have tools available to perform addition and multiplication, you haven't provided the numbers to calculate.\\n\\nWould you like me to:\\n1. Multiply two numbers together, or\\n2. Add two numbers together?\\n\\nIf so, please provide the specific values you'd like me to use for the calculation.\", additional_kwargs={}, response_metadata={'ResponseMetadata': {'RequestId': '5a7e1c01-9023-4d23-9491-2d2c46f347a9', 'HTTPStatusCode': 200, 'HTTPHeaders': {'date': 'Wed, 17 Sep 2025 20:39:41 GMT', 'content-type': 'application/json', 'content-length': '666', 'connection': 'keep-alive', 'x-amzn-requestid': '5a7e1c01-9023-4d23-9491-2d2c46f347a9'}, 'RetryAttempts': 0}, 'stopReason': 'end_turn', 'metrics': {'latencyMs': [2441]}, 'model_name': 'us.anthropic.claude-3-7-sonnet-20250219-v1:0'}, id='run--58b14eb8-5244-40e9-aa13-6d9ef82f1997-0', usage_metadata={'input_tokens': 470, 'output_tokens': 85, 'total_tokens': 555, 'input_token_details': {'cache_creation': 0, 'cache_read': 0}})]}}\n" + ] + } + ], + "source": [ + "config = {\n", + " \"configurable\": {\n", + " \"thread_id\": \"session-2\", # New session ID\n", + " \"actor_id\": \"react-agent-1\", # Same Actor ID\n", + " }\n", + "}\n", + "\n", + "inputs = {\"messages\": [{\"role\": \"user\", \"content\": \"What values did I ask you to multiply and add?\"}]}\n", + "for chunk in graph.stream(inputs, stream_mode=\"updates\", config=config):\n", + " print(chunk)" + ] + }, + { + "cell_type": "markdown", + "id": "6fa7bd51-0ece-47d6-aae3-2a9041d98645", + "metadata": {}, + "source": [ + "## Wrapping Up\n", + "\n", + "As you can see, the AgentCore checkpointer is very powerful for persisting conversational and graph state in the background. For AgentCore runtime deployments, you must figure out how to determine the session ID and actor ID in the `/invocations` endpoint, for example like so:\n", + "\n", + "```python\n", + "# ... LangGraph create_react_agent logic and checkpoint initialization ...\n", + "\n", + "# AgentCore Entrypoint\n", + "@app.entrypoint\n", + "def agent_invocation(payload, context):\n", + " \"\"\"\n", + " AgentCore invocation endpoint that handles incoming requests.\n", + " \n", + " Sample payload format:\n", + " {\n", + " \"prompt\": \"user question here\",\n", + " \"thread_id\": \"optional-thread-id\",\n", + " \"actor_id\": \"optional-actor-id\"\n", + " }\n", + " \"\"\"\n", + " print(\"Received payload:\", payload)\n", + " \n", + " # Extract prompt from payload\n", + " prompt = payload[\"prompt\"]\n", + " inputs = {\"messages\": [{\"role\": \"user\", \"content\": prompt}]}\n", + " \n", + " # Extract or generate thread_id and actor_id\n", + " # In production, you might derive these from the context or caller identity\n", + " thread_id = payload.get(\"thread_id\")\n", + " actor_id = payload.get(\"actor_id\")\n", + " \n", + " # Create runtime config for invocation\n", + " config = {\n", + " \"configurable\": {\n", + " \"thread_id\": thread_id,\n", + " \"actor_id\": actor_id,\n", + " }\n", + " }\n", + " \n", + " # Invoke the graph with checkpointing\n", + " output = graph.invoke(inputs, config=config)\n", + " \n", + " # Return the response\n", + " return {\n", + " \"result\": output['messages'][-1],\n", + " \"thread_id\": thread_id,\n", + " \"actor_id\": actor_id,\n", + " \"message_count\": len(output['messages'])\n", + " }\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "eda8649a-d48f-467c-94b9-99dd8fb2c078", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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.11.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/samples/memory/checkpointer-demo.ipynb b/samples/memory/checkpointer-demo.ipynb deleted file mode 100644 index 7f3ade097..000000000 --- a/samples/memory/checkpointer-demo.ipynb +++ /dev/null @@ -1,486 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "8208f7d6-391d-4054-9e67-ac0f85878dcd", - "metadata": {}, - "source": [ - "# Bedrock AgentCore Memory Checkpointer Walkthrough\n", - "\n", - "This sample notebook walks through setup and usage of the Bedrock AgentCore Memory Checkpointer with LangGraph. This approach enables saving of conversations and state data to the Memory API for persistent storage, fault tolerance, and human-in-the-loop workflows.\n", - "\n", - "### Setup\n", - "For this notebook you will need:\n", - "1. An Amazon Web Services development account\n", - "2. Bedrock Model Access (i.e. Claude 3.7 Sonnet)\n", - "3. An AgentCore Memory Resource configured (see below section for details)\n", - "\n", - "### AgentCore Memory Resource\n", - "\n", - "Either in the AWS developer portal or using the boto3 library you must create an [AgentCore Memory Resource](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/bedrock-agentcore-control/client/create_memory.html). For just using the `AgentCoreMemorySaver` checkpointer in this notebook, you do not need to specify any specific long-term memory strategies. However, it may be beneficial to supplement this approach with the `AgentCoreMemoryStore` to save and extract conversational insights, so you may want to enable strategies for that use case.\n", - "\n", - "Once you have the Memory enabled and in a `ACTIVE` state, take note of the `memoryId`, we will need it later." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "8eff8706-6715-4981-983b-934561ee0a19", - "metadata": {}, - "outputs": [], - "source": [ - "# Install general dependencies\n", - "from typing import Annotated, Any, Dict, List\n", - "\n", - "from langchain.chat_models import init_chat_model\n", - "from langchain.tools import tool\n", - "\n", - "# Import LangGraph and LangChain components\n", - "from langchain_core.messages import AIMessage, HumanMessage, SystemMessage\n", - "from langchain_core.runnables import RunnableConfig\n", - "from langgraph.graph import END, START, StateGraph\n", - "from langgraph.graph.message import add_messages\n", - "from langgraph.prebuilt import ToolNode, tools_condition\n", - "from typing_extensions import TypedDict" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "fc12fd92-b84a-4d2f-b96d-83d3da08bf70", - "metadata": {}, - "outputs": [], - "source": [ - "# Import the AgentCoreMemorySaver that we will use as a checkpointer\n", - "# from langgraph_checkpoint_aws.checkpoint.agentcore_memory.saver import AgentCoreMemorySaver\n", - "from agentcore_memory.saver import AgentCoreMemorySaver\n", - "\n", - "import logging\n", - "logging.getLogger().setLevel(logging.DEBUG)" - ] - }, - { - "cell_type": "markdown", - "id": "51d91a14-541c-426c-a52c-5fdcf29f8953", - "metadata": {}, - "source": [ - "## AgentCore Memory Configuration\n", - "- `REGION` corresponds to the AWS region that your resources are present in, these are passed to the `AgentCoreMemorySaver`.\n", - "- `MEMORY_ID` corresponds to your top level AgentCore Memory resource. Within this resource we will store checkpoints for multiple actors and sessions\n", - "- `MODEL_ID` this is the bedrock model that will power our LangGraph agent through Bedrock Converse.\n", - "\n", - "We will use the `MEMORY_ID` and any additional boto3 client keyword args (in our case, `REGION`) to instantiate our checkpointer." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "226d094c-a05d-4f88-851d-cc42ff63ef11", - "metadata": {}, - "outputs": [], - "source": [ - "REGION = \"ap-southeast-2\"\n", - "MEMORY_ID = \"memory_tnpk0-AMDRU75vYn\"\n", - "MODEL_ID = \"apac.anthropic.claude-3-7-sonnet-20250219-v1:0\"\n", - "\n", - "# Initialize checkpointer for state persistence\n", - "checkpointer = AgentCoreMemorySaver(MEMORY_ID, region_name=REGION)\n", - "\n", - "# Initialize LLM\n", - "llm = init_chat_model(MODEL_ID, model_provider=\"bedrock_converse\", region_name=REGION)" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "a8a1e31f-137a-4bd6-b959-4dc7d8176949", - "metadata": {}, - "outputs": [], - "source": [ - "# Define agent state and LangGraph graph builder\n", - "class State(TypedDict):\n", - " messages: Annotated[list, add_messages]\n", - "\n", - "\n", - "graph_builder = StateGraph(State)" - ] - }, - { - "cell_type": "markdown", - "id": "c56c77aa-4b19-49a5-af3c-db2b4798384b", - "metadata": {}, - "source": [ - "## Define our tools\n", - "\n", - "For this demo notebook we will only give our agent access to two simple tools, adding and multiplying." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "dd81ba1c-3fb8-474c-ae40-0ee7c62ca234", - "metadata": {}, - "outputs": [], - "source": [ - "@tool\n", - "def add(a: int, b: int):\n", - " \"\"\"Add two integers and return the result\"\"\"\n", - " return a + b\n", - "\n", - "\n", - "@tool\n", - "def multiply(a: int, b: int):\n", - " \"\"\"Multiply two integers and return the result\"\"\"\n", - " return a * b\n", - "\n", - "\n", - "tools = [add, multiply]\n", - "\n", - "# Bind the tools to our LLM so it can understand their structure\n", - "llm_with_tools = llm.bind_tools(tools)" - ] - }, - { - "cell_type": "markdown", - "id": "651b6e3f-8b41-4c07-8a32-680666b2661e", - "metadata": {}, - "source": [ - "## Build our LangGraph agent graph\n", - "\n", - "Our agent will have a few simple nodes, mainly a chatbot node and a tool node. This will enable our chatbot to use the add and multiply tools as much as it needs and then return a response. We will visualize this graph below." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "9e63decd-2e15-4f12-9e82-6a0aac3f1892", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "✅ Agent with checkpointing compiled successfully\n" - ] - }, - { - "data": { - "image/png": "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", - "text/plain": [ - "" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Our chatbot node will contain the LLM invocation\n", - "def chatbot(state: State):\n", - " return {\"messages\": [llm_with_tools.invoke(state[\"messages\"])]}\n", - "\n", - "\n", - "graph_builder.add_node(\"chatbot\", chatbot)\n", - "tool_node = ToolNode(tools=tools)\n", - "graph_builder.add_node(\"tools\", tool_node)\n", - "\n", - "graph_builder.add_conditional_edges(\n", - " \"chatbot\",\n", - " tools_condition,\n", - ")\n", - "\n", - "# Finish off the other edges\n", - "graph_builder.add_edge(START, \"chatbot\")\n", - "graph_builder.add_edge(\"tools\", \"chatbot\")\n", - "graph_builder.add_edge(\"chatbot\", END)\n", - "\n", - "# Compile the graph with our AgentCoreMemorySaver as the checkpointer\n", - "graph = graph_builder.compile(checkpointer=checkpointer)\n", - "\n", - "print(\"✅ Agent with checkpointing compiled successfully\")\n", - "graph" - ] - }, - { - "cell_type": "markdown", - "id": "b6cbbb3a-f85c-42f5-bbfb-40f5f6b8b48c", - "metadata": {}, - "source": [ - "## IMPORTANT: State and Config\n", - "\n", - "\n", - "### LangGraph RuntimeConfig\n", - "In LangGraph, config is a `RuntimeConfig` that contains attributes that are necessary at invocation time, for example user IDs or session IDs. For the `AgentCoreMemorySaver`, `thread_id` and `actor_id` must be set in the config. For instance, your AgentCore invocation endpoint could assign this based on the identity or user ID of the caller. Additional documentation here: [https://langchain-ai.github.io/langgraphjs/how-tos/configuration/](https://langchain-ai.github.io/langgraphjs/how-tos/configuration/)\n", - "\n", - "### LangGraph Invocation State\n", - "The state that is passed to the `graph.invoke` method is an instance of our `State` we defined earlier: \n", - "```\n", - "class State(TypedDict):\n", - " messages: Annotated[list, add_messages]\n", - "```\n", - "As you can see, we have defined messages with the `add_messages` annotation. This means that each node (or invocation) state can return a `messages` key that will simply add the value to the latest value of the `State`. So for example, our chatbot node may return a response such as `messages=[\"Hi of course I can help you with that\"]` and it will be appended to our state messages below instead of overwriting it. For more information on State, see the docs here: [https://langchain-ai.github.io/langgraph/concepts/low_level/#working-with-messages-in-graph-state](https://langchain-ai.github.io/langgraph/concepts/low_level/#working-with-messages-in-graph-state)" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "859fee36-1eff-4713-9c3b-387b702c0301", - "metadata": {}, - "outputs": [], - "source": [ - "# For our demo we will have dummy thread and actor IDs\n", - "config = {\n", - " \"configurable\": {\n", - " \"thread_id\": \"thread-0\",\n", - " \"actor_id\": \"user-0\",\n", - " }\n", - "}\n", - "\n", - "# This invocation state is where you would fill in a query prompt from the user at /invocations\n", - "invocation_state = {\n", - " \"messages\": [HumanMessage(\"What is 1337 times 200 + 17? Follow pemdas.\")]\n", - "}" - ] - }, - { - "cell_type": "markdown", - "id": "96044de0-2d32-4811-ac30-43f4416f4303", - "metadata": {}, - "source": [ - "### Run the agent" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "c2bc4869-58cd-4914-9a7a-8d39ecd16226", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'messages': [HumanMessage(content='What is 1337 times 200 + 17? Follow pemdas.', additional_kwargs={}, response_metadata={}, id='b48c5506-2d06-4ba8-8da5-e315ad5cc654'),\n", - " AIMessage(content=[{'type': 'text', 'text': \"I'll solve this step by step following PEMDAS (Parentheses, Exponents, Multiplication/Division, Addition/Subtraction).\\n\\nAccording to PEMDAS, I should perform multiplication before addition. So I'll first multiply 1337 by 200, and then add 17 to that result.\\n\\nLet me calculate 1337 × 200:\"}, {'type': 'tool_use', 'name': 'multiply', 'input': {'a': 1337, 'b': 200}, 'id': 'tooluse_zZPvbVtOR2u2uxjQMtAq_A'}], additional_kwargs={}, response_metadata={'ResponseMetadata': {'RequestId': 'e1c53591-ec66-4bcc-851e-74e43091ff2f', 'HTTPStatusCode': 200, 'HTTPHeaders': {'date': 'Mon, 15 Sep 2025 13:45:15 GMT', 'content-type': 'application/json', 'content-length': '691', 'connection': 'keep-alive', 'x-amzn-requestid': 'e1c53591-ec66-4bcc-851e-74e43091ff2f'}, 'RetryAttempts': 0}, 'stopReason': 'tool_use', 'metrics': {'latencyMs': [3046]}, 'model_name': 'apac.anthropic.claude-3-7-sonnet-20250219-v1:0'}, id='run--5727d585-bf6b-4ca8-9b15-9e9275e112e6-0', tool_calls=[{'name': 'multiply', 'args': {'a': 1337, 'b': 200}, 'id': 'tooluse_zZPvbVtOR2u2uxjQMtAq_A', 'type': 'tool_call'}], usage_metadata={'input_tokens': 472, 'output_tokens': 153, 'total_tokens': 625, 'input_token_details': {'cache_creation': 0, 'cache_read': 0}}),\n", - " ToolMessage(content='267400', name='multiply', id='25e90eed-b36e-474a-b9f6-74a6294814c6', tool_call_id='tooluse_zZPvbVtOR2u2uxjQMtAq_A'),\n", - " AIMessage(content=[{'type': 'text', 'text': \"Now I'll add 17 to this result:\"}, {'type': 'tool_use', 'name': 'add', 'input': {'a': 267400, 'b': 17}, 'id': 'tooluse_RkwUNdsXT16Yh8dktOyMRQ'}], additional_kwargs={}, response_metadata={'ResponseMetadata': {'RequestId': 'a9c3bb70-784b-4550-a162-d0380023e801', 'HTTPStatusCode': 200, 'HTTPHeaders': {'date': 'Mon, 15 Sep 2025 13:45:17 GMT', 'content-type': 'application/json', 'content-length': '425', 'connection': 'keep-alive', 'x-amzn-requestid': 'a9c3bb70-784b-4550-a162-d0380023e801'}, 'RetryAttempts': 0}, 'stopReason': 'tool_use', 'metrics': {'latencyMs': [1589]}, 'model_name': 'apac.anthropic.claude-3-7-sonnet-20250219-v1:0'}, id='run--5b2347b4-54e9-4c72-b9ff-ca3b111688ac-0', tool_calls=[{'name': 'add', 'args': {'a': 267400, 'b': 17}, 'id': 'tooluse_RkwUNdsXT16Yh8dktOyMRQ', 'type': 'tool_call'}], usage_metadata={'input_tokens': 638, 'output_tokens': 82, 'total_tokens': 720, 'input_token_details': {'cache_creation': 0, 'cache_read': 0}}),\n", - " ToolMessage(content='267417', name='add', id='b5a708d3-5f32-4f34-93c5-1383dcc11479', tool_call_id='tooluse_RkwUNdsXT16Yh8dktOyMRQ'),\n", - " AIMessage(content='Following PEMDAS, the answer to 1337 × 200 + 17 is 267,417.', additional_kwargs={}, response_metadata={'ResponseMetadata': {'RequestId': '079b938e-969a-44ad-a8b9-c9d936fcc238', 'HTTPStatusCode': 200, 'HTTPHeaders': {'date': 'Mon, 15 Sep 2025 13:45:19 GMT', 'content-type': 'application/json', 'content-length': '354', 'connection': 'keep-alive', 'x-amzn-requestid': '079b938e-969a-44ad-a8b9-c9d936fcc238'}, 'RetryAttempts': 0}, 'stopReason': 'end_turn', 'metrics': {'latencyMs': [1397]}, 'model_name': 'apac.anthropic.claude-3-7-sonnet-20250219-v1:0'}, id='run--bdb9b4ac-1f5a-489a-a956-d879aac4d942-0', usage_metadata={'input_tokens': 733, 'output_tokens': 31, 'total_tokens': 764, 'input_token_details': {'cache_creation': 0, 'cache_read': 0}}),\n", - " HumanMessage(content='Create a LaTeX formula in markdown that shows the previous calculation and results', additional_kwargs={}, response_metadata={}, id='ad93df1b-1eac-4e08-8805-b42ae9f59462'),\n", - " AIMessage(content=\"Here's a LaTeX formula in markdown that shows the previous calculation and results:\\n\\n```math\\n1337 \\\\times 200 + 17 = 267400 + 17 = 267417\\n```\\n\\nThis displays the calculation step by step, showing the multiplication of 1337 by 200 first (following PEMDAS - order of operations), which equals 267400, then the addition of 17 to get the final result of 267417.\", additional_kwargs={}, response_metadata={'ResponseMetadata': {'RequestId': 'baa5c348-5b8b-40e0-8b69-6465193ce041', 'HTTPStatusCode': 200, 'HTTPHeaders': {'date': 'Mon, 15 Sep 2025 13:45:44 GMT', 'content-type': 'application/json', 'content-length': '657', 'connection': 'keep-alive', 'x-amzn-requestid': 'baa5c348-5b8b-40e0-8b69-6465193ce041'}, 'RetryAttempts': 0}, 'stopReason': 'end_turn', 'metrics': {'latencyMs': [2624]}, 'model_name': 'apac.anthropic.claude-3-7-sonnet-20250219-v1:0'}, id='run--1faf142a-b246-4870-9552-09b5f6b9d05b-0', usage_metadata={'input_tokens': 781, 'output_tokens': 106, 'total_tokens': 887, 'input_token_details': {'cache_creation': 0, 'cache_read': 0}}),\n", - " HumanMessage(content='What is 1337 times 200 + 17? Follow pemdas.', additional_kwargs={}, response_metadata={}, id='e6f0b5e6-a8c6-4a7a-96cc-0a1b6f11931f'),\n", - " AIMessage(content=[{'type': 'text', 'text': \"I'll solve this step by step following PEMDAS (Parentheses, Exponents, Multiplication/Division, Addition/Subtraction).\\n\\nAccording to PEMDAS, I should perform multiplication before addition. So I'll first multiply 1337 by 200, and then add 17 to that result.\\n\\nLet me calculate 1337 × 200:\"}, {'type': 'tool_use', 'name': 'multiply', 'input': {'a': 1337, 'b': 200}, 'id': 'tooluse_20HotERoStqxI3CgbvcQMg'}], additional_kwargs={}, response_metadata={'ResponseMetadata': {'RequestId': '51736bf1-9e9e-483a-8b60-a00b99c0a4ff', 'HTTPStatusCode': 200, 'HTTPHeaders': {'date': 'Mon, 15 Sep 2025 13:50:16 GMT', 'content-type': 'application/json', 'content-length': '692', 'connection': 'keep-alive', 'x-amzn-requestid': '51736bf1-9e9e-483a-8b60-a00b99c0a4ff'}, 'RetryAttempts': 0}, 'stopReason': 'tool_use', 'metrics': {'latencyMs': [2453]}, 'model_name': 'apac.anthropic.claude-3-7-sonnet-20250219-v1:0'}, id='run--0ca8262a-6d70-4a50-b13b-d6df5f478149-0', tool_calls=[{'name': 'multiply', 'args': {'a': 1337, 'b': 200}, 'id': 'tooluse_20HotERoStqxI3CgbvcQMg', 'type': 'tool_call'}], usage_metadata={'input_tokens': 909, 'output_tokens': 153, 'total_tokens': 1062, 'input_token_details': {'cache_creation': 0, 'cache_read': 0}}),\n", - " ToolMessage(content='267400', name='multiply', id='a43f5f72-d03f-4953-b748-f882c3b98b95', tool_call_id='tooluse_20HotERoStqxI3CgbvcQMg'),\n", - " AIMessage(content=[{'type': 'text', 'text': \"Now I'll add 17 to this result:\"}, {'type': 'tool_use', 'name': 'add', 'input': {'a': 267400, 'b': 17}, 'id': 'tooluse_dXOCeJXhS1e-p3DGxhUeFg'}], additional_kwargs={}, response_metadata={'ResponseMetadata': {'RequestId': '618612aa-4795-4e4b-9c4d-850d87ccd805', 'HTTPStatusCode': 200, 'HTTPHeaders': {'date': 'Mon, 15 Sep 2025 13:50:18 GMT', 'content-type': 'application/json', 'content-length': '427', 'connection': 'keep-alive', 'x-amzn-requestid': '618612aa-4795-4e4b-9c4d-850d87ccd805'}, 'RetryAttempts': 0}, 'stopReason': 'tool_use', 'metrics': {'latencyMs': [1722]}, 'model_name': 'apac.anthropic.claude-3-7-sonnet-20250219-v1:0'}, id='run--755de27f-2d2d-4804-b8b8-2d2fddc2f86e-0', tool_calls=[{'name': 'add', 'args': {'a': 267400, 'b': 17}, 'id': 'tooluse_dXOCeJXhS1e-p3DGxhUeFg', 'type': 'tool_call'}], usage_metadata={'input_tokens': 1075, 'output_tokens': 82, 'total_tokens': 1157, 'input_token_details': {'cache_creation': 0, 'cache_read': 0}}),\n", - " ToolMessage(content='267417', name='add', id='118bff77-abcf-4517-96e4-f60b80d5604b', tool_call_id='tooluse_dXOCeJXhS1e-p3DGxhUeFg'),\n", - " AIMessage(content='Following PEMDAS, the answer to 1337 × 200 + 17 is 267,417.', additional_kwargs={}, response_metadata={'ResponseMetadata': {'RequestId': '3abe3820-54cf-456f-95a0-61fcef15dcc4', 'HTTPStatusCode': 200, 'HTTPHeaders': {'date': 'Mon, 15 Sep 2025 13:50:19 GMT', 'content-type': 'application/json', 'content-length': '356', 'connection': 'keep-alive', 'x-amzn-requestid': '3abe3820-54cf-456f-95a0-61fcef15dcc4'}, 'RetryAttempts': 0}, 'stopReason': 'end_turn', 'metrics': {'latencyMs': [1186]}, 'model_name': 'apac.anthropic.claude-3-7-sonnet-20250219-v1:0'}, id='run--d653f490-c780-423a-ab5e-24130110daaf-0', usage_metadata={'input_tokens': 1170, 'output_tokens': 31, 'total_tokens': 1201, 'input_token_details': {'cache_creation': 0, 'cache_read': 0}})]}" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "graph.invoke(invocation_state, config=config)" - ] - }, - { - "cell_type": "markdown", - "id": "f0d28db8-c9cc-4fa1-922d-89476e9f71c5", - "metadata": {}, - "source": [ - "## Inspect the current state with AgentCoreMemory\n", - "\n", - "Under the hood when you call `graph.get_state(config)` it calls the checkpointer to retrieve the latest checkpoint saved for our actor and session. We can take a look at the values saved at this point in the conversation from `messages`." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "60042ec5-cd64-48f3-bd7d-cb7ca9e21b39", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "human: What is 1337 times 200 + 17? Follow pemdas.\n", - "=========================================\n", - "ai: I'll solve this step by step following PEMDAS (Parentheses, Exponents, Multiplication/Division, Addition/Subtraction).\n", - "\n", - "According to PEMDAS, I should perform multiplication before addition. So I'll first multiply 1337 by 200, and then add 17 to that result.\n", - "\n", - "Let me calculate 1337 × 200:\n", - "=========================================\n", - "tool: 267400\n", - "=========================================\n", - "ai: Now I'll add 17 to this result:\n", - "=========================================\n", - "tool: 267417\n", - "=========================================\n", - "ai: Following PEMDAS, the answer to 1337 × 200 + 17 is 267,417.\n", - "=========================================\n", - "human: Create a LaTeX formula in markdown that shows the previous calculation and results\n", - "=========================================\n", - "ai: Here's a LaTeX formula in markdown that shows the previous calculation and results:\n", - "\n", - "```math\n", - "1337 \\times 200 + 17 = 267400 + 17 = 267417\n", - "```\n", - "\n", - "This displays the calculation step by step, showing the multiplication of 1337 by 200 first (following PEMDAS - order of operations), which equals 267400, then the addition of 17 to get the final result of 267417.\n", - "=========================================\n", - "human: What is 1337 times 200 + 17? Follow pemdas.\n", - "=========================================\n", - "ai: I'll solve this step by step following PEMDAS (Parentheses, Exponents, Multiplication/Division, Addition/Subtraction).\n", - "\n", - "According to PEMDAS, I should perform multiplication before addition. So I'll first multiply 1337 by 200, and then add 17 to that result.\n", - "\n", - "Let me calculate 1337 × 200:\n", - "=========================================\n", - "tool: 267400\n", - "=========================================\n", - "ai: Now I'll add 17 to this result:\n", - "=========================================\n", - "tool: 267417\n", - "=========================================\n", - "ai: Following PEMDAS, the answer to 1337 × 200 + 17 is 267,417.\n", - "=========================================\n" - ] - } - ], - "source": [ - "for message in graph.get_state(config).values.get(\"messages\"):\n", - " print(f\"{message.type}: {message.text()}\")\n", - " print(\"=========================================\")" - ] - }, - { - "cell_type": "markdown", - "id": "3aa0c9bd-6fb6-467d-b51a-84696238b4fa", - "metadata": {}, - "source": [ - "## Look at a previous checkpoints during execution\n", - "Using the `graph.get_state_history(config)` we will see the checkpoints that were saved, then inspect a previous checkpoint's values for `messages`. Checkpoints are listed so the most recent checkpoints appear first." - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "c2232c2b-154d-4b50-93b0-925eb88e67c8", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "(Checkpoint ID: 1f0923ae-b162-61aa-800f-bba4b057543e) # of messages in state: 14\n", - "(Checkpoint ID: 1f0923ae-a40f-61e2-800e-bc65f256d255) # of messages in state: 13\n", - "(Checkpoint ID: 1f0923ae-a3ff-6c38-800d-505d7ed13c25) # of messages in state: 12\n", - "(Checkpoint ID: 1f0923ae-918a-6f26-800c-7b2ebf1316a2) # of messages in state: 11\n", - "(Checkpoint ID: 1f0923ae-9173-629a-800b-b53ad08339fe) # of messages in state: 10\n", - "(Checkpoint ID: 1f0923ae-72c7-6ab2-800a-5b5912be455a) # of messages in state: 9\n", - "(Checkpoint ID: 1f0923ae-72bd-6e0e-8009-33e40701ee42) # of messages in state: 8\n", - "(Checkpoint ID: 1f0923a4-6e20-6324-8008-16ed6f369bd9) # of messages in state: 8\n", - "(Checkpoint ID: 1f0923a4-5323-6b48-8007-e1247fbfc0cb) # of messages in state: 7\n", - "(Checkpoint ID: 1f0923a4-530f-6a62-8006-4b60a52adca0) # of messages in state: 6\n", - "(Checkpoint ID: 1f0923a3-7efb-649c-8005-acd1eb738210) # of messages in state: 6\n", - "(Checkpoint ID: 1f0923a3-6fa2-62a2-8004-a44cd9675857) # of messages in state: 5\n", - "(Checkpoint ID: 1f0923a3-6f9b-69d4-8003-6100b95bc700) # of messages in state: 4\n", - "(Checkpoint ID: 1f0923a3-5e70-6da8-8002-40a909cdb9bd) # of messages in state: 3\n", - "(Checkpoint ID: 1f0923a3-5e61-6858-8001-256b044cf577) # of messages in state: 2\n", - "(Checkpoint ID: 1f0923a3-3f5c-6f5c-8000-7ce3a145a6ed) # of messages in state: 1\n", - "(Checkpoint ID: 1f0923a3-3f4f-646a-bfff-6d736ba7793a) # of messages in state: 0\n" - ] - } - ], - "source": [ - "for checkpoint in graph.get_state_history(config):\n", - " print(\n", - " f\"(Checkpoint ID: {checkpoint.config['configurable']['checkpoint_id']}) # of messages in state: {len(checkpoint.values.get('messages'))}\"\n", - " )" - ] - }, - { - "cell_type": "markdown", - "id": "ab07194f-49b4-4fb9-a74b-d86b970cace6", - "metadata": {}, - "source": [ - "## Continue the conversation\n", - "By using the same config with our session and actor from before, we can continue the conversation by invoking our LangGraph agent with a new invocation state. The checkpointer in the background will take care of loading in context so that all the previous messages from the first interaction are loaded in." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "b7117c6a-d1d5-4866-aee9-2b7229fd73fd", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "AIMessage(content=\"Here's a LaTeX formula in markdown that shows the previous calculation and results:\\n\\n```math\\n1337 \\\\times 200 + 17 = 267400 + 17 = 267417\\n```\\n\\nThis displays the calculation step by step, showing the multiplication of 1337 by 200 first (following PEMDAS - order of operations), which equals 267400, then the addition of 17 to get the final result of 267417.\", additional_kwargs={}, response_metadata={'ResponseMetadata': {'RequestId': 'cf97747e-d4d4-49bf-b801-56253cc788c2', 'HTTPStatusCode': 200, 'HTTPHeaders': {'date': 'Mon, 15 Sep 2025 13:50:36 GMT', 'content-type': 'application/json', 'content-length': '659', 'connection': 'keep-alive', 'x-amzn-requestid': 'cf97747e-d4d4-49bf-b801-56253cc788c2'}, 'RetryAttempts': 0}, 'stopReason': 'end_turn', 'metrics': {'latencyMs': [2207]}, 'model_name': 'apac.anthropic.claude-3-7-sonnet-20250219-v1:0'}, id='run--88043c79-6656-47e0-b489-ad0219b553f0-0', usage_metadata={'input_tokens': 1218, 'output_tokens': 106, 'total_tokens': 1324, 'input_token_details': {'cache_creation': 0, 'cache_read': 0}})" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# This invocation state is where you would fill in a query prompt from the user at /invocations\n", - "invocation_state = {\n", - " \"messages\": [\n", - " HumanMessage(\n", - " \"Create a LaTeX formula in markdown that shows the previous calculation and results\"\n", - " )\n", - " ]\n", - "}\n", - "graph.invoke(invocation_state, config=config)\n", - "\n", - "# Display the response\n", - "graph.get_state(config).values.get(\"messages\")[-1]" - ] - }, - { - "cell_type": "markdown", - "id": "6fa7bd51-0ece-47d6-aae3-2a9041d98645", - "metadata": {}, - "source": [ - "## Wrapping Up\n", - "\n", - "As you can see, the AgentCore checkpointer is very powerful for persisting conversational and graph state in the background. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "eda8649a-d48f-467c-94b9-99dd8fb2c078", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "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.11.10" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} From 63c54c8e6459209906ea749ec307539b11e00661 Mon Sep 17 00:00:00 2001 From: Jack Gordley Date: Thu, 18 Sep 2025 20:57:22 -0700 Subject: [PATCH 3/8] updating sample notebooks, new tests, and general fixes --- .../agentcore/__init__.py | 3 + .../agentcore/helpers.py | 6 +- .../agentcore/models.py | 9 +- .../agentcore/saver.py | 7 +- .../integration_tests/agentcore/test_saver.py | 312 ++++++ .../tests/unit_tests/agentcore/__init__.py | 1 + .../tests/unit_tests/agentcore/test_saver.py | 969 ++++++++++++++++++ .../agentcore_memory_checkpointer.ipynb | 4 +- ...tcore_memory_checkpointer_human_loop.ipynb | 340 ++++++ 9 files changed, 1641 insertions(+), 10 deletions(-) create mode 100644 libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/__init__.py create mode 100644 libs/langgraph-checkpoint-aws/tests/integration_tests/agentcore/test_saver.py create mode 100644 libs/langgraph-checkpoint-aws/tests/unit_tests/agentcore/__init__.py create mode 100644 libs/langgraph-checkpoint-aws/tests/unit_tests/agentcore/test_saver.py create mode 100644 samples/memory/agentcore_memory_checkpointer_human_loop.ipynb diff --git a/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/__init__.py b/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/__init__.py new file mode 100644 index 000000000..9c52f924f --- /dev/null +++ b/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/__init__.py @@ -0,0 +1,3 @@ +from langgraph_checkpoint_aws.agentcore.saver import AgentCoreMemorySaver + +__all__ = ["AgentCoreMemorySaver"] diff --git a/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/helpers.py b/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/helpers.py index bd601aabc..a6869d297 100644 --- a/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/helpers.py +++ b/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/helpers.py @@ -160,7 +160,7 @@ def store_events_batch( ) def get_events( - self, session_id: str, actor_id: str, max_results: int = None + self, session_id: str, actor_id: str, limit: int = 100 ) -> List[EventType]: """Retrieve events from AgentCore Memory.""" all_events = [] @@ -191,7 +191,7 @@ def get_events( logger.warning(f"Failed to decode event: {e}") next_token = response.get("nextToken") - if not next_token or (max_results and len(all_events) >= max_results): + if not next_token or (limit and len(all_events) >= limit): break return all_events @@ -277,6 +277,7 @@ def build_checkpoint_tuple( parent_config = { "configurable": { "thread_id": config.thread_id, + "actor_id": config.actor_id, "checkpoint_ns": config.checkpoint_ns, "checkpoint_id": checkpoint_event.parent_checkpoint_id, } @@ -296,6 +297,7 @@ def build_checkpoint_tuple( config={ "configurable": { "thread_id": config.thread_id, + "actor_id": config.actor_id, "checkpoint_ns": config.checkpoint_ns, "checkpoint_id": checkpoint_event.checkpoint_id, } diff --git a/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/models.py b/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/models.py index f7c60d8b8..6a466a395 100644 --- a/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/models.py +++ b/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/models.py @@ -18,11 +18,10 @@ class CheckpointerConfig(BaseModel): @property def session_id(self) -> str: """Generate session ID from thread_id and checkpoint_ns.""" - return ( - f"{self.thread_id}#{self.checkpoint_ns}" - if self.checkpoint_ns - else self.thread_id - ) + if self.checkpoint_ns: + # Use underscore separator to ensure valid session ID pattern + return f"{self.thread_id}_{self.checkpoint_ns}" + return self.thread_id @classmethod def from_runnable_config(cls, config: Dict[str, Any]) -> "CheckpointerConfig": diff --git a/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/saver.py b/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/saver.py index e8100f281..03670c112 100644 --- a/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/saver.py +++ b/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/saver.py @@ -42,7 +42,11 @@ class AgentCoreMemorySaver(BaseCheckpointSaver[str]): """ AgentCore Memory checkpoint saver. - This checkpoint saver stores checkpoints in Bedrock AgentCore memory + This saver persists Checkpoints as serialized blob events in AgentCore Memory. + + Args: + memory_id: the ID of the memory resource created in AgentCore Memory + serde: serialization protocol to be used. Defaults to JSONPlusSerializer """ def __init__( @@ -187,6 +191,7 @@ def put( return { "configurable": { "thread_id": checkpoint_config.thread_id, + "actor_id": checkpoint_config.actor_id, "checkpoint_ns": checkpoint_config.checkpoint_ns, "checkpoint_id": checkpoint["id"], } diff --git a/libs/langgraph-checkpoint-aws/tests/integration_tests/agentcore/test_saver.py b/libs/langgraph-checkpoint-aws/tests/integration_tests/agentcore/test_saver.py new file mode 100644 index 000000000..7848ee67b --- /dev/null +++ b/libs/langgraph-checkpoint-aws/tests/integration_tests/agentcore/test_saver.py @@ -0,0 +1,312 @@ +import datetime +import os +import random +import string +from typing import Literal + +import pytest +from langchain_aws import ChatBedrock +from langchain_core.tools import tool +from langgraph.checkpoint.base import Checkpoint, uuid6 +from langgraph.prebuilt import create_react_agent + +from langgraph_checkpoint_aws.agentcore.saver import AgentCoreMemorySaver + + +def generate_valid_session_id(): + """Generate a valid session ID that matches AgentCore pattern [a-zA-Z0-9][a-zA-Z0-9-_]*""" + # Start with letter, then 6 random alphanumeric chars + chars = string.ascii_letters + string.digits + return "test" + "".join(random.choices(chars, k=6)) + + +def generate_valid_actor_id(): + """Generate a valid actor ID that matches AgentCore pattern [a-zA-Z0-9][a-zA-Z0-9-_]*""" + # Start with letter, then 6 random alphanumeric chars + chars = string.ascii_letters + string.digits + return "actor" + "".join(random.choices(chars, k=6)) + + +@tool +def add(a: int, b: int): + """Add two integers and return the result.""" + return a + b + + +@tool +def multiply(a: int, b: int): + """Multiply two integers and return the result.""" + return a * b + + +@tool +def get_weather(city: Literal["nyc", "sf"]): + """Use this to get weather information.""" + if city == "nyc": + return "It might be cloudy in nyc" + elif city == "sf": + return "It's always sunny in sf" + else: + raise AssertionError("Unknown city") + + +class TestAgentCoreMemorySaver: + @pytest.fixture + def tools(self): + return [add, multiply, get_weather] + + @pytest.fixture + def model(self): + return ChatBedrock( + model="anthropic.claude-3-sonnet-20240229-v1:0", region="us-west-2" + ) + + @pytest.fixture + def memory_id(self): + memory_id = os.environ.get("AGENTCORE_MEMORY_ID") + if not memory_id: + pytest.skip("AGENTCORE_MEMORY_ID environment variable not set") + return memory_id + + @pytest.fixture + def memory_saver(self, memory_id): + return AgentCoreMemorySaver(memory_id=memory_id, region_name="us-west-2") + + @pytest.fixture + def boto_agentcore_client(self, memory_saver): + return memory_saver.checkpoint_event_client.client + + def test_tool_responses(self): + assert add.invoke({"a": 5, "b": 3}) == 8 + assert multiply.invoke({"a": 4, "b": 6}) == 24 + assert get_weather.invoke("sf") == "It's always sunny in sf" + assert get_weather.invoke("nyc") == "It might be cloudy in nyc" + + def test_checkpoint_save_and_retrieve(self, memory_saver): + thread_id = generate_valid_session_id() + actor_id = generate_valid_actor_id() + + config = { + "configurable": { + "thread_id": thread_id, + "actor_id": actor_id, + "checkpoint_ns": "test_namespace", + } + } + + checkpoint = Checkpoint( + v=1, + id=str(uuid6(clock_seq=-2)), + ts=datetime.datetime.now(datetime.timezone.utc).isoformat(), + channel_values={ + "messages": ["test message"], + "results": {"status": "completed"}, + }, + channel_versions={"messages": "v1", "results": "v1"}, + versions_seen={"node1": {"messages": "v1"}}, + pending_sends=[], + ) + + checkpoint_metadata = { + "source": "input", + "step": 1, + "writes": {"node1": ["write1", "write2"]}, + } + + try: + saved_config = memory_saver.put( + config, + checkpoint, + checkpoint_metadata, + {"messages": "v2", "results": "v2"}, + ) + + assert saved_config["configurable"]["checkpoint_id"] == checkpoint["id"] + assert saved_config["configurable"]["thread_id"] == thread_id + assert saved_config["configurable"]["actor_id"] == actor_id + assert saved_config["configurable"]["checkpoint_ns"] == "test_namespace" + + checkpoint_tuple = memory_saver.get_tuple(saved_config) + assert checkpoint_tuple.checkpoint["id"] == checkpoint["id"] + + # Metadata includes original metadata plus actor_id from config + expected_metadata = checkpoint_metadata.copy() + expected_metadata["actor_id"] = actor_id + assert checkpoint_tuple.metadata == expected_metadata + assert checkpoint_tuple.config == saved_config + + finally: + memory_saver.delete_thread(thread_id, actor_id) + + def test_math_agent_with_checkpointing(self, tools, model, memory_saver): + thread_id = generate_valid_session_id() + actor_id = generate_valid_actor_id() + + try: + graph = create_react_agent(model, tools=tools, checkpointer=memory_saver) + config = { + "configurable": { + "thread_id": thread_id, + "actor_id": actor_id, + } + } + + response = graph.invoke( + { + "messages": [ + ("human", "What is 15 times 23? Then add 100 to the result.") + ] + }, + config, + ) + assert response, "Response should not be empty" + assert "messages" in response + assert len(response["messages"]) > 1 + + checkpoint = memory_saver.get(config) + assert checkpoint, "Checkpoint should not be empty" + + checkpoint_tuples = list(memory_saver.list(config)) + assert checkpoint_tuples, "Checkpoint tuples should not be empty" + assert isinstance(checkpoint_tuples, list) + + # Continue conversation to test state persistence + response2 = graph.invoke( + { + "messages": [ + ( + "human", + "What was the final result from my previous calculation?", + ) + ] + }, + config, + ) + assert response2, "Second response should not be empty" + + # Verify we have more checkpoints after second interaction + checkpoint_tuples_after = list(memory_saver.list(config)) + assert len(checkpoint_tuples_after) > len(checkpoint_tuples) + + finally: + memory_saver.delete_thread(thread_id, actor_id) + + def test_weather_query_with_checkpointing(self, tools, model, memory_saver): + thread_id = generate_valid_session_id() + actor_id = generate_valid_actor_id() + + try: + graph = create_react_agent(model, tools=tools, checkpointer=memory_saver) + config = { + "configurable": { + "thread_id": thread_id, + "actor_id": actor_id, + } + } + + response = graph.invoke( + {"messages": [("human", "What's the weather in sf and nyc?")]}, config + ) + assert response, "Response should not be empty" + + checkpoint = memory_saver.get(config) + assert checkpoint, "Checkpoint should not be empty" + + checkpoint_tuples = list(memory_saver.list(config)) + assert checkpoint_tuples, "Checkpoint tuples should not be empty" + + finally: + memory_saver.delete_thread(thread_id, actor_id) + + def test_multiple_sessions_isolation(self, tools, model, memory_saver): + thread_id_1 = generate_valid_session_id() + thread_id_2 = generate_valid_session_id() + actor_id = generate_valid_actor_id() + + try: + graph = create_react_agent(model, tools=tools, checkpointer=memory_saver) + + config_1 = { + "configurable": { + "thread_id": thread_id_1, + "actor_id": actor_id, + } + } + + config_2 = { + "configurable": { + "thread_id": thread_id_2, + "actor_id": actor_id, + } + } + + # First session + response_1 = graph.invoke( + {"messages": [("human", "Calculate 10 times 5")]}, config_1 + ) + assert response_1, "First session response should not be empty" + + # Second session + response_2 = graph.invoke( + {"messages": [("human", "What's the weather in sf?")]}, config_2 + ) + assert response_2, "Second session response should not be empty" + + # Verify sessions are isolated + checkpoints_1 = list(memory_saver.list(config_1)) + checkpoints_2 = list(memory_saver.list(config_2)) + + assert len(checkpoints_1) > 0 + assert len(checkpoints_2) > 0 + + # Verify different checkpoint IDs + checkpoint_ids_1 = { + cp.config["configurable"]["checkpoint_id"] for cp in checkpoints_1 + } + checkpoint_ids_2 = { + cp.config["configurable"]["checkpoint_id"] for cp in checkpoints_2 + } + assert checkpoint_ids_1.isdisjoint(checkpoint_ids_2) + + finally: + memory_saver.delete_thread(thread_id_1, actor_id) + memory_saver.delete_thread(thread_id_2, actor_id) + + def test_checkpoint_listing_with_limit(self, tools, model, memory_saver): + thread_id = generate_valid_session_id() + actor_id = generate_valid_actor_id() + + try: + graph = create_react_agent(model, tools=tools, checkpointer=memory_saver) + config = { + "configurable": { + "thread_id": thread_id, + "actor_id": actor_id, + } + } + + # Create multiple interactions to generate several checkpoints + for i in range(3): + graph.invoke( + {"messages": [("human", f"Calculate {i + 1} times 2")]}, config + ) + + # Test listing with limit + all_checkpoints = list(memory_saver.list(config)) + limited_checkpoints = list(memory_saver.list(config, limit=2)) + + assert len(all_checkpoints) >= 3 + assert len(limited_checkpoints) == 2 + + # Verify limited checkpoints are the most recent ones + assert ( + limited_checkpoints[0].config["configurable"]["checkpoint_id"] + == all_checkpoints[0].config["configurable"]["checkpoint_id"] + ) + assert ( + limited_checkpoints[1].config["configurable"]["checkpoint_id"] + == all_checkpoints[1].config["configurable"]["checkpoint_id"] + ) + + finally: + memory_saver.delete_thread(thread_id, actor_id) diff --git a/libs/langgraph-checkpoint-aws/tests/unit_tests/agentcore/__init__.py b/libs/langgraph-checkpoint-aws/tests/unit_tests/agentcore/__init__.py new file mode 100644 index 000000000..22cd57e2c --- /dev/null +++ b/libs/langgraph-checkpoint-aws/tests/unit_tests/agentcore/__init__.py @@ -0,0 +1 @@ +"""Unit tests for AgentCore Memory Checkpoint Saver.""" diff --git a/libs/langgraph-checkpoint-aws/tests/unit_tests/agentcore/test_saver.py b/libs/langgraph-checkpoint-aws/tests/unit_tests/agentcore/test_saver.py new file mode 100644 index 000000000..c6a4f4541 --- /dev/null +++ b/libs/langgraph-checkpoint-aws/tests/unit_tests/agentcore/test_saver.py @@ -0,0 +1,969 @@ +""" +Unit tests for AgentCore Memory Checkpoint Saver. +""" + +import json +from unittest.mock import MagicMock, Mock, patch + +import pytest +from langchain_core.runnables import RunnableConfig +from langgraph.checkpoint.base import Checkpoint, CheckpointMetadata, CheckpointTuple +from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer +from langgraph.constants import TASKS + +from langgraph_checkpoint_aws.agentcore.constants import ( + EMPTY_CHANNEL_VALUE, + EventDecodingError, + InvalidConfigError, +) +from langgraph_checkpoint_aws.agentcore.helpers import ( + CheckpointEventClient, + EventProcessor, + EventSerializer, +) +from langgraph_checkpoint_aws.agentcore.models import ( + ChannelDataEvent, + CheckpointerConfig, + CheckpointEvent, + WriteItem, + WritesEvent, +) +from langgraph_checkpoint_aws.agentcore.saver import AgentCoreMemorySaver + + +@pytest.fixture +def sample_checkpoint_event(): + return CheckpointEvent( + checkpoint_id="checkpoint_123", + checkpoint_data={ + "v": 1, + "id": "checkpoint_123", + "ts": "2024-01-01T00:00:00Z", + "channel_versions": {"default": "v1", "tasks": "v2"}, + "versions_seen": {}, + "pending_sends": [], + }, + metadata={"source": "input", "step": -1}, + parent_checkpoint_id="parent_checkpoint_id", + thread_id="test_thread_id", + checkpoint_ns="test_namespace", + ) + + +@pytest.fixture +def sample_channel_data_event(): + return ChannelDataEvent( + channel="default", + version="v1", + value="test_value", + thread_id="test_thread_id", + checkpoint_ns="test_namespace", + ) + + +@pytest.fixture +def sample_writes_event(): + return WritesEvent( + checkpoint_id="checkpoint_123", + writes=[ + WriteItem( + task_id="task_1", + channel="channel_1", + value="value_1", + task_path="/path/1", + ), + WriteItem( + task_id="task_2", + channel=TASKS, + value="value_2", + task_path="/path/2", + ), + ], + ) + + +class TestAgentCoreMemorySaver: + """Test suite for AgentCoreMemorySaver.""" + + @pytest.fixture + def mock_boto_client(self): + mock_client = Mock() + mock_client.create_event = MagicMock() + mock_client.list_events = MagicMock() + mock_client.delete_event = MagicMock() + return mock_client + + @pytest.fixture + def memory_id(self): + return "test-memory-id" + + @pytest.fixture + def saver(self, mock_boto_client, memory_id): + with patch("boto3.client") as mock_boto3_client: + mock_boto3_client.return_value = mock_boto_client + yield AgentCoreMemorySaver(memory_id=memory_id) + + @pytest.fixture + def runnable_config(self): + return RunnableConfig( + configurable={ + "thread_id": "test_thread_id", + "actor_id": "test_actor_id", + "checkpoint_ns": "test_namespace", + "checkpoint_id": "test_checkpoint_id", + } + ) + + @pytest.fixture + def sample_checkpoint(self): + return Checkpoint( + v=1, + id="checkpoint_123", + ts="2024-01-01T00:00:00Z", + channel_values={ + "default": "value1", + "tasks": ["task1", "task2"], + "results": {"status": "completed"}, + }, + channel_versions={"default": "v1", "tasks": "v2", "results": "v1"}, + versions_seen={ + "node1": {"default": "v1", "tasks": "v2"}, + "node2": {"results": "v1"}, + }, + pending_sends=[], + ) + + @pytest.fixture + def sample_checkpoint_metadata(self): + return CheckpointMetadata( + source="input", + step=-1, + writes={"node1": ["write1", "write2"], "node2": {"key": "value"}}, + parents={ + "namespace1": "parent_checkpoint_1", + "namespace2": "parent_checkpoint_2", + }, + ) + + def test_init_with_default_client(self, memory_id): + with patch("boto3.client") as mock_boto3_client: + mock_client = Mock() + mock_boto3_client.return_value = mock_client + + saver = AgentCoreMemorySaver(memory_id=memory_id) + + assert saver.memory_id == memory_id + assert isinstance(saver.serializer, EventSerializer) + assert isinstance(saver.checkpoint_event_client, CheckpointEventClient) + assert isinstance(saver.processor, EventProcessor) + mock_boto3_client.assert_called_once_with("bedrock-agentcore") + + def test_init_with_custom_parameters(self, memory_id): + with patch("boto3.client") as mock_boto3_client: + mock_client = Mock() + mock_boto3_client.return_value = mock_client + + saver = AgentCoreMemorySaver( + memory_id=memory_id, + region_name="us-west-2", + ) + + assert saver.memory_id == memory_id + mock_boto3_client.assert_called_once_with( + "bedrock-agentcore", + region_name="us-west-2", + ) + + def test_get_tuple_success( + self, + saver, + mock_boto_client, + runnable_config, + sample_checkpoint_event, + sample_channel_data_event, + ): + # Remove specific checkpoint_id from config to get latest + runnable_config["configurable"].pop("checkpoint_id", None) + + mock_boto_client.list_events.return_value = { + "events": [ + { + "eventId": "event_1", + "payload": [ + { + "blob": saver.serializer.serialize_event( + sample_checkpoint_event + ) + } + ], + }, + { + "eventId": "event_2", + "payload": [ + { + "blob": saver.serializer.serialize_event( + sample_channel_data_event + ) + } + ], + }, + ] + } + + result = saver.get_tuple(runnable_config) + + assert isinstance(result, CheckpointTuple) + assert result.config["configurable"]["checkpoint_id"] == "checkpoint_123" + assert result.checkpoint["id"] == "checkpoint_123" + mock_boto_client.list_events.assert_called() + + def test_get_tuple_no_checkpoints(self, saver, mock_boto_client, runnable_config): + # Mock empty list_events response + mock_boto_client.list_events.return_value = {"events": []} + + result = saver.get_tuple(runnable_config) + + assert result is None + mock_boto_client.list_events.assert_called_once() + + def test_get_tuple_with_specific_checkpoint_id( + self, + saver, + mock_boto_client, + runnable_config, + sample_checkpoint_event, + ): + # Set specific checkpoint_id + runnable_config["configurable"]["checkpoint_id"] = "checkpoint_123" + + mock_boto_client.list_events.return_value = { + "events": [ + { + "eventId": "event_1", + "payload": [ + { + "blob": saver.serializer.serialize_event( + sample_checkpoint_event + ) + } + ], + } + ] + } + + result = saver.get_tuple(runnable_config) + + assert isinstance(result, CheckpointTuple) + assert result.config["configurable"]["checkpoint_id"] == "checkpoint_123" + + def test_get_tuple_checkpoint_not_found( + self, saver, mock_boto_client, runnable_config + ): + # Set specific checkpoint_id that doesn't exist + runnable_config["configurable"]["checkpoint_id"] = "non_existent_checkpoint" + + sample_event = CheckpointEvent( + checkpoint_id="different_checkpoint", + checkpoint_data={}, + metadata={}, + thread_id="test_thread_id", + checkpoint_ns="test_namespace", + ) + + mock_boto_client.list_events.return_value = { + "events": [ + { + "eventId": "event_1", + "payload": [ + {"blob": saver.serializer.serialize_event(sample_event)} + ], + } + ] + } + + result = saver.get_tuple(runnable_config) + + assert result is None + + def test_list_success( + self, + saver, + mock_boto_client, + runnable_config, + sample_checkpoint_event, + ): + # Remove specific checkpoint_id from config to list all + runnable_config["configurable"].pop("checkpoint_id", None) + + checkpoint_event_1 = sample_checkpoint_event + checkpoint_event_2 = CheckpointEvent( + checkpoint_id="checkpoint_456", + checkpoint_data={ + "v": 1, + "id": "checkpoint_456", + "ts": "2024-01-02T00:00:00Z", + "channel_versions": {}, + "versions_seen": {}, + "pending_sends": [], + }, + metadata={"source": "output", "step": 1}, + parent_checkpoint_id="checkpoint_123", + thread_id="test_thread_id", + checkpoint_ns="test_namespace", + ) + + mock_boto_client.list_events.return_value = { + "events": [ + { + "eventId": "event_1", + "payload": [ + {"blob": saver.serializer.serialize_event(checkpoint_event_1)} + ], + }, + { + "eventId": "event_2", + "payload": [ + {"blob": saver.serializer.serialize_event(checkpoint_event_2)} + ], + }, + ] + } + + results = list(saver.list(runnable_config)) + + assert len(results) == 2 + assert all(isinstance(r, CheckpointTuple) for r in results) + # Should be sorted in descending order + assert results[0].config["configurable"]["checkpoint_id"] == "checkpoint_456" + assert results[1].config["configurable"]["checkpoint_id"] == "checkpoint_123" + + def test_list_with_limit( + self, + saver, + mock_boto_client, + runnable_config, + ): + # Remove specific checkpoint_id from config to list all + runnable_config["configurable"].pop("checkpoint_id", None) + + events = [] + for i in range(5): + checkpoint_event = CheckpointEvent( + checkpoint_id=f"checkpoint_{i}", + checkpoint_data={ + "v": 1, + "id": f"checkpoint_{i}", + "ts": f"2024-01-0{i + 1}T00:00:00Z", + "channel_versions": {}, + "versions_seen": {}, + "pending_sends": [], + }, + metadata={"step": i}, + thread_id="test_thread_id", + checkpoint_ns="test_namespace", + ) + events.append( + { + "eventId": f"event_{i}", + "payload": [ + {"blob": saver.serializer.serialize_event(checkpoint_event)} + ], + } + ) + + mock_boto_client.list_events.return_value = {"events": events} + + results = list(saver.list(runnable_config, limit=3)) + + assert len(results) == 3 + + def test_list_with_before( + self, + saver, + mock_boto_client, + runnable_config, + ): + # Remove specific checkpoint_id from config to list all + runnable_config["configurable"].pop("checkpoint_id", None) + + events = [] + for i in range(3): + checkpoint_event = CheckpointEvent( + checkpoint_id=f"checkpoint_{i}", + checkpoint_data={ + "v": 1, + "id": f"checkpoint_{i}", + "ts": f"2024-01-0{i + 1}T00:00:00Z", + "channel_versions": {}, + "versions_seen": {}, + "pending_sends": [], + }, + metadata={"step": i}, + thread_id="test_thread_id", + checkpoint_ns="test_namespace", + ) + events.append( + { + "eventId": f"event_{i}", + "payload": [ + {"blob": saver.serializer.serialize_event(checkpoint_event)} + ], + } + ) + + mock_boto_client.list_events.return_value = {"events": events} + + before_config = RunnableConfig(configurable={"checkpoint_id": "checkpoint_2"}) + + results = list(saver.list(runnable_config, before=before_config)) + + assert len(results) == 2 + assert results[0].config["configurable"]["checkpoint_id"] == "checkpoint_1" + assert results[1].config["configurable"]["checkpoint_id"] == "checkpoint_0" + + def test_list_empty(self, saver, mock_boto_client, runnable_config): + # Mock empty list_events response + mock_boto_client.list_events.return_value = {"events": []} + + results = list(saver.list(runnable_config)) + + assert len(results) == 0 + + def test_put_success( + self, + saver, + mock_boto_client, + runnable_config, + sample_checkpoint, + sample_checkpoint_metadata, + ): + new_versions = {"default": "v2", "tasks": "v3"} + result = saver.put( + runnable_config, + sample_checkpoint, + sample_checkpoint_metadata, + new_versions, + ) + + assert result["configurable"]["checkpoint_id"] == "checkpoint_123" + assert result["configurable"]["thread_id"] == "test_thread_id" + assert result["configurable"]["checkpoint_ns"] == "test_namespace" + + mock_boto_client.create_event.assert_called_once() + call_args = mock_boto_client.create_event.call_args[1] + assert call_args["memoryId"] == saver.memory_id + assert call_args["actorId"] == "test_actor_id" + assert "payload" in call_args + # Should have channel events + checkpoint event + assert len(call_args["payload"]) == len(new_versions) + 1 + + def test_put_with_empty_channel_values( + self, + saver, + mock_boto_client, + runnable_config, + sample_checkpoint, + sample_checkpoint_metadata, + ): + sample_checkpoint["channel_values"] = {} + + new_versions = {"empty_channel": "v1"} + result = saver.put( + runnable_config, + sample_checkpoint, + sample_checkpoint_metadata, + new_versions, + ) + + assert result["configurable"]["checkpoint_id"] == "checkpoint_123" + mock_boto_client.create_event.assert_called_once() + + def test_put_writes_success( + self, + saver, + mock_boto_client, + runnable_config, + ): + # Set checkpoint_id in config + runnable_config["configurable"]["checkpoint_id"] = "checkpoint_123" + + writes = [ + ("channel_1", "value_1"), + ("channel_2", "value_2"), + (TASKS, {"task": "data"}), + ] + + saver.put_writes( + runnable_config, + writes, + task_id="task_123", + task_path="/test/path", + ) + + mock_boto_client.create_event.assert_called_once() + call_args = mock_boto_client.create_event.call_args[1] + assert call_args["memoryId"] == saver.memory_id + assert call_args["actorId"] == "test_actor_id" + assert len(call_args["payload"]) == 1 + + def test_put_writes_no_checkpoint_id( + self, + saver, + mock_boto_client, + ): + # Create config without checkpoint_id + config = RunnableConfig( + configurable={ + "thread_id": "test_thread_id", + "actor_id": "test_actor_id", + } + ) + + with pytest.raises(InvalidConfigError) as exc_info: + saver.put_writes(config, [("channel", "value")], "task_id") + + assert "checkpoint_id is required" in str(exc_info.value) + + def test_delete_thread_success( + self, + saver, + mock_boto_client, + ): + mock_boto_client.list_events.return_value = { + "events": [ + {"eventId": "event_1"}, + {"eventId": "event_2"}, + ] + } + + saver.delete_thread("thread_id", "actor_id") + + assert mock_boto_client.list_events.called + assert mock_boto_client.delete_event.call_count == 2 + mock_boto_client.delete_event.assert_any_call( + memoryId=saver.memory_id, + sessionId="thread_id", + eventId="event_1", + actorId="actor_id", + ) + mock_boto_client.delete_event.assert_any_call( + memoryId=saver.memory_id, + sessionId="thread_id", + eventId="event_2", + actorId="actor_id", + ) + + def test_delete_thread_with_pagination( + self, + saver, + mock_boto_client, + ): + # Mock paginated list_events responses + mock_boto_client.list_events.side_effect = [ + { + "events": [{"eventId": "event_1"}, {"eventId": "event_2"}], + "nextToken": "token_1", + }, + { + "events": [{"eventId": "event_3"}], + "nextToken": None, + }, + ] + + saver.delete_thread("thread_id", "actor_id") + + assert mock_boto_client.list_events.call_count == 2 + assert mock_boto_client.delete_event.call_count == 3 + + def test_get_next_version(self, saver): + # Test with None + version = saver.get_next_version(None, None) + assert version.startswith("00000000000000000000000000000001.") + + version = saver.get_next_version(5, None) + assert version.startswith("00000000000000000000000000000006.") + + version = saver.get_next_version( + "00000000000000000000000000000010.123456", None + ) + assert version.startswith("00000000000000000000000000000011.") + + +class TestCheckpointerConfig: + """Test suite for CheckpointerConfig.""" + + def test_from_runnable_config_success(self): + config = RunnableConfig( + configurable={ + "thread_id": "test_thread", + "actor_id": "test_actor", + "checkpoint_ns": "test_ns", + "checkpoint_id": "test_checkpoint", + } + ) + + checkpoint_config = CheckpointerConfig.from_runnable_config(config) + + assert checkpoint_config.thread_id == "test_thread" + assert checkpoint_config.actor_id == "test_actor" + assert checkpoint_config.checkpoint_ns == "test_ns" + assert checkpoint_config.checkpoint_id == "test_checkpoint" + assert checkpoint_config.session_id == "test_thread_test_ns" + + def test_from_runnable_config_no_namespace(self): + config = RunnableConfig( + configurable={ + "thread_id": "test_thread", + "actor_id": "test_actor", + } + ) + + checkpoint_config = CheckpointerConfig.from_runnable_config(config) + + assert checkpoint_config.thread_id == "test_thread" + assert checkpoint_config.actor_id == "test_actor" + assert checkpoint_config.checkpoint_ns == "" + assert checkpoint_config.checkpoint_id is None + assert checkpoint_config.session_id == "test_thread" + + def test_from_runnable_config_missing_thread_id(self): + config = RunnableConfig( + configurable={ + "actor_id": "test_actor", + } + ) + + with pytest.raises(InvalidConfigError) as exc_info: + CheckpointerConfig.from_runnable_config(config) + + assert "thread_id" in str(exc_info.value) + + def test_from_runnable_config_missing_actor_id(self): + config = RunnableConfig( + configurable={ + "thread_id": "test_thread", + } + ) + + with pytest.raises(InvalidConfigError) as exc_info: + CheckpointerConfig.from_runnable_config(config) + + assert "actor_id" in str(exc_info.value) + + +class TestEventSerializer: + """Test suite for EventSerializer.""" + + @pytest.fixture + def serializer(self): + return EventSerializer(JsonPlusSerializer()) + + def test_serialize_deserialize_checkpoint_event( + self, serializer, sample_checkpoint_event + ): + # Serialize + serialized = serializer.serialize_event(sample_checkpoint_event) + assert isinstance(serialized, str) + + deserialized = serializer.deserialize_event(serialized) + assert isinstance(deserialized, CheckpointEvent) + assert deserialized.checkpoint_id == sample_checkpoint_event.checkpoint_id + assert deserialized.thread_id == sample_checkpoint_event.thread_id + + def test_serialize_deserialize_channel_data_event( + self, serializer, sample_channel_data_event + ): + # Serialize + serialized = serializer.serialize_event(sample_channel_data_event) + assert isinstance(serialized, str) + + deserialized = serializer.deserialize_event(serialized) + assert isinstance(deserialized, ChannelDataEvent) + assert deserialized.channel == sample_channel_data_event.channel + assert deserialized.value == sample_channel_data_event.value + + def test_serialize_deserialize_writes_event(self, serializer, sample_writes_event): + # Serialize + serialized = serializer.serialize_event(sample_writes_event) + assert isinstance(serialized, str) + + deserialized = serializer.deserialize_event(serialized) + assert isinstance(deserialized, WritesEvent) + assert deserialized.checkpoint_id == sample_writes_event.checkpoint_id + assert len(deserialized.writes) == len(sample_writes_event.writes) + + def test_serialize_channel_data_with_empty_value(self, serializer): + event = ChannelDataEvent( + channel="test", + version="v1", + value=EMPTY_CHANNEL_VALUE, + thread_id="thread", + checkpoint_ns="ns", + ) + + serialized = serializer.serialize_event(event) + deserialized = serializer.deserialize_event(serialized) + + assert deserialized.value == EMPTY_CHANNEL_VALUE + + def test_deserialize_invalid_json(self, serializer): + with pytest.raises(EventDecodingError) as exc_info: + serializer.deserialize_event("invalid json {") + + assert "Failed to parse JSON" in str(exc_info.value) + + def test_deserialize_unknown_event_type(self, serializer): + invalid_event = json.dumps({"event_type": "unknown_type", "data": "test"}) + + with pytest.raises(EventDecodingError) as exc_info: + serializer.deserialize_event(invalid_event) + + assert "Unknown event type" in str(exc_info.value) + + +class TestCheckpointEventClient: + """Test suite for CheckpointEventClient.""" + + @pytest.fixture + def mock_boto_client(self): + mock_client = Mock() + mock_client.create_event = MagicMock() + mock_client.list_events = MagicMock() + mock_client.delete_event = MagicMock() + return mock_client + + @pytest.fixture + def serializer(self): + return EventSerializer(JsonPlusSerializer()) + + @pytest.fixture + def client(self, mock_boto_client, serializer): + with patch("boto3.client") as mock_boto3_client: + mock_boto3_client.return_value = mock_boto_client + yield CheckpointEventClient("test-memory-id", serializer) + + def test_store_event(self, client, mock_boto_client, sample_checkpoint_event): + client.store_event(sample_checkpoint_event, "session_id", "actor_id") + + mock_boto_client.create_event.assert_called_once() + call_args = mock_boto_client.create_event.call_args[1] + assert call_args["memoryId"] == "test-memory-id" + assert call_args["actorId"] == "actor_id" + assert call_args["sessionId"] == "session_id" + assert len(call_args["payload"]) == 1 + + def test_store_events_batch( + self, + client, + mock_boto_client, + sample_checkpoint_event, + sample_channel_data_event, + ): + events = [sample_checkpoint_event, sample_channel_data_event] + client.store_events_batch(events, "session_id", "actor_id") + + mock_boto_client.create_event.assert_called_once() + call_args = mock_boto_client.create_event.call_args[1] + assert len(call_args["payload"]) == 2 + + def test_get_events( + self, client, mock_boto_client, serializer, sample_checkpoint_event + ): + # Mock list_events response + mock_boto_client.list_events.return_value = { + "events": [ + { + "eventId": "event_1", + "payload": [ + {"blob": serializer.serialize_event(sample_checkpoint_event)} + ], + } + ] + } + + events = client.get_events("session_id", "actor_id") + + assert len(events) == 1 + assert isinstance(events[0], CheckpointEvent) + mock_boto_client.list_events.assert_called_once() + + def test_get_events_with_pagination( + self, client, mock_boto_client, serializer, sample_checkpoint_event + ): + # Mock paginated responses + mock_boto_client.list_events.side_effect = [ + { + "events": [ + { + "eventId": "event_1", + "payload": [ + { + "blob": serializer.serialize_event( + sample_checkpoint_event + ) + } + ], + } + ], + "nextToken": "token_1", + }, + { + "events": [ + { + "eventId": "event_2", + "payload": [ + { + "blob": serializer.serialize_event( + sample_checkpoint_event + ) + } + ], + } + ], + "nextToken": None, + }, + ] + + events = client.get_events("session_id", "actor_id") + + assert len(events) == 2 + assert mock_boto_client.list_events.call_count == 2 + + def test_get_events_with_decoding_error(self, client, mock_boto_client, serializer): + # Mock list_events response with invalid blob + mock_boto_client.list_events.return_value = { + "events": [ + { + "eventId": "event_1", + "payload": [{"blob": "invalid json {"}], + } + ] + } + + events = client.get_events("session_id", "actor_id") + + assert len(events) == 0 + + def test_delete_events(self, client, mock_boto_client): + # Mock list_events response + mock_boto_client.list_events.return_value = { + "events": [ + {"eventId": "event_1"}, + {"eventId": "event_2"}, + ] + } + + client.delete_events("session_id", "actor_id") + + assert mock_boto_client.list_events.called + assert mock_boto_client.delete_event.call_count == 2 + + +class TestEventProcessor: + """Test suite for EventProcessor.""" + + @pytest.fixture + def processor(self): + return EventProcessor() + + def test_process_events( + self, + processor, + sample_checkpoint_event, + sample_channel_data_event, + sample_writes_event, + ): + events = [ + sample_checkpoint_event, + sample_channel_data_event, + sample_writes_event, + ] + + checkpoints, writes_by_checkpoint, channel_data = processor.process_events( + events + ) + + assert len(checkpoints) == 1 + assert "checkpoint_123" in checkpoints + assert len(writes_by_checkpoint["checkpoint_123"]) == 2 + assert ("default", "v1") in channel_data + + def test_process_events_empty_channel_value( + self, processor, sample_channel_data_event + ): + sample_channel_data_event.value = EMPTY_CHANNEL_VALUE + events = [sample_channel_data_event] + + checkpoints, writes_by_checkpoint, channel_data = processor.process_events( + events + ) + + assert len(channel_data) == 0 + + def test_build_checkpoint_tuple( + self, + processor, + sample_checkpoint_event, + ): + writes = [ + WriteItem( + task_id="task_1", + channel="channel_1", + value="value_1", + task_path="/path/1", + ) + ] + channel_data = {("default", "v1"): "test_value"} + config = CheckpointerConfig( + thread_id="test_thread", + actor_id="test_actor", + checkpoint_ns="test_ns", + ) + + tuple_result = processor.build_checkpoint_tuple( + sample_checkpoint_event, writes, channel_data, config + ) + + assert isinstance(tuple_result, CheckpointTuple) + assert tuple_result.checkpoint["id"] == "checkpoint_123" + assert len(tuple_result.pending_writes) == 1 + assert tuple_result.checkpoint["channel_values"]["default"] == "test_value" + + def test_build_checkpoint_tuple_with_parent( + self, + processor, + sample_checkpoint_event, + ): + config = CheckpointerConfig( + thread_id="test_thread", + actor_id="test_actor", + checkpoint_ns="test_ns", + ) + + tuple_result = processor.build_checkpoint_tuple( + sample_checkpoint_event, [], {}, config + ) + + assert tuple_result.parent_config is not None + assert ( + tuple_result.parent_config["configurable"]["checkpoint_id"] + == "parent_checkpoint_id" + ) + + def test_build_checkpoint_tuple_no_parent( + self, + processor, + sample_checkpoint_event, + ): + sample_checkpoint_event.parent_checkpoint_id = None + config = CheckpointerConfig( + thread_id="test_thread", + actor_id="test_actor", + checkpoint_ns="test_ns", + ) + + tuple_result = processor.build_checkpoint_tuple( + sample_checkpoint_event, [], {}, config + ) + + assert tuple_result.parent_config is None diff --git a/samples/memory/agentcore_memory_checkpointer.ipynb b/samples/memory/agentcore_memory_checkpointer.ipynb index e83223e2d..7a0ccbbb4 100644 --- a/samples/memory/agentcore_memory_checkpointer.ipynb +++ b/samples/memory/agentcore_memory_checkpointer.ipynb @@ -132,7 +132,7 @@ "source": [ "## Build our LangGraph agent graph\n", "\n", - "Our agent will have a few simple nodes, mainly a chatbot node and a tool node. This will enable our chatbot to use the add and multiply tools as much as it needs and then return a response. We will visualize this graph below." + "Our agent will be built with the `create_react_agent` builder. It just has a few simple nodes, mainly a chatbot node and a tool node. This will enable our chatbot to use the add and multiply tools as much as it needs and then return a response. We will visualize this graph below." ] }, { @@ -443,7 +443,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, diff --git a/samples/memory/agentcore_memory_checkpointer_human_loop.ipynb b/samples/memory/agentcore_memory_checkpointer_human_loop.ipynb new file mode 100644 index 000000000..7234d3d14 --- /dev/null +++ b/samples/memory/agentcore_memory_checkpointer_human_loop.ipynb @@ -0,0 +1,340 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "e7d0b686-22f6-494b-952b-c97ee3bb0b60", + "metadata": {}, + "source": [ + "# Bedrock AgentCore Memory Checkpointer - Human in the Loop Example\n", + "\n", + "This sample notebook walks through setup and usage of the Bedrock AgentCore Memory Checkpointer with LangGraph. This example specifically showcases the ability to use a human-in-the-loop workflow to interrupt graph execution and resume it with human intervention.\n", + "\n", + "This notebook closely follows the walkthrough here on LangGraph - https://langchain-ai.github.io/langgraph/tutorials/get-started/4-human-in-the-loop/\n", + "\n", + "### Setup\n", + "For this notebook you will need:\n", + "1. An Amazon Web Services development account\n", + "2. Bedrock Model Access (i.e. Claude 3.7 Sonnet)\n", + "3. An AgentCore Memory Resource configured (see below section for details)\n", + "\n", + "### AgentCore Memory Resource\n", + "\n", + "Either in the AWS developer portal or using the boto3 library you must create an [AgentCore Memory Resource](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/bedrock-agentcore-control/client/create_memory.html). For just using the `AgentCoreMemorySaver` checkpointer in this notebook, you do not need to specify any specific long-term memory strategies. However, it may be beneficial to supplement this approach with the `AgentCoreMemoryStore` to save and extract conversational insights, so you may want to enable strategies for that use case.\n", + "\n", + "Once you have the Memory enabled and in a `ACTIVE` state, take note of the `memoryId`, we will need it later." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "bc7a3dfc-c14f-4a81-9c44-b44546d4196d", + "metadata": {}, + "outputs": [], + "source": [ + "# Import LangGraph and LangChain components\n", + "from langchain.chat_models import init_chat_model\n", + "from langchain.tools import tool\n", + "from langgraph.prebuilt import create_react_agent\n", + "\n", + "# Imports that enable human-in-the-loop\n", + "from langgraph.types import Command, interrupt" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "630de2fb-235a-4de5-a8e9-685a2babcdd1", + "metadata": {}, + "outputs": [], + "source": [ + "# Import the AgentCoreMemorySaver that we will use as a checkpointer\n", + "from langgraph_checkpoint_aws import AgentCoreMemorySaver\n", + "\n", + "import logging\n", + "logging.getLogger().setLevel(logging.DEBUG)" + ] + }, + { + "cell_type": "markdown", + "id": "a16f95ec-beb2-4007-a46c-730d0c156d18", + "metadata": {}, + "source": [ + "## AgentCore Memory Configuration\n", + "- `REGION` corresponds to the AWS region that your resources are present in, these are passed to the `AgentCoreMemorySaver`.\n", + "- `MEMORY_ID` corresponds to your top level AgentCore Memory resource. Within this resource we will store checkpoints for multiple actors and sessions\n", + "- `MODEL_ID` this is the bedrock model that will power our LangGraph agent through Bedrock Converse.\n", + "\n", + "We will use the `MEMORY_ID` and any additional boto3 client keyword args (in our case, `REGION`) to instantiate our checkpointer." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "fb67b4ee-ff3f-4576-8072-8885c2a47e11", + "metadata": {}, + "outputs": [], + "source": [ + "REGION = \"us-west-2\"\n", + "MEMORY_ID = \"YOUR_MEMORY_ID\"\n", + "MODEL_ID = \"us.anthropic.claude-3-7-sonnet-20250219-v1:0\"\n", + "\n", + "# Initialize checkpointer for state persistence\n", + "checkpointer = AgentCoreMemorySaver(MEMORY_ID, region_name=REGION)\n", + "\n", + "# Initialize LLM\n", + "llm = init_chat_model(MODEL_ID, model_provider=\"bedrock_converse\", region_name=REGION)" + ] + }, + { + "cell_type": "markdown", + "id": "783f9b1e-fd88-4546-afdb-1168e3a88ff2", + "metadata": {}, + "source": [ + "## Enable the Human Assistance Tool\n", + "\n", + "Using the LangGraph `interrupt` type, we can interrupt the agent graph execution to give the chance for a human to intervene and respond to the query to continue execution. " + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "aecb558a-8ee4-486c-92ab-3ff4485fdfe5", + "metadata": {}, + "outputs": [], + "source": [ + "@tool\n", + "def human_assistance(query: str) -> str:\n", + " \"\"\"Request assistance from a human.\"\"\"\n", + " human_response = interrupt({\"query\": query})\n", + " return human_response[\"data\"]\n", + "\n", + "@tool\n", + "def add(a: int, b: int):\n", + " \"\"\"Add two integers and return the result\"\"\"\n", + " return a + b\n", + "\n", + "@tool\n", + "def multiply(a: int, b: int):\n", + " \"\"\"Multiply two integers and return the result\"\"\"\n", + " return a * b\n", + "\n", + "\n", + "tools = [add, multiply, human_assistance]\n", + "\n", + "# Bind the tools to our LLM so it can understand their structure\n", + "llm_with_tools = llm.bind_tools(tools)" + ] + }, + { + "cell_type": "markdown", + "id": "30359ac1-90b1-4627-a4b0-203c1d598bf1", + "metadata": {}, + "source": [ + "## Build our LangGraph agent graph\n", + "\n", + "Our agent will be built with the `create_react_agent` builder. It just has a few simple nodes, mainly a chatbot node and a tool node. This will enable our chatbot to use the add, multiply, and human assistance tools as much as it needs and then return a response. We will visualize this graph below." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "2cbc7289-6595-4b31-8472-47d2de00c4bd", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "graph = create_react_agent(\n", + " model=llm_with_tools,\n", + " tools=tools,\n", + " prompt=\"You are a helpful assistant\",\n", + " checkpointer=checkpointer,\n", + ")\n", + "\n", + "graph" + ] + }, + { + "cell_type": "markdown", + "id": "fcd4b967-3c83-4f1c-ae22-d5ba97b7fbe1", + "metadata": {}, + "source": [ + "## IMPORTANT: Input and Config\n", + "\n", + "For this example we will ask explicitly for user assistance to make sure that the human assistance tool is called. In reality, this could be triggered by several conditions, for example a safety flag may route a conversation to a human if certain keywords are used.\n", + "\n", + "### Graph Invoke Input\n", + "We only need to pass the newest user message in as an argument `inputs`. This could include other state variables too but for the simple `create_react_agent`, messages are all that's required.\n", + "\n", + "### LangGraph RuntimeConfig\n", + "In LangGraph, config is a `RuntimeConfig` that contains attributes that are necessary at invocation time, for example user IDs or session IDs. For the `AgentCoreMemorySaver`, `thread_id` and `actor_id` must be set in the config. For instance, your AgentCore invocation endpoint could assign this based on the identity or user ID of the caller. Additional documentation here: [https://langchain-ai.github.io/langgraphjs/how-tos/configuration/](https://langchain-ai.github.io/langgraphjs/how-tos/configuration/)\n", + "\n", + "\n", + "\n", + "For the example, when the human assistance tool is called we expect the execution to be interrupted until a human intervenes." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "c50a6a6e-b16f-41dc-8f16-1d102f6debc4", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "================================\u001b[1m Human Message \u001b[0m=================================\n", + "\n", + "I'm frustrated with my current support. Please loop in a human.\n", + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "[{'type': 'text', 'text': \"I understand you're feeling frustrated and would like to speak with a human for support. I can certainly help you request human assistance.\"}, {'type': 'tool_use', 'name': 'human_assistance', 'input': {'query': 'User is frustrated with current support and has requested to speak with a human representative.'}, 'id': 'tooluse_6FDef0XWQhGv0aiA3D3_IQ'}]\n", + "Tool Calls:\n", + " human_assistance (tooluse_6FDef0XWQhGv0aiA3D3_IQ)\n", + " Call ID: tooluse_6FDef0XWQhGv0aiA3D3_IQ\n", + " Args:\n", + " query: User is frustrated with current support and has requested to speak with a human representative.\n" + ] + } + ], + "source": [ + "user_input = \"I'm frustrated with my current support. Please loop in a human.\"\n", + "config = {\"configurable\": {\"thread_id\": \"2\", \"actor_id\": \"demo-notebook\"}}\n", + "\n", + "events = graph.stream(\n", + " {\"messages\": [{\"role\": \"user\", \"content\": user_input}]},\n", + " config,\n", + " stream_mode=\"values\",\n", + ")\n", + "for event in events:\n", + " if \"messages\" in event:\n", + " event[\"messages\"][-1].pretty_print()" + ] + }, + { + "cell_type": "markdown", + "id": "b0f2af97-524a-4626-8dc4-85c5b677af90", + "metadata": {}, + "source": [ + "### Human-in-the-loop\n", + "\n", + "As you can see, execution paused when the human assistance tool was called. Inspecting the state (which uses the AgentCore memory checkpointer), you can see that the execution stopped at the tool node." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "0629af32-b660-4157-97ca-597b21ec66c3", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "('tools',)" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "snapshot = graph.get_state(config)\n", + "snapshot.next" + ] + }, + { + "cell_type": "markdown", + "id": "ae31390c-0b1d-4b7f-9185-a28ee2bb79aa", + "metadata": {}, + "source": [ + "### Responding as a Human\n", + "\n", + "Using the LangGraph `Command` with our response, we can resume the execution and pass our response back to the LLM for it to respond to the user. This powerful workflow is enabled by checkpointing so that conversation and graph state is persisted under the hood, enabling pausing and resuming chats on demand through certain conditions. " + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "aaf88072-ab8d-4e5f-a605-6d65e66f6207", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "[{'type': 'text', 'text': \"I understand you're feeling frustrated and would like to speak with a human for support. I can certainly help you request human assistance.\"}, {'type': 'tool_use', 'name': 'human_assistance', 'input': {'query': 'User is frustrated with current support and has requested to speak with a human representative.'}, 'id': 'tooluse_6FDef0XWQhGv0aiA3D3_IQ'}]\n", + "Tool Calls:\n", + " human_assistance (tooluse_6FDef0XWQhGv0aiA3D3_IQ)\n", + " Call ID: tooluse_6FDef0XWQhGv0aiA3D3_IQ\n", + " Args:\n", + " query: User is frustrated with current support and has requested to speak with a human representative.\n", + "=================================\u001b[1m Tool Message \u001b[0m=================================\n", + "Name: human_assistance\n", + "\n", + "I'm sorry to hear that you are frustrated. Looking at the past conversation history, I can see that you've requested a refund. I've gone ahead and credited it to your account.\n", + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "Thank you for your patience. A human representative has reviewed your case and has taken action on your behalf. They mentioned that they've processed a refund and it has been credited to your account. \n", + "\n", + "Is there anything else you'd like help with regarding your account or the refund process?\n" + ] + } + ], + "source": [ + "human_response = (\n", + " \"I'm sorry to hear that you are frustrated. Looking at the past conversation history, I can see that you've requested a refund. I've gone ahead and credited it to your account.\"\n", + ")\n", + "\n", + "human_command = Command(resume={\"data\": human_response})\n", + "\n", + "events = graph.stream(human_command, config, stream_mode=\"values\")\n", + "for event in events:\n", + " if \"messages\" in event:\n", + " event[\"messages\"][-1].pretty_print()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a83eaf68-9730-4356-9b0c-454cc6e4c371", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.11.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 4b12da50a2dc27d907d7fd51baafd163d185e297 Mon Sep 17 00:00:00 2001 From: Jack Gordley Date: Thu, 18 Sep 2025 21:26:42 -0700 Subject: [PATCH 4/8] Fixing UTC timestamp for early python versions --- .../langgraph_checkpoint_aws/agentcore/helpers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/helpers.py b/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/helpers.py index a6869d297..967429ade 100644 --- a/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/helpers.py +++ b/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/helpers.py @@ -5,10 +5,10 @@ from __future__ import annotations import base64 +import datetime import json import logging from collections import defaultdict -from datetime import UTC, datetime from typing import Any, Dict, List, Union import boto3 @@ -134,7 +134,7 @@ def store_event(self, event: EventType, session_id: str, actor_id: str) -> None: memoryId=self.memory_id, actorId=actor_id, sessionId=session_id, - eventTimestamp=datetime.now(UTC), + eventTimestamp=datetime.datetime.now(datetime.timezone.utc), payload=[{"blob": serialized}], ) @@ -144,7 +144,7 @@ def store_events_batch( """Store multiple events in a single API call to AgentCore Memory.""" # Serialize all events into payload blobs payload = [] - timestamp = datetime.now(UTC) + timestamp = datetime.datetime.now(datetime.timezone.utc) for event in events: serialized = self.serializer.serialize_event(event) From abec69dbe34423a1d9c53b349a69adb8687bb142 Mon Sep 17 00:00:00 2001 From: Jack Gordley Date: Mon, 22 Sep 2025 18:11:25 -0700 Subject: [PATCH 5/8] limit fix for list and improved language in notebooks --- .../agentcore/helpers.py | 8 +- .../agentcore/saver.py | 4 +- .../tests/unit_tests/agentcore/test_saver.py | 10 +- .../agentcore_memory_checkpointer.ipynb | 122 +++++------------- ...tcore_memory_checkpointer_human_loop.ipynb | 49 ++++--- 5 files changed, 72 insertions(+), 121 deletions(-) diff --git a/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/helpers.py b/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/helpers.py index 967429ade..d8043aab6 100644 --- a/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/helpers.py +++ b/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/helpers.py @@ -118,7 +118,7 @@ def deserialize_event(self, data: str) -> EventType: raise EventDecodingError(f"Failed to deserialize event: {e}") -class CheckpointEventClient: +class AgentCoreEventClient: """Handles low-level event storage and retrieval from AgentCore Memory for checkpoints.""" def __init__(self, memory_id: str, serializer: EventSerializer, **boto3_kwargs): @@ -163,6 +163,10 @@ def get_events( self, session_id: str, actor_id: str, limit: int = 100 ) -> List[EventType]: """Retrieve events from AgentCore Memory.""" + + if limit is not None and limit <= 0: + return [] + all_events = [] next_token = None @@ -191,7 +195,7 @@ def get_events( logger.warning(f"Failed to decode event: {e}") next_token = response.get("nextToken") - if not next_token or (limit and len(all_events) >= limit): + if not next_token or (limit is not None and len(all_events) >= limit): break return all_events diff --git a/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/saver.py b/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/saver.py index 03670c112..d8b291fe3 100644 --- a/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/saver.py +++ b/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/saver.py @@ -25,7 +25,7 @@ InvalidConfigError, ) from langgraph_checkpoint_aws.agentcore.helpers import ( - CheckpointEventClient, + AgentCoreEventClient, EventProcessor, EventSerializer, ) @@ -60,7 +60,7 @@ def __init__( self.memory_id = memory_id self.serializer = EventSerializer(self.serde) - self.checkpoint_event_client = CheckpointEventClient( + self.checkpoint_event_client = AgentCoreEventClient( memory_id, self.serializer, **boto3_kwargs ) self.processor = EventProcessor() diff --git a/libs/langgraph-checkpoint-aws/tests/unit_tests/agentcore/test_saver.py b/libs/langgraph-checkpoint-aws/tests/unit_tests/agentcore/test_saver.py index c6a4f4541..aa64f2d3e 100644 --- a/libs/langgraph-checkpoint-aws/tests/unit_tests/agentcore/test_saver.py +++ b/libs/langgraph-checkpoint-aws/tests/unit_tests/agentcore/test_saver.py @@ -17,7 +17,7 @@ InvalidConfigError, ) from langgraph_checkpoint_aws.agentcore.helpers import ( - CheckpointEventClient, + AgentCoreEventClient, EventProcessor, EventSerializer, ) @@ -154,7 +154,7 @@ def test_init_with_default_client(self, memory_id): assert saver.memory_id == memory_id assert isinstance(saver.serializer, EventSerializer) - assert isinstance(saver.checkpoint_event_client, CheckpointEventClient) + assert isinstance(saver.checkpoint_event_client, AgentCoreEventClient) assert isinstance(saver.processor, EventProcessor) mock_boto3_client.assert_called_once_with("bedrock-agentcore") @@ -721,8 +721,8 @@ def test_deserialize_unknown_event_type(self, serializer): assert "Unknown event type" in str(exc_info.value) -class TestCheckpointEventClient: - """Test suite for CheckpointEventClient.""" +class TestAgentCoreEventClient: + """Test suite for AgentCoreEventClient.""" @pytest.fixture def mock_boto_client(self): @@ -740,7 +740,7 @@ def serializer(self): def client(self, mock_boto_client, serializer): with patch("boto3.client") as mock_boto3_client: mock_boto3_client.return_value = mock_boto_client - yield CheckpointEventClient("test-memory-id", serializer) + yield AgentCoreEventClient("test-memory-id", serializer) def test_store_event(self, client, mock_boto_client, sample_checkpoint_event): client.store_event(sample_checkpoint_event, "session_id", "actor_id") diff --git a/samples/memory/agentcore_memory_checkpointer.ipynb b/samples/memory/agentcore_memory_checkpointer.ipynb index 7a0ccbbb4..cbfc50a23 100644 --- a/samples/memory/agentcore_memory_checkpointer.ipynb +++ b/samples/memory/agentcore_memory_checkpointer.ipynb @@ -34,7 +34,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 10, "id": "8eff8706-6715-4981-983b-934561ee0a19", "metadata": {}, "outputs": [], @@ -47,7 +47,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 11, "id": "fc12fd92-b84a-4d2f-b96d-83d3da08bf70", "metadata": {}, "outputs": [], @@ -74,7 +74,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 22, "id": "226d094c-a05d-4f88-851d-cc42ff63ef11", "metadata": {}, "outputs": [], @@ -102,7 +102,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 13, "id": "dd81ba1c-3fb8-474c-ae40-0ee7c62ca234", "metadata": {}, "outputs": [], @@ -119,10 +119,7 @@ " return a * b\n", "\n", "\n", - "tools = [add, multiply]\n", - "\n", - "# Bind the tools to our LLM so it can understand their structure\n", - "llm_with_tools = llm.bind_tools(tools)" + "tools = [add, multiply]" ] }, { @@ -137,25 +134,25 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 14, "id": "a116a57f", "metadata": {}, "outputs": [ { "data": { - "image/png": "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", + "image/png": "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", "text/plain": [ - "" + "" ] }, - "execution_count": 7, + "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "graph = create_react_agent(\n", - " model=llm_with_tools,\n", + " model=llm,\n", " tools=[add, multiply],\n", " prompt=\"You are a helpful assistant\",\n", " checkpointer=checkpointer,\n", @@ -181,7 +178,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 23, "id": "859fee36-1eff-4713-9c3b-387b702c0301", "metadata": {}, "outputs": [], @@ -206,7 +203,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 17, "id": "c2bc4869-58cd-4914-9a7a-8d39ecd16226", "metadata": {}, "outputs": [ @@ -214,11 +211,11 @@ "name": "stdout", "output_type": "stream", "text": [ - "{'agent': {'messages': [AIMessage(content=[{'type': 'text', 'text': \"I'll solve this step-by-step using the available tools.\\n\\nFirst, I'll multiply 1337 by 515321:\"}, {'type': 'tool_use', 'name': 'multiply', 'input': {'a': 1337, 'b': 515321}, 'id': 'tooluse_ufONDYV9T_GDk9uYGKrUBA'}], additional_kwargs={}, response_metadata={'ResponseMetadata': {'RequestId': 'c3d3db9c-985f-4c2a-88f0-4aaa5c673900', 'HTTPStatusCode': 200, 'HTTPHeaders': {'date': 'Wed, 17 Sep 2025 20:37:20 GMT', 'content-type': 'application/json', 'content-length': '497', 'connection': 'keep-alive', 'x-amzn-requestid': 'c3d3db9c-985f-4c2a-88f0-4aaa5c673900'}, 'RetryAttempts': 0}, 'stopReason': 'tool_use', 'metrics': {'latencyMs': [2280]}, 'model_name': 'us.anthropic.claude-3-7-sonnet-20250219-v1:0'}, id='run--364254f3-1cd7-4394-8d27-b52355454b40-0', tool_calls=[{'name': 'multiply', 'args': {'a': 1337, 'b': 515321}, 'id': 'tooluse_ufONDYV9T_GDk9uYGKrUBA', 'type': 'tool_call'}], usage_metadata={'input_tokens': 482, 'output_tokens': 100, 'total_tokens': 582, 'input_token_details': {'cache_creation': 0, 'cache_read': 0}})]}}\n", - "{'tools': {'messages': [ToolMessage(content='688984177', name='multiply', id='db6b78b8-165d-44cd-96b5-cd3b21bdc4a6', tool_call_id='tooluse_ufONDYV9T_GDk9uYGKrUBA')]}}\n", - "{'agent': {'messages': [AIMessage(content=[{'type': 'text', 'text': \"Now I'll add 412 to the result:\"}, {'type': 'tool_use', 'name': 'add', 'input': {'a': 688984177, 'b': 412}, 'id': 'tooluse_PuRt_QohS-2ZW_-Xjp1brQ'}], additional_kwargs={}, response_metadata={'ResponseMetadata': {'RequestId': '8177a4ec-802a-44ca-9323-936e91f3aa69', 'HTTPStatusCode': 200, 'HTTPHeaders': {'date': 'Wed, 17 Sep 2025 20:37:22 GMT', 'content-type': 'application/json', 'content-length': '429', 'connection': 'keep-alive', 'x-amzn-requestid': '8177a4ec-802a-44ca-9323-936e91f3aa69'}, 'RetryAttempts': 0}, 'stopReason': 'tool_use', 'metrics': {'latencyMs': [1642]}, 'model_name': 'us.anthropic.claude-3-7-sonnet-20250219-v1:0'}, id='run--80836015-5332-4d24-aeb6-3ad1cebf826f-0', tool_calls=[{'name': 'add', 'args': {'a': 688984177, 'b': 412}, 'id': 'tooluse_PuRt_QohS-2ZW_-Xjp1brQ', 'type': 'tool_call'}], usage_metadata={'input_tokens': 596, 'output_tokens': 83, 'total_tokens': 679, 'input_token_details': {'cache_creation': 0, 'cache_read': 0}})]}}\n", - "{'tools': {'messages': [ToolMessage(content='688984589', name='add', id='422ab45d-0a3e-4643-9c0b-0cc0073775fc', tool_call_id='tooluse_PuRt_QohS-2ZW_-Xjp1brQ')]}}\n", - "{'agent': {'messages': [AIMessage(content='The final answer is 688,984,589.\\n\\nFirst I multiplied 1337 by 515321, which equals 688,984,177. Then I added 412 to that result, giving us the final value of 688,984,589.', additional_kwargs={}, response_metadata={'ResponseMetadata': {'RequestId': '7a9cbf35-301a-43e4-97dd-fbb8dd64bcfb', 'HTTPStatusCode': 200, 'HTTPHeaders': {'date': 'Wed, 17 Sep 2025 20:37:23 GMT', 'content-type': 'application/json', 'content-length': '465', 'connection': 'keep-alive', 'x-amzn-requestid': '7a9cbf35-301a-43e4-97dd-fbb8dd64bcfb'}, 'RetryAttempts': 0}, 'stopReason': 'end_turn', 'metrics': {'latencyMs': [1785]}, 'model_name': 'us.anthropic.claude-3-7-sonnet-20250219-v1:0'}, id='run--c1484fdb-6b49-4d6b-8248-6fb901808436-0', usage_metadata={'input_tokens': 693, 'output_tokens': 61, 'total_tokens': 754, 'input_token_details': {'cache_creation': 0, 'cache_read': 0}})]}}\n" + "{'agent': {'messages': [AIMessage(content=[{'type': 'text', 'text': \"I'll help you calculate this step by step using the available tools.\\n\\nFirst, I'll multiply 1337 by 515321, and then add 412 to the result.\"}, {'type': 'tool_use', 'name': 'multiply', 'input': {'a': 1337, 'b': 515321}, 'id': 'tooluse_8NTeZaI6SRWVrWS3msurvQ'}], additional_kwargs={}, response_metadata={'ResponseMetadata': {'RequestId': 'cb32ee7b-35c8-42f4-b28d-ab81611f57f3', 'HTTPStatusCode': 200, 'HTTPHeaders': {'date': 'Tue, 23 Sep 2025 00:51:44 GMT', 'content-type': 'application/json', 'content-length': '563', 'connection': 'keep-alive', 'x-amzn-requestid': 'cb32ee7b-35c8-42f4-b28d-ab81611f57f3'}, 'RetryAttempts': 0}, 'stopReason': 'tool_use', 'metrics': {'latencyMs': [4900]}, 'model_name': 'us.anthropic.claude-3-7-sonnet-20250219-v1:0'}, id='run--8fa9fc8d-4491-4a79-bf9e-8921f710b1a4-0', tool_calls=[{'name': 'multiply', 'args': {'a': 1337, 'b': 515321}, 'id': 'tooluse_8NTeZaI6SRWVrWS3msurvQ', 'type': 'tool_call'}], usage_metadata={'input_tokens': 482, 'output_tokens': 110, 'total_tokens': 592, 'input_token_details': {'cache_creation': 0, 'cache_read': 0}})]}}\n", + "{'tools': {'messages': [ToolMessage(content='688984177', name='multiply', id='9232d136-aa20-4283-82fa-a64502656160', tool_call_id='tooluse_8NTeZaI6SRWVrWS3msurvQ')]}}\n", + "{'agent': {'messages': [AIMessage(content=[{'type': 'text', 'text': \"Now I'll add 412 to this result:\"}, {'type': 'tool_use', 'name': 'add', 'input': {'a': 688984177, 'b': 412}, 'id': 'tooluse_uowB7HdbQn-R-YQvufO3Pg'}], additional_kwargs={}, response_metadata={'ResponseMetadata': {'RequestId': 'dfd50e9e-0dfe-43d9-807b-7a29d0bc8f3b', 'HTTPStatusCode': 200, 'HTTPHeaders': {'date': 'Tue, 23 Sep 2025 00:51:47 GMT', 'content-type': 'application/json', 'content-length': '451', 'connection': 'keep-alive', 'x-amzn-requestid': 'dfd50e9e-0dfe-43d9-807b-7a29d0bc8f3b'}, 'RetryAttempts': 0}, 'stopReason': 'tool_use', 'metrics': {'latencyMs': [2412]}, 'model_name': 'us.anthropic.claude-3-7-sonnet-20250219-v1:0'}, id='run--524ea0f5-ddb0-4752-a224-6a3285f27d7e-0', tool_calls=[{'name': 'add', 'args': {'a': 688984177, 'b': 412}, 'id': 'tooluse_uowB7HdbQn-R-YQvufO3Pg', 'type': 'tool_call'}], usage_metadata={'input_tokens': 606, 'output_tokens': 83, 'total_tokens': 689, 'input_token_details': {'cache_creation': 0, 'cache_read': 0}})]}}\n", + "{'tools': {'messages': [ToolMessage(content='688984589', name='add', id='3a368934-2258-491d-96f1-e61852deaeaa', tool_call_id='tooluse_uowB7HdbQn-R-YQvufO3Pg')]}}\n", + "{'agent': {'messages': [AIMessage(content='The result of multiplying 1337 by 515321 and then adding 412 is 688,984,589.', additional_kwargs={}, response_metadata={'ResponseMetadata': {'RequestId': 'dadc1ae0-5f52-4ead-be93-90399d29359a', 'HTTPStatusCode': 200, 'HTTPHeaders': {'date': 'Tue, 23 Sep 2025 00:51:48 GMT', 'content-type': 'application/json', 'content-length': '391', 'connection': 'keep-alive', 'x-amzn-requestid': 'dadc1ae0-5f52-4ead-be93-90399d29359a'}, 'RetryAttempts': 0}, 'stopReason': 'end_turn', 'metrics': {'latencyMs': [1377]}, 'model_name': 'us.anthropic.claude-3-7-sonnet-20250219-v1:0'}, id='run--ffd35948-1221-4958-8212-5b5aee7ce904-0', usage_metadata={'input_tokens': 703, 'output_tokens': 32, 'total_tokens': 735, 'input_token_details': {'cache_creation': 0, 'cache_read': 0}})]}}\n" ] } ], @@ -239,7 +236,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 18, "id": "60042ec5-cd64-48f3-bd7d-cb7ca9e21b39", "metadata": {}, "outputs": [ @@ -249,19 +246,17 @@ "text": [ "human: What is 1337 times 515321? Then add 412 and return the value to me.\n", "=========================================\n", - "ai: I'll solve this step-by-step using the available tools.\n", + "ai: I'll help you calculate this step by step using the available tools.\n", "\n", - "First, I'll multiply 1337 by 515321:\n", + "First, I'll multiply 1337 by 515321, and then add 412 to the result.\n", "=========================================\n", "tool: 688984177\n", "=========================================\n", - "ai: Now I'll add 412 to the result:\n", + "ai: Now I'll add 412 to this result:\n", "=========================================\n", "tool: 688984589\n", "=========================================\n", - "ai: The final answer is 688,984,589.\n", - "\n", - "First I multiplied 1337 by 515321, which equals 688,984,177. Then I added 412 to that result, giving us the final value of 688,984,589.\n", + "ai: The result of multiplying 1337 by 515321 and then adding 412 is 688,984,589.\n", "=========================================\n" ] } @@ -283,7 +278,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 19, "id": "c2232c2b-154d-4b50-93b0-925eb88e67c8", "metadata": {}, "outputs": [ @@ -291,13 +286,13 @@ "name": "stdout", "output_type": "stream", "text": [ - "(Checkpoint ID: 1f094061-dc46-6a54-8005-daf53773fe70) # of messages in state: 6\n", - "(Checkpoint ID: 1f094061-ca8e-63b6-8004-f69f7debce02) # of messages in state: 5\n", - "(Checkpoint ID: 1f094061-ca73-6598-8003-770972f8d2e2) # of messages in state: 4\n", - "(Checkpoint ID: 1f094061-ba19-6efe-8002-94a1cb0a0063) # of messages in state: 3\n", - "(Checkpoint ID: 1f094061-ba06-6f20-8001-ffd58cf51b40) # of messages in state: 2\n", - "(Checkpoint ID: 1f094061-a168-61b2-8000-8a5fb60f2073) # of messages in state: 1\n", - "(Checkpoint ID: 1f094061-a145-62de-bfff-db79a2d38f86) # of messages in state: 0\n" + "(Checkpoint ID: 1f098177-c42d-6f60-8005-354f815e0136) # of messages in state: 6\n", + "(Checkpoint ID: 1f098177-b654-6baa-8004-a8b0f118635a) # of messages in state: 5\n", + "(Checkpoint ID: 1f098177-b647-6b76-8003-6283dc729a79) # of messages in state: 4\n", + "(Checkpoint ID: 1f098177-9e9b-670c-8002-588abfad5280) # of messages in state: 3\n", + "(Checkpoint ID: 1f098177-9e89-628c-8001-2bc2df91b7a3) # of messages in state: 2\n", + "(Checkpoint ID: 1f098177-6d2f-6fce-8000-6268b3fc0a3c) # of messages in state: 1\n", + "(Checkpoint ID: 1f098177-6d20-6a7e-bfff-c58a1ddceda6) # of messages in state: 0\n" ] } ], @@ -319,7 +314,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 20, "id": "b7117c6a-d1d5-4866-aee9-2b7229fd73fd", "metadata": {}, "outputs": [ @@ -327,7 +322,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "{'agent': {'messages': [AIMessage(content='The first calculations you asked me to do were to multiply 1337 by 515321, and then add 412 to the result.\\n\\nSpecifically:\\n1. Multiply: 1337 × 515321 = 688,984,177\\n2. Add: 688,984,177 + 412 = 688,984,589\\n\\nI performed these calculations using the multiply and add tools as requested in your initial query.', additional_kwargs={}, response_metadata={'ResponseMetadata': {'RequestId': 'd6d1a259-9195-4d8b-8f3f-92b2043a1f62', 'HTTPStatusCode': 200, 'HTTPHeaders': {'date': 'Wed, 17 Sep 2025 20:38:06 GMT', 'content-type': 'application/json', 'content-length': '605', 'connection': 'keep-alive', 'x-amzn-requestid': 'd6d1a259-9195-4d8b-8f3f-92b2043a1f62'}, 'RetryAttempts': 0}, 'stopReason': 'end_turn', 'metrics': {'latencyMs': [2263]}, 'model_name': 'us.anthropic.claude-3-7-sonnet-20250219-v1:0'}, id='run--1f8ee6ce-ed7f-46fc-b7f2-a97cf5440884-0', usage_metadata={'input_tokens': 768, 'output_tokens': 100, 'total_tokens': 868, 'input_token_details': {'cache_creation': 0, 'cache_read': 0}})]}}\n" + "{'agent': {'messages': [AIMessage(content='The first calculations you asked me to do were:\\n1. Multiply 1337 by 515321\\n2. Add 412 to the result of the multiplication\\n\\nSpecifically, I calculated 1337 × 515321 = 688,984,177, and then added 412 to get the final answer of 688,984,589.', additional_kwargs={}, response_metadata={'ResponseMetadata': {'RequestId': '4dc65f04-85c5-4eb7-8139-1f852c3709bc', 'HTTPStatusCode': 200, 'HTTPHeaders': {'date': 'Tue, 23 Sep 2025 00:52:08 GMT', 'content-type': 'application/json', 'content-length': '557', 'connection': 'keep-alive', 'x-amzn-requestid': '4dc65f04-85c5-4eb7-8139-1f852c3709bc'}, 'RetryAttempts': 0}, 'stopReason': 'end_turn', 'metrics': {'latencyMs': [2033]}, 'model_name': 'us.anthropic.claude-3-7-sonnet-20250219-v1:0'}, id='run--b19686f9-8c7d-4950-91e2-7c9a1bb5534d-0', usage_metadata={'input_tokens': 749, 'output_tokens': 81, 'total_tokens': 830, 'input_token_details': {'cache_creation': 0, 'cache_read': 0}})]}}\n" ] } ], @@ -350,7 +345,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 21, "id": "8ce88359-ee1e-44bd-9095-ad22b880dda8", "metadata": {}, "outputs": [ @@ -358,7 +353,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "{'agent': {'messages': [AIMessage(content=\"I don't see that you've asked me to multiply or add any specific values yet. While I have tools available to perform addition and multiplication, you haven't provided the numbers to calculate.\\n\\nWould you like me to:\\n1. Multiply two numbers together, or\\n2. Add two numbers together?\\n\\nIf so, please provide the specific values you'd like me to use for the calculation.\", additional_kwargs={}, response_metadata={'ResponseMetadata': {'RequestId': '5a7e1c01-9023-4d23-9491-2d2c46f347a9', 'HTTPStatusCode': 200, 'HTTPHeaders': {'date': 'Wed, 17 Sep 2025 20:39:41 GMT', 'content-type': 'application/json', 'content-length': '666', 'connection': 'keep-alive', 'x-amzn-requestid': '5a7e1c01-9023-4d23-9491-2d2c46f347a9'}, 'RetryAttempts': 0}, 'stopReason': 'end_turn', 'metrics': {'latencyMs': [2441]}, 'model_name': 'us.anthropic.claude-3-7-sonnet-20250219-v1:0'}, id='run--58b14eb8-5244-40e9-aa13-6d9ef82f1997-0', usage_metadata={'input_tokens': 470, 'output_tokens': 85, 'total_tokens': 555, 'input_token_details': {'cache_creation': 0, 'cache_read': 0}})]}}\n" + "{'agent': {'messages': [AIMessage(content=\"I don't see that you've asked me to multiply or add any specific values yet. While I have tools available to perform multiplication and addition, you haven't provided any numbers for me to work with.\\n\\nWould you like me to multiply or add some numbers? If so, please provide the values you'd like me to use.\", additional_kwargs={}, response_metadata={'ResponseMetadata': {'RequestId': '8ddf83ff-6536-4d41-b6b1-9763a1085fe2', 'HTTPStatusCode': 200, 'HTTPHeaders': {'date': 'Tue, 23 Sep 2025 00:52:21 GMT', 'content-type': 'application/json', 'content-length': '623', 'connection': 'keep-alive', 'x-amzn-requestid': '8ddf83ff-6536-4d41-b6b1-9763a1085fe2'}, 'RetryAttempts': 0}, 'stopReason': 'end_turn', 'metrics': {'latencyMs': [2118]}, 'model_name': 'us.anthropic.claude-3-7-sonnet-20250219-v1:0'}, id='run--052bbac2-e944-4adf-8ff5-24263d10c209-0', usage_metadata={'input_tokens': 470, 'output_tokens': 70, 'total_tokens': 540, 'input_token_details': {'cache_creation': 0, 'cache_read': 0}})]}}\n" ] } ], @@ -382,54 +377,7 @@ "source": [ "## Wrapping Up\n", "\n", - "As you can see, the AgentCore checkpointer is very powerful for persisting conversational and graph state in the background. For AgentCore runtime deployments, you must figure out how to determine the session ID and actor ID in the `/invocations` endpoint, for example like so:\n", - "\n", - "```python\n", - "# ... LangGraph create_react_agent logic and checkpoint initialization ...\n", - "\n", - "# AgentCore Entrypoint\n", - "@app.entrypoint\n", - "def agent_invocation(payload, context):\n", - " \"\"\"\n", - " AgentCore invocation endpoint that handles incoming requests.\n", - " \n", - " Sample payload format:\n", - " {\n", - " \"prompt\": \"user question here\",\n", - " \"thread_id\": \"optional-thread-id\",\n", - " \"actor_id\": \"optional-actor-id\"\n", - " }\n", - " \"\"\"\n", - " print(\"Received payload:\", payload)\n", - " \n", - " # Extract prompt from payload\n", - " prompt = payload[\"prompt\"]\n", - " inputs = {\"messages\": [{\"role\": \"user\", \"content\": prompt}]}\n", - " \n", - " # Extract or generate thread_id and actor_id\n", - " # In production, you might derive these from the context or caller identity\n", - " thread_id = payload.get(\"thread_id\")\n", - " actor_id = payload.get(\"actor_id\")\n", - " \n", - " # Create runtime config for invocation\n", - " config = {\n", - " \"configurable\": {\n", - " \"thread_id\": thread_id,\n", - " \"actor_id\": actor_id,\n", - " }\n", - " }\n", - " \n", - " # Invoke the graph with checkpointing\n", - " output = graph.invoke(inputs, config=config)\n", - " \n", - " # Return the response\n", - " return {\n", - " \"result\": output['messages'][-1],\n", - " \"thread_id\": thread_id,\n", - " \"actor_id\": actor_id,\n", - " \"message_count\": len(output['messages'])\n", - " }\n", - "```" + "As you can see, the AgentCore checkpointer is very powerful for persisting conversational and graph state in the background. For more information, check out the official AgentCore Memory documentation here: https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/memory.html" ] }, { diff --git a/samples/memory/agentcore_memory_checkpointer_human_loop.ipynb b/samples/memory/agentcore_memory_checkpointer_human_loop.ipynb index 7234d3d14..6c7b625ff 100644 --- a/samples/memory/agentcore_memory_checkpointer_human_loop.ipynb +++ b/samples/memory/agentcore_memory_checkpointer_human_loop.ipynb @@ -69,7 +69,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 10, "id": "fb67b4ee-ff3f-4576-8072-8885c2a47e11", "metadata": {}, "outputs": [], @@ -119,10 +119,7 @@ " return a * b\n", "\n", "\n", - "tools = [add, multiply, human_assistance]\n", - "\n", - "# Bind the tools to our LLM so it can understand their structure\n", - "llm_with_tools = llm.bind_tools(tools)" + "tools = [add, multiply, human_assistance]" ] }, { @@ -143,9 +140,9 @@ "outputs": [ { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAANgAAAD5CAIAAADKsmwpAAAAAXNSR0IArs4c6QAAIABJREFUeJztnXdcFNf+v89sb7QtdBAsiIiKATUSY8OYYETF3m4sv1y9liQkGu81ucbc5KvGG3M1otFg9EaJigXEHkUTQUEiqKAUQUFQelu2953fH+uLcHGp7uycZc/zyh+7O7Nz3hsez3zmzMwZDMdxgECQDYXsAAgEQCIiYAGJiIACJCICCpCICChAIiKggEZ2AOjQqg0NlVqlzKCU6Q16XKe1geEtJptCY2AcBxrHgeLmyyY7Tk/A0DiiCaVc//iuvDRP0VSjcXZlcByoHAeaI5+m09jA/x86iyKu0SplehoDKy9U9g3m9R3K7TeUR3auboBEBDiOZ5xvrClTiXxYfYO53gM4ZCd6JbRqY2me/HmRqvKJKjxKEPCaA9mJuoS9i1j4h/R6Ql14lOC1iS5kZ7EwMrEu43yjUqaf/Bd3riPsNZhdi5iWVE+lgzeiRGQHIZCmWk3y3qpJC918A6Hu6e1XxN9P1fHdGMPGOpMdxBqc3V/5+hSBmy+L7CDtYqcino+r8hnICRlnFxaaOLuvMnCE48AwSEtGexxHzDjf4NmPbVcWAgCmr/K695u4oUpDdhDz2J2Ij+/LAAChEb3t0KQrLNjgm5ZUjxth3AfanYipifXDJ9ijhSb6DuHdOttAdgoz2JeI92+IA8Mc2Twq2UFII2Sc8+P7coVUT3aQttiXiGX5itFRfLJTkMzYmcKc1GayU7TFjkQsK1DQ6BQq1Y5+sll8A7l56RKyU7TFjv4qTx8q/IdwrdzoP/7xj7Nnz/bgi2+99VZlZSUBiQCDRRF5MyufqIjYeI+xIxGb6rT9rC5iQUFBD75VXV0tFosJiPOCgOG8iidK4rbfA+xFRK3a2FCpYfOIOuWanp6+cuXKMWPGzJgxY/PmzQ0NDQCAsLCwqqqqr7/+evz48QAAuVy+f//+JUuWmFbbuXOnWq02fT0iIuL48eN//etfw8LCUlNTo6KiAADTp09ft24dEWm5TvT6CsgGFHH7oKlWE7+ljKCNFxYWhoaGHjhwoLq6Oj09ff78+WvWrMFxXK1Wh4aGJicnm1Y7cODAqFGjUlJSsrKyfvvtt8jIyO+//9606O23354zZ863336bmZmp0+lu3rwZGhpaUVFBUODaclXCd88I2njPgP2iDEuhkOi5TkT92JycHBaLtXz5cgqF4u7uHhQU9OTJk5dXW7x4cUREhL+/v+ltbm5uRkbGhx9+CADAMMzJyWn9+vUEJWwD14mmkMA1gmMvIhqNgMEmqg4JCQlRq9UxMTGjRo0aO3asj49PWFjYy6vR6fTbt29v3ry5uLhYr9cDAPj8P8eSgoKCCIr3MhQaxmDBVZXBlYY4uI5USb2OoI0HBgbu3r1bJBLFxsZGR0evXr06Nzf35dViY2Pj4uKio6OTk5Ozs7OXLVvWeimDwSAo3ssomvVUGma15rqCvYjIcaQpiTydEB4evmnTpvPnz3/55ZcSiSQmJsbU57WA43hiYuK8efOio6Pd3d0BADKZjLg8HaOQ6mG7VNZeRGRzqUIvpl5nJGLjd+/ezcjIAACIRKKpU6euW7dOJpNVV1e3Xken06lUKldXV9NbrVablpZGRJiuoFEaXX2YZLVuFnsREQDA5lFLHyqI2HJubu6GDRuSkpLEYnFeXl5CQoJIJPLw8GAyma6urpmZmdnZ2RQKxc/P79y5cxUVFc3NzV999VVISIhUKlUozETy8/MDAKSkpOTl5RERuPiezK0PXBfJ2pGI/sHcp3mEiLh48eLo6OgdO3a89dZbK1as4HK5cXFxNBoNALB8+fKsrKx169apVKqtW7eyWKzZs2fPmDFj5MiRa9euZbFYkyZNqqqqarNBb2/vqKio/fv3x8bGEhG4rEDpP9jaY/sdY0dXaGs1xosHq6NXe5EdhGSeFSlLH8rHz3YlO8j/YEc9IoNJcfVm3vuNwFNnNkHGuYbBo53ITtEWuA6diCZ8qmDv+pL27hw1Go0TJ040u0ir1dLpdAwzM+TRt2/fQ4cOWTrpC3JycmJiYrobKSAgIC4uzuy3iu/JXNwYIi+4jlTsa9dsIjet2WjEh48372J7QyoajYbJNP/HwzCMxyNwToUeRKJQKFyu+RLw4sGqN6NFjny6RTNaALsTEQBw6VD1wDAH25qRwyLA/MPtqEZsYcpyj9sXGuueq8kOYlVSE+sFHgw4LbTTHvHFeY7vK15/V2DrM910kdTEeldf5qARjmQHaRd77BFNhd3sGJ+sq+L8TOgumrcsOI6f3VfpyKfBbKH99ogt3L7Y8DRfGT5V4BcE1wCvRchOacrPlE6Y6+o7EPaO395FBAA0VmkyLjQy2RSvAWz/wVyOg80PadVXaMoLFXevi4e+6Twqkk+hwHWhjVmQiC+oLFEVZcme5itc3Oh8NwbXicZ1pHGdqAYD2cm6AIbhsia9QmrAjXjxPTmLS+k/jDf0TWfYLjrsACRiW2rKVPWVWoVEr5DqKRRMKbOkiSqVqrS0dPDgwRbcJgCA50IDOOA6Uh1caJ792A4u0A0TdgoS0aqUlJRs3Ljx5MmTZAeBDpvpuhG9GyQiAgqQiAgoQCIioACJiIACJCICCpCICChAIiKgAImIgAIkIgIKkIgIKEAiIqAAiYiAAiQiAgqQiAgoQCIioACJiIACJCICCpCICChAIiKgAImIgAIkIgIKkIgIKEAiWhUMw1qecIFoDRLRquA4XldXR3YKGEEiIqAAiYiAAiQiAgqQiAgoQCIioACJiIACJCICCpCICChAIiKgAImIgAIkIgIKkIgIKEAiIqAAiYiAAiQiAgrQA3+swfz585VKJQBAq9U2NjZ6eHiYHkF/5coVsqPBAuoRrcH06dNramqqqqoaGhpwHK+qqqqqqnJwcCA7F0QgEa3B/PnzfX19W3+CYdiYMWPISwQdSERrgGHYzJkzqVRqyyd9+vSZN28eqaHgAoloJebOnevj42N6jWHYuHHjTJUiwgQS0UrQaLT58+czmUwAgLe39+zZs8lOBBdIROsxc+ZMb29vAEB4eDjqDttAIzsAdBiNeHO9TtqgMxIwrhUV8X6KMWX8yHmleQqLb5xOx/geDK6jTf5N0Tji/1B0V5aXLlHKDZ7+HIVUT3ac7sF2oD4rVLj1YY2fLeI525iOSMQ/eZQtLbqrGD/XnULByM7Sc8R1mrRTNdFrvLhOtuQiqhFfUPJAXnhHPnG+h01bCABwcWVOXel7+OsysoN0DyTiCx7cbH5jei+ZlYZKw0ZGiu5caSQ7SDdAIgIAgFppqK/Qsnm2tC/rGJ4zrfqphuwU3QCJCAAA0kadex822SksiYOAYTTYUvWPRDSBKWQ2dozcMbgBKCS29IuQiAgoQCIioACJiIACJCICCpCICChAIiKgAImIgAIkIgIKkIgIKEAiIqAAiYiAAiQiAgqQiDbAmeST27ZvJjsFsSARbYCiogKyIxBO77kU1MrI5fJTp3+5k3W7rKxEwBeGh49bvmwVi8UCABiNxu93b7+VfoNBZ0REvBM8eNjGz2MST13h8wV6vf7goR8y/7hVV1cTHBwSPX3u66+/mHhkxsxJy5b+TSJpPnwkjs1mjwgbvXbNeoFAGPPJitzcewCAq1cvnj97g8fjkf3TCQH1iD0k6UzCseM/z5v7l61bdq1c+dGN1JTDR+JMi06dPnr+QtIHaz/dv/8XNptz8NAPAAAKhQIA2B3779OJx6JnzDt29Py4sRGb/7UhNe266Vt0Ov3EiSMUCiX5zPXD/018mJfz8+EfAQC7/hM3aFDw5Mnv/n49u7daiHrEnjN3zuJxYyP69PE3vc3Ly72TlbFyxYcAgCtXL4x9c+L4cZMAAIsWLruTlWFaR6PRXLl6YeGCpdOiZgEApkROz8vLPRJ/YNzYCNMKXl4+ixctBwAAnsOIsNHFxYWk/Tyrg0TsIXQ6PSv79jfbNz8pKdbr9QAAFxc+AMBgMJSVlUa+M61lzbFvRjx4cB8AUFxcqNVqR4SNblkUMiz08q/nJFKJk6MTACAgYFDLIgcHR4VCbvWfRRpIxB4SdyD20qXklSs/GhE22s3N/aeDey9dPgsAkCvkOI5zONyWNZ2cnE0v5HIZAOCDj/5fm02JmxpNImKYbd/J+iogEXsCjuPnLyTOnrVw6rvRpk9MkgEAOGwOAECn07WsLBa/uK1TIBQBANZ98rmXl0/rrbm6ulsxO6QgEXuCwWBQqVRC4Yv7oLVabcbtNNNrOp3u6upWVlbSsnJ6RqrphbeXr2k2sOEhYaZPxOImHMc5HI7VfwF0oKPmnkCj0Xx9/S7/eq6yqkIiaf73jq+GBIfIZFKFQgEACB899mrKxazsTBzHT50+KpNJTd/icDhLl6w8En/g4cMcrVabmnZ9/YbVu77/ptPmvLx8Cgvz7t3P0mq1xP84ckAi9pBNn29lMVlLl81e/N6M0NdGvv/+WhaTFT1rUnVN1ZL3VgwZMnzD39f+5b3o8vKns2ctBADQaHQAwPx57326/otjCT9HTR///e7tnh7e69b9s9O2ot6diWHYpxvWKJWWn0MMEtAkTAAAUPdccz2hbuoKny6s2zlqtbqursbX18/0NuHEkaNHD50/d8MiG+8ikgbdjRNViz/rY81GXwXUI1qehBNHVvxtUWJSgkTS/NvvV0+e+mXaNDQ/bCeggxXLs3TJColEfPXqhQM/xYpEbtEz5i1auIzsULCDRCSEjz78O9kRbAy0a0ZAARIRAQVIRAQUIBERUIBEREABEhEBBUhEBBQgERFQgEREQAESEQEFSEQAAKBQMUd+rzrbiRtxvjuT7BTdAIkIAABCT0ZZgcJIxPNISaKxWk1j2NIdMEjEFwSOcKx+qiQ7hcVoqtH4B9vSHQhIxBdMnCe6lVSrktvSQ3La4/7vjbgBHxDiQHaQboCu0AYAgKKiIqlUOmxIaPyW8mHj+TxnurMrAzeSHaubGI14Q6W6sUoNjPjE+Tb2gEskInjy5MkXX3xx6NAh08w12deaKh6rAI5J6i1/p5IRx3U6HZPBsPiWAQB8T+ajorwGVb7PIJqfn5+fn19gYCCNZhsHYXYtYkVFhbe3d0lJSb9+/azTYklJycaNG0+ePEnQ9jdu3HjlyhUMw1xcXHg8HpPJ9PT0DAgIWLVqFUEtWgr7FfHWrVvffvvt2bNnrdmoTCa7e/fu+PHjCdr+o0ePYmJiGhoaWn9oNBo9PDwuXrxIUKMWwR4PVuRyuckJK1sIAHBwcCDOQgBAYGDgoEGD2nzI5XIht9AeRTx37ty2bdsAAJGRkdZvvb6+/ocffiC0iYULF7q4uLS8pVAoN2/eJLRFi2BHIpqKkKKioi1btpCVQSqV3rhB7A3OI0aM6Nevn+nHGo3Gvn37Wr/j7wH2ImJKSkpycjIA4NNPPyUxhqur6+rVq4luZe7cuU5OTgAAHx+fhISE3NzcrVu3Et3oK2IXByulpaVxcXHffNP5LDO9hkWLFtXW1l67ds30NjEx8cyZM7/88gvZudoH79XcunWroaGhqamJ7CAvqKur27t3LylNFxQUhIaG5uXlkdJ6p/TmXfP169dPnDghEAhaF+/kYoUasT0GDRqUnZ29ffv206dPkxKgY3rnrrm4uDggIODhw4dDhgwhO8v/QPQ4YlfYtm2bVqvdvBmuB7f0QhEPHz5cXl7+xRdfkB0EXs6dO3f06NH4+HgGMScbewLZtYElMdWCZ8+eJTtIu5BYI7bh8ePHr7/++v3798kO8oLeUyMeOHDAdJA4bdq0LqxODiTWiG3o37//7du3Y2Njjx07RnYW0EvGEXU6XVVVlcFgmDNnDtlZOsE644hd5+DBg9XV1f/8Z+ez1hKNzdeIx44dGzlypK+vL0Tljq1x+fLlAwcOxMfHc7ncLqxOCLbdI6akpFRXV/fv399WLLTCueYeEBkZuXPnzsjIyKysLLIy2KqIV69eBQAMGTJk3bp1ZGfpBvDUiG3o06dPWlrawYMHDx8+TEoAmxRxz549Dx8+BAC4u9vYo3JgqxHbsH//folEsmHDBhLaJvuwvXsUFhbiOJ6bm0t2kN7MtWvXpk6dKhaLrdmoLfWImzZtKigoAAAMHTqU7Cw9BM4asQ0RERE//vjjrFmz0tPTrdaobYgoFotVKtXo0aNnzpxJdpZXAtoasQ2enp6mM/U//fSTdVq0ARG3bdtWWVnJZrOnTJlCdpZXBfIasQ27d+/W6XQff/yxFdqCfRwxNTW1vr5+9mz0wBzSSEtL27JlS3x8vKsrkfdKW7Mg7RaxsbE4jqtUKrKDWBJ4zjV3i/r6+nfeeScnJ4e4JiDdNSclJTU1NQEATDe99xpYLNb9+/fJTtFthELh5cuX9+7dW1lZSVATkO6a1Wo1jUazlVkKuoVOp9Pr9RiG2dy/sbCwsKysLAwjZJIxSHtEFovVKy00PVmczWafOHGiurqa7Czd4NGjRwMHDiTIQnhF3LVrV1JSEtkpCGTJkiUxMTFkp+gGhYWFL9+6b0EgFVGr1ep0OrJTEMuJEycAAM+fPyc7SJcoKCgICgoibvuQivjxxx/PmjWL7BTWIDU19e7du2Sn6Bw77RHpdHpvrRHbsHjx4suXL5OdonMePXpkjyL2+hqxNaYLpDMzM8kO0i4FBQWEWgiviPZQI7ahoqLiypUrZKcwD9H7ZXifYP/xxx8TN1IAJ7Nnzz516hTZKcxTUFBA9B3ikPaI9lMjtsZ089fx48fJDtIWK/SIkIpoVzViGwQCAVSzghiNxsePHw8cOJDQViAV0Q5rxBYmT57s5+dHdoo/IXoE0QSkItrPOKJZwsLCAACQzJpihf0yvCLaZ43Yhujo6KNHj5Kdwr5FtOcasYXhw4dPmDCB7BT2vWu25xqxNZ6enqaukawAer3+6dOnAwYMILohSEW08xqxDfv374+Pj2/9yeTJk63TtHW6Q3hFRDVia9zc3ObNmyeXy1UqFQBgypQpjY2Nn332mRWatk6BCO+ZlV27dvn6+tr6zaMWhMFgMBiMMWPGODs719XVYRiWn5/f1NTE5/MJbbegoGDEiBGENmEC0h4R1YhmEQgENTU1ptdNTU1WeJKP1XpESO9Z0el0GIahvXNrZs2aVV5e3vLWaDSGh4fv2bOHuBa1Wu24ceNu375NXBMtQNojohqxDdHR0U+fPjUa/3yGNIVCKS8vLy0tJa5Rqx2pwCsiGkdsw5kzZ6Kjo/38/JydnU3dIQCgtraW0L2z1fbL8B6soBrxZTZt2gQAePDgwc2bN2/evNnY2CgRK1Ov35k5bRFBLRblPxs+fLhMrO/xFnAcOPK75BhcNeLEiRMlEklLJAzDcBx3d3e/dOkS2dHgIjul6cEtsRHT6zU4m7D7o/V6PZVGe5XLQl08mJWPlf2HcUdNETjy6R2sCVePGB4efunSJQrlz4KBQqFERUWRGgo6fj1cw+PTI5f78pw7+tNCgl5nbK7Tnvq+YuYaLxfXdmeYhqtGXLBggemkVgve3t4LFiwgLxF0XP65xsWdOWyswCYsBADQ6BShF2vuJ/5n9lZKm9ott+AScfDgwcHBwS1vMQx75513TOU5AgBQVqBgsKlBr8PyaMFuMWGeR+alpvaWwiUiAOC9994TCoWm197e3nPnziU7EUTUPdfQmdD9ybqIixvzSY6svaXQ/aqgoKCWmYkjIyPhebAoDGiUBqEHk+wUPYRKw3wHcpvrtWaXQiciAGDp0qUCgcDd3R11h21QSA16Wx7UaqrVtndz5qseNVeVKCUNeoVMr5QajAag1xu78KVOEYwZuIrL5WZf1gBQ++qbY7IpGMA4jlSOI1XgyRR52mqn0ovpoYjlhYrie/LSPIWLOxvHMSqdSqFTKVSqpUYlg4eOBwDIFBbZGJArMaPBYKjUG7RqnVqiUxv6DeUGhjm49bGxGQp7Md0WsfqpKu1MI53DwGjMfqNdaHQqMcEIRKvSNzYoUpPFbA54c4bAWWQbj0/r3XRPxGvH66tK1QJ/PtfFhvsSBpvG93ECAEjrFImxVYNGOoRPFZAdyt7p6sGKXmf8+atytYHp+5qnTVvYGkdXbr/RPnU1lDN7iZoaGtFFuiSiQY/HbSz1CHLjCUh7jCpxOHs50p0cE3bYxoSZvZXORTQa8X0bSoIi/Jlc2zin1AN4Ao6jF//w/5V3YV0EIXQu4tFtzwaEe1klDJlwnFl8H+eLB21pgvXeRCci3khscPZxZnLt4rjSwZWnA8yc1Gayg9gjHYnYWKV5mqdwEPGsmIdknD2dbiU3QHWNpp3QkYhpyY1Cf2LvVoQQ9wCXm8mNZKewO9oVsaZMpTdQHEQc6+bpKjkPr63fNEquEFt8y0I/58pSjUZlsPiWbZQZMycdiSf8YbntivgkV4FRe+1hcidglLJ8JdkhLMO/vvrHpctnyU7ROe2KWPJA4eAKaXdINBw+93GOnOwUlqGoqIDsCF3C/Ck+cZ2W7UAn7mC57NmDq7//9LyigMd1GTRwzOQJ77NYXABAeuaplNRDq5bvO5Kwsbau1MOt/9jwBSNem2r61oVfY7NzLzEZnOFD33YV+hKUDQDg6MqpzpcSt32rMSEiDADw7Y6v9+3fef7sDQBAenrq4SNx5c+eOjk59+8/8KMP/u7m5m5auYNFLWT+kX7ixJFHRfl8vjA4eNiK9z8QCIQWiWq+R5Q369Uqi1zQZYaGxuc//vyBTqdZu+KnJQu3V9c+3ndolcGgBwBQaXSVSpZ8ccfcGZ99+1Xm0OCJJ5P/T9xcAwDIuJOYcef0zHc//WjlfwUunim/HyQonukWBblYp5D2/DZKSPj1UjoA4NP1m0wWZt/944svP508+d2TCZc2b/qmtrZ61+5vTGt2sKiF4sePNn720fDhI34+dPrDDzaUlBRv//eXlopqXkSl1EAl7LKae7m/0qj0pQu2u4n83F37zpn+eWV1UV5hqmmpwaB7a8L7fXyGYBgWFvIujuOV1cUAgFu3Tw4dHDE0eCKH4zjitan9+4YRFM8Eg0VVSGxexDYc+u++sW9OnD1roZOT8+DBQ1ev+iQz89ajooKOF7WQ9zCHxWItXrTczc191Mjw777dt2DBUktla0dEmZ7KIOpO07JnD3y8g7jcF7dE8V08BHzvp+U5LSv4eg02veCwHQEAKrUMx/GGpudurv4t63h7BhIUzwSdTVXafo/YhtLSx4GBg1veDgwIAgA8epTf8aIWgoeEqNXqjZ/HnDp9tKLyuZOT8/AQi3UH7dqGAaIGdVVq+fPKgvWbRrX+UCr7c+ju5avJ1RqF0WhgMv88eGIw2ATFM2E0ANC7njgkl8s1Gg2T+eeVUxwOBwCgVCo6WNR6CwEDAr/Ztjst7Xrcgdgf9u0MfW3k0iUrg4OHWSSeeRE5jjSDTm2RBl7GwUHg3yfk7YkrWn/I5Tp18BUWk0uhUHWtImm0xA6vGLQGriNcsw+8IiwWCwCgVqtaPlEoFQAAAV/YwaI2Gxk1MnzUyPBlS/929+4fiUnHP/s85kzSNSrVAlWc+V0zx4Fq0BE1ouvpNqBZUtPXb3j/vqGm/3g8F1dhR08WwTDMxdmj7NnDlk8Ki9IJimdCqzZwHG3v4vMOoNFoAwMG5ec/aPnE9LpvvwEdLGq9hZycu3/cyQAACIWit9+eumb1Oplc1tBQb5F45kV05NPoDKJ2TGPDFxiNxnOXd2q16rr68gtX9ny3Z2F17ZOOvzUseNLDgt9zHl4DAPx280h5RR5B8UxXvvGcab2gR2QymSKRa3Z25v2cbL1eHz1j3q30G4mJx6Uy6f2c7B/2/ee14SMG9B8IAOhgUQt5+blf/mvD+QtJzc3igsK8pDMJQqFIKBRZJKr5/9dOQoZebVDLtCwHyw8lcjiO69ce+/1m/K79S+rqy3y9B8+Z8XmnBx+Txi1TKMTJl7775eTn/n1CpkXGHDv1BUFXJ0hrFS6uveSs0qKFy//78/47WRnHj12YPPnd+oa6E6fi9/zwnZube1jo6399f61ptQ4WtTB3zuLmZvGevTv+s3Mrg8GYOOHtnf+Js8h+uaPZwG5fbKwow0V97fH+9qr8uhERvAHDHcgO0pZfD9d49uP5D7HV66HOxJZP/5unk9DMP/J2T/H1H8bF9b1t/KKLYJjBf3AvvCkCZtotg0TeLDYHl9QqnNzM/0maJXU79pifp4vN5Kk05s/Vuov6rl1xoKdpzfDPLRHtLTIY9FSqmR/o6z14xZLd7X2rvlTsH8SmMWCcA6MX01E9Pnam8PSuyvZEdODxP1kdb3aRVqtmMMzf6UehWPgIoL0MAACtTsOgm5nUgUZrt/A1Goz1TyVz1vSzXEBEl+hICycBfdAoXmO9zEFkplqiUml8F09z37Mqls0grZaMn2OZs/iIbtHJDih8qlDZIFc2EzW4DRWSaimPawwa1dHQOoIgOq+E5n3i/ex+jU7dyw9cmmvkqib5pIWuZAexU7pUkq/c3vdx+vNe3C9KauRArZi/3ofsIPZLl0TEMGz1jv7SyiZpbbszftou4udiBqaasYr8etee6cYgxfz1PgKBoTSzQlpnoeniyEZcKX10o9x/IC1yadtLkRFWpnuDKW9ECYJGOaSdaWwoUeJUuqOIa4vzkKikGlm90qjRCD3pU77sw2T3qosbbJRuj+q5uDKmr/SoKVM/zpGXPKhlcmhGI0ZlUKl0KoVGBYRdxfgqYBim1xmMWr1ea9CqdEw2ZUAIL+A1EZoZER56OLzs7sdy92O9OUPYVKOVNOgUUr1CojfojQY9jCIyWBiFSuE6cjiOVKEXg+dke714r+dVz3Pw3Rl8d9SvIF4VdEbVluA60Wx60gO+O7O94g2JaEuwuZSGSg3ZKXqITmusKFY4Cc3vP5GItoRbH5ZOY6uT8jTVaDq4xBOJaEv4BHAwDNz/zSYnK/vtWNUb09qdNB+u5zUjukJaUr1Oh/cb6ijwtIFZ9RW6zPHgAAAAZ0lEQVRSvaRe83tCzV8+9+W2P16BRLRJ8m5L8jOkaqVBQ9jMMBZB5MVsrtP6D+G+ESXs+HGWSEQbBseBVg21iLgRZ3G7dOIKiYiAAnSwgoACJCICCpCICChAIiKgAImIgAIkIgIK/j88u/2J087bqAAAAABJRU5ErkJggg==", + "image/png": "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", "text/plain": [ - "" + "" ] }, "execution_count": 5, @@ -155,7 +152,7 @@ ], "source": [ "graph = create_react_agent(\n", - " model=llm_with_tools,\n", + " model=llm,\n", " tools=tools,\n", " prompt=\"You are a helpful assistant\",\n", " checkpointer=checkpointer,\n", @@ -186,7 +183,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 7, "id": "c50a6a6e-b16f-41dc-8f16-1d102f6debc4", "metadata": {}, "outputs": [ @@ -196,21 +193,21 @@ "text": [ "================================\u001b[1m Human Message \u001b[0m=================================\n", "\n", - "I'm frustrated with my current support. Please loop in a human.\n", + "I would like to work with a customer service human agent.\n", "==================================\u001b[1m Ai Message \u001b[0m==================================\n", "\n", - "[{'type': 'text', 'text': \"I understand you're feeling frustrated and would like to speak with a human for support. I can certainly help you request human assistance.\"}, {'type': 'tool_use', 'name': 'human_assistance', 'input': {'query': 'User is frustrated with current support and has requested to speak with a human representative.'}, 'id': 'tooluse_6FDef0XWQhGv0aiA3D3_IQ'}]\n", + "[{'type': 'text', 'text': \"I understand you'd like to connect with a customer service human agent. I can help you with that by requesting human assistance.\"}, {'type': 'tool_use', 'name': 'human_assistance', 'input': {'query': 'Customer requesting to speak with a human customer service agent.'}, 'id': 'tooluse_tFutleh5RK-mGOvg6YQwYw'}]\n", "Tool Calls:\n", - " human_assistance (tooluse_6FDef0XWQhGv0aiA3D3_IQ)\n", - " Call ID: tooluse_6FDef0XWQhGv0aiA3D3_IQ\n", + " human_assistance (tooluse_tFutleh5RK-mGOvg6YQwYw)\n", + " Call ID: tooluse_tFutleh5RK-mGOvg6YQwYw\n", " Args:\n", - " query: User is frustrated with current support and has requested to speak with a human representative.\n" + " query: Customer requesting to speak with a human customer service agent.\n" ] } ], "source": [ - "user_input = \"I'm frustrated with my current support. Please loop in a human.\"\n", - "config = {\"configurable\": {\"thread_id\": \"2\", \"actor_id\": \"demo-notebook\"}}\n", + "user_input = \"I would like to work with a customer service human agent.\"\n", + "config = {\"configurable\": {\"thread_id\": \"1\", \"actor_id\": \"demo-notebook\"}}\n", "\n", "events = graph.stream(\n", " {\"messages\": [{\"role\": \"user\", \"content\": user_input}]},\n", @@ -234,7 +231,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 8, "id": "0629af32-b660-4157-97ca-597b21ec66c3", "metadata": {}, "outputs": [ @@ -244,7 +241,7 @@ "('tools',)" ] }, - "execution_count": 9, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } @@ -266,7 +263,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 9, "id": "aaf88072-ab8d-4e5f-a605-6d65e66f6207", "metadata": {}, "outputs": [ @@ -276,21 +273,23 @@ "text": [ "==================================\u001b[1m Ai Message \u001b[0m==================================\n", "\n", - "[{'type': 'text', 'text': \"I understand you're feeling frustrated and would like to speak with a human for support. I can certainly help you request human assistance.\"}, {'type': 'tool_use', 'name': 'human_assistance', 'input': {'query': 'User is frustrated with current support and has requested to speak with a human representative.'}, 'id': 'tooluse_6FDef0XWQhGv0aiA3D3_IQ'}]\n", + "[{'type': 'text', 'text': \"I understand you'd like to connect with a customer service human agent. I can help you with that by requesting human assistance.\"}, {'type': 'tool_use', 'name': 'human_assistance', 'input': {'query': 'Customer requesting to speak with a human customer service agent.'}, 'id': 'tooluse_tFutleh5RK-mGOvg6YQwYw'}]\n", "Tool Calls:\n", - " human_assistance (tooluse_6FDef0XWQhGv0aiA3D3_IQ)\n", - " Call ID: tooluse_6FDef0XWQhGv0aiA3D3_IQ\n", + " human_assistance (tooluse_tFutleh5RK-mGOvg6YQwYw)\n", + " Call ID: tooluse_tFutleh5RK-mGOvg6YQwYw\n", " Args:\n", - " query: User is frustrated with current support and has requested to speak with a human representative.\n", + " query: Customer requesting to speak with a human customer service agent.\n", "=================================\u001b[1m Tool Message \u001b[0m=================================\n", "Name: human_assistance\n", "\n", "I'm sorry to hear that you are frustrated. Looking at the past conversation history, I can see that you've requested a refund. I've gone ahead and credited it to your account.\n", "==================================\u001b[1m Ai Message \u001b[0m==================================\n", "\n", - "Thank you for your patience. A human representative has reviewed your case and has taken action on your behalf. They mentioned that they've processed a refund and it has been credited to your account. \n", + "A human customer service agent has responded to your request and provided the following information:\n", + "\n", + "\"I'm sorry to hear that you are frustrated. Looking at the past conversation history, I can see that you've requested a refund. I've gone ahead and credited it to your account.\"\n", "\n", - "Is there anything else you'd like help with regarding your account or the refund process?\n" + "Is there anything else you'd like help with regarding your refund or any other customer service matters?\n" ] } ], From 86bdc1e159ea65b24aa7f3afafd1a95d902f963e Mon Sep 17 00:00:00 2001 From: Piyush Jain Date: Mon, 22 Sep 2025 18:23:52 -0700 Subject: [PATCH 6/8] Update samples/memory/agentcore_memory_checkpointer.ipynb --- samples/memory/agentcore_memory_checkpointer.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/memory/agentcore_memory_checkpointer.ipynb b/samples/memory/agentcore_memory_checkpointer.ipynb index cbfc50a23..cf2416ec1 100644 --- a/samples/memory/agentcore_memory_checkpointer.ipynb +++ b/samples/memory/agentcore_memory_checkpointer.ipynb @@ -153,7 +153,7 @@ "source": [ "graph = create_react_agent(\n", " model=llm,\n", - " tools=[add, multiply],\n", + " tools=tools,\n", " prompt=\"You are a helpful assistant\",\n", " checkpointer=checkpointer,\n", ")\n", From 36edb27bbe8b041f7e68ef0d25ed08f33a8d39f5 Mon Sep 17 00:00:00 2001 From: Piyush Jain Date: Mon, 22 Sep 2025 18:33:19 -0700 Subject: [PATCH 7/8] Update samples/memory/agentcore_memory_checkpointer.ipynb --- samples/memory/agentcore_memory_checkpointer.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/memory/agentcore_memory_checkpointer.ipynb b/samples/memory/agentcore_memory_checkpointer.ipynb index cf2416ec1..88f1e129e 100644 --- a/samples/memory/agentcore_memory_checkpointer.ipynb +++ b/samples/memory/agentcore_memory_checkpointer.ipynb @@ -29,7 +29,7 @@ "metadata": {}, "outputs": [], "source": [ - "%pip install langchain" + "%pip install langchain langchain-aws" ] }, { From 3c24436727bba61052c10a9745a479a42fc47e68 Mon Sep 17 00:00:00 2001 From: Jack Gordley Date: Tue, 23 Sep 2025 15:30:36 -0700 Subject: [PATCH 8/8] Adding client header for langgraph memory to boto --- .../agentcore/helpers.py | 17 +++++++++++++---- .../langgraph_checkpoint_aws/agentcore/saver.py | 4 ++-- .../tests/unit_tests/agentcore/test_saver.py | 15 +++++++-------- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/helpers.py b/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/helpers.py index d8043aab6..0af27bbf0 100644 --- a/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/helpers.py +++ b/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/helpers.py @@ -12,6 +12,7 @@ from typing import Any, Dict, List, Union import boto3 +from botocore.config import Config from langgraph.checkpoint.base import CheckpointTuple, SerializerProtocol from langgraph_checkpoint_aws.agentcore.constants import ( @@ -121,12 +122,20 @@ def deserialize_event(self, data: str) -> EventType: class AgentCoreEventClient: """Handles low-level event storage and retrieval from AgentCore Memory for checkpoints.""" - def __init__(self, memory_id: str, serializer: EventSerializer, **boto3_kwargs): + def __init__( + self, memory_id: str, serializer: EventSerializer = None, **boto3_kwargs + ): self.memory_id = memory_id self.serializer = serializer - self.client = boto3.client("bedrock-agentcore", **boto3_kwargs) - def store_event(self, event: EventType, session_id: str, actor_id: str) -> None: + config = Config( + user_agent_extra="x-client-framework:langgraph_agentcore_memory" + ) + self.client = boto3.client("bedrock-agentcore", config=config, **boto3_kwargs) + + def store_blob_event( + self, event: EventType, session_id: str, actor_id: str + ) -> None: """Store an event in AgentCore Memory.""" serialized = self.serializer.serialize_event(event) @@ -138,7 +147,7 @@ def store_event(self, event: EventType, session_id: str, actor_id: str) -> None: payload=[{"blob": serialized}], ) - def store_events_batch( + def store_blob_events_batch( self, events: List[EventType], session_id: str, actor_id: str ) -> None: """Store multiple events in a single API call to AgentCore Memory.""" diff --git a/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/saver.py b/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/saver.py index d8b291fe3..bd13dd4f9 100644 --- a/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/saver.py +++ b/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/saver.py @@ -184,7 +184,7 @@ def put( ) events_to_store.append(checkpoint_event) - self.checkpoint_event_client.store_events_batch( + self.checkpoint_event_client.store_blob_events_batch( events_to_store, checkpoint_config.session_id, checkpoint_config.actor_id ) @@ -226,7 +226,7 @@ def put_writes( writes=write_items, ) - self.checkpoint_event_client.store_event( + self.checkpoint_event_client.store_blob_event( writes_event, checkpoint_config.session_id, checkpoint_config.actor_id ) diff --git a/libs/langgraph-checkpoint-aws/tests/unit_tests/agentcore/test_saver.py b/libs/langgraph-checkpoint-aws/tests/unit_tests/agentcore/test_saver.py index aa64f2d3e..1de512199 100644 --- a/libs/langgraph-checkpoint-aws/tests/unit_tests/agentcore/test_saver.py +++ b/libs/langgraph-checkpoint-aws/tests/unit_tests/agentcore/test_saver.py @@ -3,7 +3,7 @@ """ import json -from unittest.mock import MagicMock, Mock, patch +from unittest.mock import ANY, MagicMock, Mock, patch import pytest from langchain_core.runnables import RunnableConfig @@ -156,7 +156,7 @@ def test_init_with_default_client(self, memory_id): assert isinstance(saver.serializer, EventSerializer) assert isinstance(saver.checkpoint_event_client, AgentCoreEventClient) assert isinstance(saver.processor, EventProcessor) - mock_boto3_client.assert_called_once_with("bedrock-agentcore") + mock_boto3_client.assert_called_once_with("bedrock-agentcore", config=ANY) def test_init_with_custom_parameters(self, memory_id): with patch("boto3.client") as mock_boto3_client: @@ -170,8 +170,7 @@ def test_init_with_custom_parameters(self, memory_id): assert saver.memory_id == memory_id mock_boto3_client.assert_called_once_with( - "bedrock-agentcore", - region_name="us-west-2", + "bedrock-agentcore", region_name="us-west-2", config=ANY ) def test_get_tuple_success( @@ -742,8 +741,8 @@ def client(self, mock_boto_client, serializer): mock_boto3_client.return_value = mock_boto_client yield AgentCoreEventClient("test-memory-id", serializer) - def test_store_event(self, client, mock_boto_client, sample_checkpoint_event): - client.store_event(sample_checkpoint_event, "session_id", "actor_id") + def test_store_blob_event(self, client, mock_boto_client, sample_checkpoint_event): + client.store_blob_event(sample_checkpoint_event, "session_id", "actor_id") mock_boto_client.create_event.assert_called_once() call_args = mock_boto_client.create_event.call_args[1] @@ -752,7 +751,7 @@ def test_store_event(self, client, mock_boto_client, sample_checkpoint_event): assert call_args["sessionId"] == "session_id" assert len(call_args["payload"]) == 1 - def test_store_events_batch( + def test_store_blob_events_batch( self, client, mock_boto_client, @@ -760,7 +759,7 @@ def test_store_events_batch( sample_channel_data_event, ): events = [sample_checkpoint_event, sample_channel_data_event] - client.store_events_batch(events, "session_id", "actor_id") + client.store_blob_events_batch(events, "session_id", "actor_id") mock_boto_client.create_event.assert_called_once() call_args = mock_boto_client.create_event.call_args[1]