From 61c7e585146d1c8b92891f2e26065ef22c8c7441 Mon Sep 17 00:00:00 2001 From: Jack Gordley Date: Tue, 30 Sep 2025 21:20:05 -0700 Subject: [PATCH 1/3] Adding tool factories for the agentcore memory store --- .../langgraph_checkpoint_aws/__init__.py | 6 + .../agentcore/__init__.py | 6 + .../agentcore/tools.py | 284 ++++++++++++ .../tests/unit_tests/agentcore/test_tools.py | 390 +++++++++++++++++ ..._memory_store_long_term_search_tools.ipynb | 406 ++++++++++++++++++ 5 files changed, 1092 insertions(+) create mode 100644 libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/tools.py create mode 100644 libs/langgraph-checkpoint-aws/tests/unit_tests/agentcore/test_tools.py create mode 100644 samples/memory/agentcore_memory_store_long_term_search_tools.ipynb diff --git a/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/__init__.py b/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/__init__.py index 2f79b184..7c6f8323 100644 --- a/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/__init__.py +++ b/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/__init__.py @@ -8,6 +8,10 @@ from langgraph_checkpoint_aws.agentcore.store import ( AgentCoreMemoryStore, ) +from langgraph_checkpoint_aws.agentcore.tools import ( + create_search_memory_tool, + create_store_event_tool, +) __version__ = "0.1.2" SDK_USER_AGENT = f"LangGraphCheckpointAWS#{__version__}" @@ -16,5 +20,7 @@ __all__ = [ "AgentCoreMemorySaver", "AgentCoreMemoryStore", + "create_search_memory_tool", + "create_store_event_tool", "SDK_USER_AGENT", ] diff --git a/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/__init__.py b/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/__init__.py index dc344c3c..6cae4a02 100644 --- a/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/__init__.py +++ b/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/__init__.py @@ -1,7 +1,13 @@ from langgraph_checkpoint_aws.agentcore.saver import AgentCoreMemorySaver from langgraph_checkpoint_aws.agentcore.store import AgentCoreMemoryStore +from langgraph_checkpoint_aws.agentcore.tools import ( + create_search_memory_tool, + create_store_event_tool, +) __all__ = [ "AgentCoreMemorySaver", "AgentCoreMemoryStore", + "create_search_memory_tool", + "create_store_event_tool", ] diff --git a/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/tools.py b/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/tools.py new file mode 100644 index 00000000..bcee85bb --- /dev/null +++ b/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/tools.py @@ -0,0 +1,284 @@ +""" +Tool factories for AgentCore Memory Store operations. + +This module provides tool factories for creating LangChain tools that interact with +AgentCore Memory Store, following the pattern established by langmem. +""" + +import functools +import logging +import typing +import uuid +from typing import Optional + +from langchain_core.messages import BaseMessage +from langchain_core.tools import StructuredTool +from langgraph.store.base import BaseStore +from langgraph.utils.config import get_config, get_store + +if typing.TYPE_CHECKING: + from langchain_core.tools.base import ArgsSchema + +try: + from pydantic import ConfigDict +except ImportError: + ConfigDict = None + +logger = logging.getLogger(__name__) + + +class NamespaceTemplate: + """Template for namespace configuration with runtime substitution.""" + + def __init__(self, namespace: tuple[str, ...] | str): + if isinstance(namespace, str): + self.namespace_parts = (namespace,) + else: + self.namespace_parts = namespace + + def __call__(self, config: dict | None = None) -> tuple[str, ...]: + """Format namespace with runtime configuration.""" + if not config: + try: + config = get_config() + except RuntimeError: + # If we're outside a runnable context, just return the template + # This allows the tool to be created outside of a runnable context + return self.namespace_parts + + configurable = config.get("configurable", {}) + formatted_parts = [] + + for part in self.namespace_parts: + if part.startswith("{") and part.endswith("}"): + # Format with configurable values + try: + formatted_part = part.format(**configurable) + formatted_parts.append(formatted_part) + except KeyError as e: + raise ValueError( + f"Missing required configurable key for namespace: {e}" + ) + else: + formatted_parts.append(part) + + return tuple(formatted_parts) + + +def create_search_memory_tool( + namespace: tuple[str, ...] | str, + *, + instructions: str = "Search for relevant memories and user preferences to provide context for your responses.", + store: BaseStore | None = None, + response_format: typing.Literal["content", "content_and_artifact"] = "content", + name: str = "search_memory", +): + """Create a tool for searching memories in AgentCore Memory Store. + + This function creates a tool that allows AI assistants to search through + processed memories using semantic search powered by AgentCore Memory service. + + Args: + namespace: The namespace for searching memories. For AgentCore, this is + typically ("facts", "{actor_id}") for user facts/preferences. + instructions: Custom instructions for when to use the search tool. + store: The BaseStore to use. If not provided, uses the configured store. + response_format: Whether to return just content or content with artifacts. + name: The name of the tool. + + Returns: + A StructuredTool for memory search. + + Example: + ```python + search_tool = create_search_memory_tool( + namespace=("facts", "{actor_id}"), + ) + ``` + """ + namespacer = NamespaceTemplate(namespace) + initial_store = store + + async def asearch_memory( + query: str, + *, + limit: int = 10, + offset: int = 0, + filter: Optional[dict] = None, + ): + """Async version of search_memory.""" + store = _get_store(initial_store) + namespace = namespacer() + + memories = await store.asearch( + namespace, + query=query, + filter=filter, + limit=limit, + offset=offset, + ) + + if response_format == "content_and_artifact": + return _format_search_results(memories), memories + return _format_search_results(memories) + + def search_memory( + query: str, + *, + limit: int = 10, + offset: int = 0, + filter: Optional[dict] = None, + ): + """Sync version of search_memory.""" + store = _get_store(initial_store) + namespace = namespacer() + + memories = store.search( + namespace, + query=query, + filter=filter, + limit=limit, + offset=offset, + ) + + if response_format == "content_and_artifact": + return _format_search_results(memories), memories + return _format_search_results(memories) + + description = f"""Search AgentCore Memory for relevant information. +{instructions}""" + + # Create the tool with proper response format handling + if response_format == "content_and_artifact": + return _SearchToolWithArtifacts.from_function( + search_memory, + asearch_memory, + name=name, + description=description, + ) + else: + return StructuredTool.from_function( + search_memory, + asearch_memory, + name=name, + description=description, + ) + + +def _get_store(initial_store: BaseStore | None = None) -> BaseStore: + """Get the store instance, either from parameter or configuration.""" + try: + if initial_store is not None: + return initial_store + else: + return get_store() + except RuntimeError as e: + raise RuntimeError( + "Could not get store. Make sure a store is configured in your graph." + ) from e + + +def _format_search_results(memories: list) -> str: + """Format search results for display.""" + if not memories: + return "No memories found." + + results = [] + for i, memory in enumerate(memories, 1): + content = memory.value.get("content", "") + score = memory.score + memory_id = memory.key + + result_str = f"{i}. {content}" + if score is not None: + result_str += f" (relevance: {score:.2f})" + result_str += f" [id: {memory_id}]" + + results.append(result_str) + + return "\n".join(results) + + +class _SearchToolWithArtifacts(StructuredTool): + """Search tool that returns both content and artifacts as a tuple.""" + + @functools.cached_property + def tool_call_schema(self) -> "ArgsSchema": + tcs = super().tool_call_schema + try: + if tcs.model_config: + tcs.model_config["json_schema_extra"] = _ensure_schema_contains_required + elif ConfigDict is not None: + tcs.model_config = ConfigDict( + json_schema_extra=_ensure_schema_contains_required + ) + except Exception: + pass + return tcs + + +def _ensure_schema_contains_required(schema: dict) -> None: + """Ensure schema contains required fields.""" + schema.setdefault("required", []) + + +# Additional helper tool for direct event storage (AgentCore specific) +def create_store_event_tool( + *, + store: Optional[BaseStore] = None, + name: str = "store_conversation_event", +): + """Create a tool for storing conversation events directly in AgentCore Memory. + + This is an AgentCore-specific tool that allows storing conversation events + that will be processed into memories by the AgentCore service. + + Args: + store: The BaseStore to use. If not provided, uses the configured store. + name: The name of the tool. + + Returns: + A StructuredTool for storing conversation events. + + Example: + ```python + store_tool = create_store_event_tool() + ``` + """ + initial_store = store + + async def astore_event( + message: BaseMessage, + actor_id: str, + session_id: str, + ): + """Store a conversation event asynchronously.""" + store = _get_store(initial_store) + namespace = (actor_id, session_id) + key = str(uuid.uuid4()) + + await store.aput(namespace, key, {"message": message}) + return f"Stored conversation event {key}" + + def store_event( + message: BaseMessage, + actor_id: str, + session_id: str, + ): + """Store a conversation event synchronously.""" + store = _get_store(initial_store) + namespace = (actor_id, session_id) + key = str(uuid.uuid4()) + + store.put(namespace, key, {"message": message}) + return f"Stored conversation event {key}" + + description = """Store a conversation event in AgentCore Memory. +This event will be automatically processed into searchable memories by the service.""" + + return StructuredTool.from_function( + store_event, + astore_event, + name=name, + description=description, + ) diff --git a/libs/langgraph-checkpoint-aws/tests/unit_tests/agentcore/test_tools.py b/libs/langgraph-checkpoint-aws/tests/unit_tests/agentcore/test_tools.py new file mode 100644 index 00000000..1e0837a2 --- /dev/null +++ b/libs/langgraph-checkpoint-aws/tests/unit_tests/agentcore/test_tools.py @@ -0,0 +1,390 @@ +""" +Unit tests for AgentCore Memory Store tools. +""" + +from unittest.mock import AsyncMock, Mock, patch + +import pytest +from langchain_core.messages import AIMessage, HumanMessage +from langchain_core.tools import StructuredTool +from langgraph.store.base import SearchItem + +from langgraph_checkpoint_aws.agentcore.tools import ( + NamespaceTemplate, + create_search_memory_tool, + create_store_event_tool, +) + + +class TestNamespaceTemplate: + """Test suite for NamespaceTemplate.""" + + def test_init_with_string(self): + """Test initialization with string namespace.""" + template = NamespaceTemplate("test_namespace") + assert template.namespace_parts == ("test_namespace",) + + def test_init_with_tuple(self): + """Test initialization with tuple namespace.""" + template = NamespaceTemplate(("part1", "part2", "part3")) + assert template.namespace_parts == ("part1", "part2", "part3") + + def test_call_with_static_namespace(self): + """Test calling template with static namespace (no placeholders).""" + template = NamespaceTemplate(("static", "namespace")) + # Provide a config to avoid runtime context error + config = {"configurable": {}} + result = template(config) + assert result == ("static", "namespace") + + def test_call_with_placeholders(self): + """Test calling template with placeholders.""" + template = NamespaceTemplate(("facts", "{actor_id}")) + config = {"configurable": {"actor_id": "user123"}} + result = template(config) + assert result == ("facts", "user123") + + def test_call_with_multiple_placeholders(self): + """Test calling template with multiple placeholders.""" + template = NamespaceTemplate(("{actor_id}", "{thread_id}", "memories")) + config = {"configurable": {"actor_id": "user123", "thread_id": "thread456"}} + result = template(config) + assert result == ("user123", "thread456", "memories") + + def test_call_missing_placeholder_value(self): + """Test calling template with missing placeholder value.""" + template = NamespaceTemplate(("facts", "{actor_id}")) + config = {"configurable": {}} # Missing actor_id + + with pytest.raises(ValueError, match="Missing required configurable key"): + template(config) + + @patch("langgraph_checkpoint_aws.agentcore.tools.get_config") + def test_call_without_config_uses_get_config(self, mock_get_config): + """Test calling template without config uses get_config.""" + # Test successful get_config call + mock_get_config.return_value = {"configurable": {"actor_id": "user123"}} + template = NamespaceTemplate(("facts", "{actor_id}")) + result = template() + assert result == ("facts", "user123") + mock_get_config.assert_called_once() + + # Test RuntimeError handling - returns template as-is + mock_get_config.reset_mock() + mock_get_config.side_effect = RuntimeError("Not in runnable context") + template = NamespaceTemplate(("facts", "{actor_id}")) + result = template() + assert result == ("facts", "{actor_id}") # Returns template unchanged + mock_get_config.assert_called_once() + + +class TestCreateSearchMemoryTool: + """Test suite for create_search_memory_tool.""" + + @pytest.fixture + def mock_store(self): + """Create a mock store with search capabilities.""" + store = Mock() + store.search = Mock() + store.asearch = AsyncMock() + return store + + @pytest.fixture + def sample_search_results(self): + """Create sample search results.""" + return [ + SearchItem( + namespace=("facts", "user123"), + key="mem-123", + value={"content": "User likes coffee"}, + created_at="2024-01-01T00:00:00Z", + updated_at="2024-01-01T00:00:00Z", + score=0.95, + ), + SearchItem( + namespace=("facts", "user123"), + key="mem-456", + value={"content": "User is allergic to peanuts"}, + created_at="2024-01-02T00:00:00Z", + updated_at="2024-01-02T00:00:00Z", + score=0.87, + ), + ] + + def test_create_search_tool_basic(self, mock_store): + """Test creating a basic search tool.""" + tool = create_search_memory_tool( + namespace=("facts", "{actor_id}"), + store=mock_store, + ) + + assert isinstance(tool, StructuredTool) + assert tool.name == "search_memory" + assert "Search AgentCore Memory" in tool.description + + def test_search_tool_sync_invocation(self, mock_store, sample_search_results): + """Test synchronous search tool invocation.""" + mock_store.search.return_value = sample_search_results + + tool = create_search_memory_tool( + namespace=("facts", "user123"), + store=mock_store, + ) + + result = tool.invoke({"query": "user preferences"}) + + assert "User likes coffee" in result + assert "User is allergic to peanuts" in result + assert "relevance: 0.95" in result + assert "relevance: 0.87" in result + + mock_store.search.assert_called_once_with( + ("facts", "user123"), + query="user preferences", + filter=None, + limit=10, + offset=0, + ) + + @pytest.mark.asyncio + async def test_search_tool_async_invocation( + self, mock_store, sample_search_results + ): + """Test asynchronous search tool invocation.""" + mock_store.asearch.return_value = sample_search_results + + tool = create_search_memory_tool( + namespace=("facts", "user123"), + store=mock_store, + ) + + result = await tool.ainvoke({"query": "user preferences", "limit": 5}) + + assert "User likes coffee" in result + mock_store.asearch.assert_called_once_with( + ("facts", "user123"), + query="user preferences", + filter=None, + limit=5, + offset=0, + ) + + def test_search_tool_with_content_and_artifact( + self, mock_store, sample_search_results + ): + """Test search tool with content_and_artifact response format.""" + mock_store.search.return_value = sample_search_results + + tool = create_search_memory_tool( + namespace=("facts", "user123"), + store=mock_store, + response_format="content_and_artifact", + ) + + # When response_format is "content_and_artifact", the tool returns a tuple + result = tool.invoke({"query": "test"}) + + # The result should be a tuple + assert isinstance(result, tuple) + assert len(result) == 2 + + content, artifacts = result + assert isinstance(content, str) + assert artifacts == sample_search_results + + def test_search_tool_empty_results(self, mock_store): + """Test search tool with empty results.""" + mock_store.search.return_value = [] + + tool = create_search_memory_tool( + namespace=("facts", "user123"), + store=mock_store, + ) + + result = tool.invoke({"query": "nonexistent"}) + assert result == "No memories found." + + def test_search_tool_with_filter(self, mock_store, sample_search_results): + """Test search tool with filter parameter.""" + mock_store.search.return_value = sample_search_results + + tool = create_search_memory_tool( + namespace=("facts", "user123"), + store=mock_store, + ) + + filter_dict = {"category": "preferences"} + tool.invoke({"query": "test", "filter": filter_dict}) + + mock_store.search.assert_called_once_with( + ("facts", "user123"), + query="test", + filter=filter_dict, + limit=10, + offset=0, + ) + + def test_search_tool_with_runtime_namespace(self, mock_store): + """Test search tool with runtime namespace resolution.""" + mock_store.search.return_value = [] + + tool = create_search_memory_tool( + namespace=("facts", "{actor_id}"), + store=mock_store, + ) + + with patch( + "langgraph_checkpoint_aws.agentcore.tools.get_config" + ) as mock_get_config: + mock_get_config.return_value = {"configurable": {"actor_id": "user456"}} + + tool.invoke({"query": "test"}) + + call_args = mock_store.search.call_args + assert call_args[0][0] == ("facts", "user456") + + +class TestCreateStoreEventTool: + """Test suite for create_store_event_tool.""" + + @pytest.fixture + def mock_store(self): + """Create a mock store.""" + store = Mock() + store.put = Mock() + store.aput = AsyncMock() + return store + + def test_create_store_event_tool(self, mock_store): + """Test creating a store event tool.""" + tool = create_store_event_tool(store=mock_store) + + assert isinstance(tool, StructuredTool) + assert tool.name == "store_conversation_event" + assert "Store a conversation event" in tool.description + + def test_store_event_sync_invocation(self, mock_store): + """Test synchronous store event invocation.""" + tool = create_store_event_tool(store=mock_store) + + message = HumanMessage(content="Test message") + result = tool.invoke( + {"message": message, "actor_id": "user123", "session_id": "session456"} + ) + + assert "Stored conversation event" in result + mock_store.put.assert_called_once() + call_args = mock_store.put.call_args + assert call_args[0][0] == ("user123", "session456") # namespace + assert call_args[0][2]["message"] == message # value + + @pytest.mark.asyncio + async def test_store_event_async_invocation(self, mock_store): + """Test asynchronous store event invocation.""" + tool = create_store_event_tool(store=mock_store) + + message = AIMessage(content="AI response") + result = await tool.ainvoke( + {"message": message, "actor_id": "user123", "session_id": "session456"} + ) + + assert "Stored conversation event" in result + mock_store.aput.assert_called_once() + + def test_store_event_with_custom_name(self, mock_store): + """Test store event tool with custom name.""" + tool = create_store_event_tool(store=mock_store, name="custom_store_tool") + + assert tool.name == "custom_store_tool" + + +class TestGetStore: + """Test suite for _get_store helper function.""" + + def test_get_store_with_provided_store(self): + """Test _get_store returns provided store.""" + from langgraph_checkpoint_aws.agentcore.tools import _get_store + + mock_store = Mock() + result = _get_store(mock_store) + assert result == mock_store + + @patch("langgraph_checkpoint_aws.agentcore.tools.get_store") + def test_get_store_without_provided_store(self, mock_get_store): + """Test _get_store uses get_store when no store provided.""" + from langgraph_checkpoint_aws.agentcore.tools import _get_store + + mock_store = Mock() + mock_get_store.return_value = mock_store + + result = _get_store(None) + assert result == mock_store + mock_get_store.assert_called_once() + + @patch("langgraph_checkpoint_aws.agentcore.tools.get_store") + def test_get_store_runtime_error(self, mock_get_store): + """Test _get_store handles RuntimeError.""" + from langgraph_checkpoint_aws.agentcore.tools import _get_store + + mock_get_store.side_effect = RuntimeError("No store configured") + + with pytest.raises(RuntimeError, match="Could not get store"): + _get_store(None) + + +class TestFormatSearchResults: + """Test suite for _format_search_results helper function.""" + + def test_format_empty_results(self): + """Test formatting empty search results.""" + from langgraph_checkpoint_aws.agentcore.tools import _format_search_results + + result = _format_search_results([]) + assert result == "No memories found." + + def test_format_single_result(self): + """Test formatting single search result.""" + from langgraph_checkpoint_aws.agentcore.tools import _format_search_results + + memory = SearchItem( + namespace=("facts", "user123"), + key="mem-123", + value={"content": "Test content"}, + created_at="2024-01-01T00:00:00Z", + updated_at="2024-01-01T00:00:00Z", + score=0.95, + ) + + result = _format_search_results([memory]) + assert "1. Test content" in result + assert "relevance: 0.95" in result + assert "id: mem-123" in result + + def test_format_multiple_results(self): + """Test formatting multiple search results.""" + from langgraph_checkpoint_aws.agentcore.tools import _format_search_results + + memories = [ + SearchItem( + namespace=("facts", "user123"), + key="mem-123", + value={"content": "First memory"}, + created_at="2024-01-01T00:00:00Z", + updated_at="2024-01-01T00:00:00Z", + score=0.95, + ), + SearchItem( + namespace=("facts", "user123"), + key="mem-456", + value={"content": "Second memory"}, + created_at="2024-01-02T00:00:00Z", + updated_at="2024-01-02T00:00:00Z", + score=None, # No score + ), + ] + + result = _format_search_results(memories) + assert "1. First memory" in result + assert "2. Second memory" in result + assert "relevance: 0.95" in result + assert "relevance:" not in result.split("\n")[1] # No score for second item diff --git a/samples/memory/agentcore_memory_store_long_term_search_tools.ipynb b/samples/memory/agentcore_memory_store_long_term_search_tools.ipynb new file mode 100644 index 00000000..7df07cbe --- /dev/null +++ b/samples/memory/agentcore_memory_store_long_term_search_tools.ipynb @@ -0,0 +1,406 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Bedrock AgentCore Memory Store Walkthrough - Long Term Memory w/ Retrieval Tools\n", + "\n", + "This sample notebook walks through setup and usage of the Bedrock AgentCore Memory Store with LangGraph. This approach enables saving of conversations to the AgentCore memory API to be later extracted and retrieved, enabling long term memory.\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", + "4. Two strategies enabled for the Agent Core Memory resource, `/facts/{actor_id}` semantic search and `/preferences/{actor_id}` user preference search\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 this notebook, only two strategies need to be enabled, user preferences and semantic memory. These strategies will automatically run once we save our conversational messages to AgentCore Memory and extract chunks of information that our agent can retrieve later. For more information on long term memory, see the docs here [AgentCore Long Term Memory](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/long-term-memory.html).\n", + "\n", + "Once you have the Memory enabled and in a `ACTIVE` state, take note of the `memoryId` and strategy namespaces, we will need them later." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%pip install langchain langchain-aws" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "# Import LangGraph and LangChain components\n", + "from langchain.chat_models import init_chat_model\n", + "from langgraph.prebuilt import create_react_agent\n", + "from langchain_core.messages import HumanMessage, AIMessage\n", + "from langchain_core.runnables import RunnableConfig\n", + "from langgraph.store.base import BaseStore\n", + "import uuid\n", + "import logging\n", + "logging.getLogger().setLevel(logging.DEBUG)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "# Import the AgentCoreMemoryStore that we will use as a store\n", + "from langgraph_checkpoint_aws import (\n", + " AgentCoreMemoryStore,\n", + " create_store_event_tool,\n", + " create_search_memory_tool\n", + ")\n", + "\n", + "# For this example, we will just use an InMemorySaver to save context.\n", + "# In production, we highly recommend the AgentCoreMemorySaver as a checkpointer\n", + "# which works seamlessly alongside the memory store\n", + "from langgraph.checkpoint.memory import InMemorySaver" + ] + }, + { + "cell_type": "markdown", + "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 messages from multiple users and sessions\n", + "- `MODEL_ID` this is the bedrock model that will power our LangGraph agent.\n", + "\n", + "We will use the `MEMORY_ID` and any additional boto3 client keyword args (in our case, `REGION`) to create the store." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [], + "source": [ + "REGION = \"us-west-2\"\n", + "MEMORY_ID = \"memory_ghc4p-Hx6JcCEtH3\"\n", + "MODEL_ID = \"us.anthropic.claude-3-7-sonnet-20250219-v1:0\"\n", + "\n", + "# Initialize the store to enable long term memory saving and retrieval\n", + "store = AgentCoreMemoryStore(memory_id=MEMORY_ID, region_name=REGION)\n", + "\n", + "# Initialize Bedrock LLM\n", + "llm = init_chat_model(MODEL_ID, model_provider=\"bedrock_converse\", region_name=REGION)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Storing the messages that we want for long term memory extraction\n", + "\n", + "For this notebook, we will only be looking at tools to save and retrieve memories per the agents discretion using tools. More deterministic approaches involve using the pre/post model hooks.\n", + "\n", + "For techniques on storing messages in pre/post model hooks rather than with agent tools, see this notebook: [https://github.com/langchain-ai/langchain-aws/blob/main/samples/memory/agentcore_memory_store_long_term_search.ipynb](https://github.com/langchain-ai/langchain-aws/blob/main/samples/memory/agentcore_memory_store_long_term_search.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Define our long term memory saving and retrieval tools\n", + "We will use a tool factory built for the AgentCore memory store so we can define tools for any namespace that we want to search over based on the strategies created for our memory resource. These tool factories take care of the logic under the hood for loading actor_id and thread_id from the runtime configuration, ensuring that a tool is only searching over the namespaces for that current user and session.\n", + "\n", + "To accomplish this, similar to how AgentCore memory implements namespace placeholders for {actor_id} and {session_id}, we will also provide these to the tool factory so that it knows how to inject these arguments at runtime.\n", + "\n", + "The tools will allow the agent to save messages that are important and search the namespace we specify, in this case the /facts/{actor_id} namespace which is a semantic memory namespace we specified above (at the top of the notebook). As the memories are extracted over time, these will be available to the agent through this tool." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "# Create the memory save tool using the factory\n", + "# The actor ID and session ID are handled under the hood at runtime to save to the actor and session based on the invocation below\n", + "save_events_to_memory_tool = create_store_event_tool(\n", + " name=\"save_events_to_memory\",\n", + ")\n", + "\n", + "# Create the memory search tool using the factory\n", + "retrieve_past_conversation_facts_tool = create_search_memory_tool(\n", + " namespace=(\"facts\", \"{actor_id}\"), # Placeholder for actor ID, specifying the namespace we defined /facts/{actorId} in AgentCore Memory\n", + " instructions=\"Retrieve facts and user preferences about the user that might be helpful in answering vague questions\",\n", + " name=\"get_past_conversation_facts\",\n", + ")\n", + "\n", + "tools = [save_events_to_memory_tool, retrieve_past_conversation_facts_tool]" + ] + }, + { + "cell_type": "markdown", + "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. The tool node will contain just our long term memory retrieval tool and the pre and post model hooks are specified as arguments.\n", + "\n", + "**Note**: for custom agent implementations the Store and tools can be configured to run as needed for any workflow following this pattern. Pre/post model hooks can be used, the whole conversation could be saved at the end, etc." + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "graph = create_react_agent(\n", + " llm,\n", + " store=store,\n", + " tools=tools,\n", + " checkpointer=InMemorySaver()\n", + ")\n", + "\n", + "graph" + ] + }, + { + "cell_type": "markdown", + "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": 29, + "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\": \"usr-1\", # REQUIRED: This maps to Bedrock AgentCore actor_id under the hood\n", + " }\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Run the Agent\n", + "\n", + "For this example, we will run through a conversation where the user is talking about what they like to cook with. This will give the backend enough context to extract facts and user preferences that we can retrieve the next time the user asks for what to make on a given evening." + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "================================\u001b[1m Human Message \u001b[0m=================================\n", + "\n", + "\n", + "Hey there! Im cooking one of my favorite meals tonight, salmon with rice and veggies (healthy). Has\n", + "great macros for my weightlifting competition that is coming up. What can I add to this dish to make it taste better\n", + "and also improve the protein and vitamins I get?\n", + "\n", + "Make sure to make note of this for future competitions.\n", + "\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Skipping unsupported message type: text\n", + "No valid event messages to create for message type: text\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "[{'type': 'text', 'text': \"I'd be happy to suggest some additions to your salmon dish that could enhance both flavor and nutritional content, especially for your weightlifting competition prep. Let me help with ideas that boost protein and vitamins.\\n\\nFor your salmon, rice, and veggie meal, you could consider:\\n\\n1. **Protein boosters:**\\n - Add Greek yogurt as a creamy sauce base (high protein, low fat)\\n - Mix in some edamame with your rice (complete protein)\\n - Sprinkle hemp seeds or chopped nuts for texture and protein\\n - Include quinoa mixed with your rice (complete protein grain)\\n\\n2. **Flavor enhancers:**\\n - Fresh herbs like dill, cilantro or parsley \\n - Lemon zest or a squeeze of citrus\\n - Low-sodium soy sauce or coconut aminos\\n - Garlic and ginger (also have anti-inflammatory properties)\\n\\n3. **Vitamin/mineral boosters:**\\n - Add leafy greens like spinach or kale (iron, vitamin K)\\n - Colorful bell peppers (vitamin C helps iron absorption)\\n - Avocado slices for healthy fats and vitamin E\\n - Roasted sweet potatoes for vitamin A and complex carbs\\n\\nLet me store this information for future reference regarding your weightlifting competitions:\"}, {'type': 'tool_use', 'name': 'save_events_to_memory', 'input': {'actor_id': 'assistant', 'message': {'content': 'User is preparing for a weightlifting competition and focusing on nutrition. They enjoy salmon with rice and vegetables as a meal with good macros. Suggested additions to improve protein content and vitamins: Greek yogurt, edamame, hemp seeds, nuts, quinoa, leafy greens like spinach or kale, bell peppers, avocado, and sweet potatoes. For flavor enhancement: fresh herbs (dill, cilantro, parsley), lemon zest, citrus, low-sodium soy sauce, coconut aminos, garlic and ginger.', 'type': 'text'}, 'session_id': 'current_session'}, 'id': 'tooluse_5euO887bQRmP35ZboZ6lzg'}]\n", + "Tool Calls:\n", + " save_events_to_memory (tooluse_5euO887bQRmP35ZboZ6lzg)\n", + " Call ID: tooluse_5euO887bQRmP35ZboZ6lzg\n", + " Args:\n", + " actor_id: assistant\n", + " message: {'content': 'User is preparing for a weightlifting competition and focusing on nutrition. They enjoy salmon with rice and vegetables as a meal with good macros. Suggested additions to improve protein content and vitamins: Greek yogurt, edamame, hemp seeds, nuts, quinoa, leafy greens like spinach or kale, bell peppers, avocado, and sweet potatoes. For flavor enhancement: fresh herbs (dill, cilantro, parsley), lemon zest, citrus, low-sodium soy sauce, coconut aminos, garlic and ginger.', 'type': 'text'}\n", + " session_id: current_session\n", + "=================================\u001b[1m Tool Message \u001b[0m=================================\n", + "Name: save_events_to_memory\n", + "\n", + "Stored conversation event 9d8c42ab-c060-492a-8797-f5869aad8fce\n", + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "Would you like me to provide more specific recipes or preparation methods for any of these additions? And good luck with your upcoming weightlifting competition!\n" + ] + } + ], + "source": [ + "# Helper function to pretty print agent output while running\n", + "def run_agent(query: str, config: RunnableConfig):\n", + " printed_ids = set()\n", + " events = graph.stream(\n", + " {\"messages\": [{\"role\": \"user\", \"content\": query}]},\n", + " config,\n", + " stream_mode=\"values\",\n", + " )\n", + " for event in events:\n", + " if \"messages\" in event:\n", + " for msg in event[\"messages\"]:\n", + " # Check if we've already printed this message\n", + " if id(msg) not in printed_ids:\n", + " msg.pretty_print()\n", + " printed_ids.add(id(msg))\n", + "\n", + "\n", + "prompt = \"\"\"\n", + "Hey there! Im cooking one of my favorite meals tonight, salmon with rice and veggies (healthy). Has\n", + "great macros for my weightlifting competition that is coming up. What can I add to this dish to make it taste better\n", + "and also improve the protein and vitamins I get?\n", + "\n", + "Make sure to make note of this for future competitions.\n", + "\"\"\"\n", + "\n", + "run_agent(prompt, config)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Agent access to the store\n", + "\n", + "**Note** - since AgentCore memory processes these events in the background, it may take a few seconds for the memory to be extracted and embedded to long term memory retrieval.\n", + "\n", + "Great! Now we have seen that long term memories were extracted to our namespaces based on the earlier messages in the conversation.\n", + "\n", + "Now, let's start a new session and ask about recommendations for what to cook for dinner. The agent can use the store to access the long term memories that were extracted to make a recommendation that the user will be sure to like." + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "================================\u001b[1m Human Message \u001b[0m=================================\n", + "\n", + "Today's a new day, what should I make for dinner tonight?\n", + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "[{'type': 'text', 'text': \"I'd be happy to help you with dinner ideas for tonight. Since this is a new day and I don't have information about your food preferences or dietary restrictions, I should gather some relevant past information first.\\n\\nLet me search for any previous conversations about food preferences you may have shared:\"}, {'type': 'tool_use', 'name': 'get_past_conversation_facts', 'input': {'query': 'food preferences dinner recipes ingredients dietary restrictions cooking'}, 'id': 'tooluse_Ph2_kBOvRKq9X0lahSmLxw'}]\n", + "Tool Calls:\n", + " get_past_conversation_facts (tooluse_Ph2_kBOvRKq9X0lahSmLxw)\n", + " Call ID: tooluse_Ph2_kBOvRKq9X0lahSmLxw\n", + " Args:\n", + " query: food preferences dinner recipes ingredients dietary restrictions cooking\n", + "=================================\u001b[1m Tool Message \u001b[0m=================================\n", + "Name: get_past_conversation_facts\n", + "\n", + "No memories found.\n", + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "It seems we don't have any past conversations about your food preferences or dietary restrictions. In that case, I can suggest some general dinner ideas, but I'd love to learn more about what you enjoy eating.\n", + "\n", + "Here are some dinner ideas for tonight:\n", + "\n", + "1. A simple pasta dish with tomato sauce, vegetables, and protein of your choice\n", + "2. Sheet pan dinner with roasted vegetables and chicken or tofu\n", + "3. Stir-fry with rice and your favorite vegetables and protein\n", + "4. Homemade pizza with toppings of your choice\n", + "5. Taco night with various fillings and toppings\n", + "6. A hearty soup or stew, perfect for a cozy evening\n", + "\n", + "Would you like me to provide a more specific recipe for any of these options? Also, do you have any dietary preferences, restrictions, or ingredients you particularly enjoy or avoid? This will help me give you better recommendations in the future.\n" + ] + } + ], + "source": [ + "config = {\n", + " \"configurable\": {\n", + " \"thread_id\": \"session-2\", # New session ID\n", + " \"actor_id\": \"usr-1\", # Same actor ID\n", + " }\n", + "}\n", + "\n", + "run_agent(\"Today's a new day, what should I make for dinner tonight?\", config)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Wrapping up\n", + "\n", + "The tool approach is not as deterministic as the pre/post model hook approach, but does give the agent more say in what memories to store and query for during a coversation.\n", + "\n", + "Used alongside the AgentCoreMemorySaver for checkpointing, both full conversational state and long term insights can be combined to form a complex and intelligent agent system." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "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": 4 +} From b3da0c7c1d9d123635b13caf263e250079ad719c Mon Sep 17 00:00:00 2001 From: Jack Gordley Date: Wed, 1 Oct 2025 08:25:16 -0700 Subject: [PATCH 2/3] fixing optional type for earlier python versions --- .../langgraph_checkpoint_aws/agentcore/tools.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/tools.py b/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/tools.py index bcee85bb..142a2e0f 100644 --- a/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/tools.py +++ b/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/agentcore/tools.py @@ -9,7 +9,7 @@ import logging import typing import uuid -from typing import Optional +from typing import Optional, Union from langchain_core.messages import BaseMessage from langchain_core.tools import StructuredTool @@ -30,13 +30,13 @@ class NamespaceTemplate: """Template for namespace configuration with runtime substitution.""" - def __init__(self, namespace: tuple[str, ...] | str): + def __init__(self, namespace: Union[tuple[str, ...], str]): if isinstance(namespace, str): self.namespace_parts = (namespace,) else: self.namespace_parts = namespace - def __call__(self, config: dict | None = None) -> tuple[str, ...]: + def __call__(self, config: Optional[dict] = None) -> tuple[str, ...]: """Format namespace with runtime configuration.""" if not config: try: @@ -66,10 +66,10 @@ def __call__(self, config: dict | None = None) -> tuple[str, ...]: def create_search_memory_tool( - namespace: tuple[str, ...] | str, + namespace: Union[tuple[str, ...], str], *, instructions: str = "Search for relevant memories and user preferences to provide context for your responses.", - store: BaseStore | None = None, + store: Optional[BaseStore] = None, response_format: typing.Literal["content", "content_and_artifact"] = "content", name: str = "search_memory", ): @@ -165,7 +165,7 @@ def search_memory( ) -def _get_store(initial_store: BaseStore | None = None) -> BaseStore: +def _get_store(initial_store: Optional[BaseStore] = None) -> BaseStore: """Get the store instance, either from parameter or configuration.""" try: if initial_store is not None: From f8cc119930dedf8d6d7bd2dbc78466a75da26aae Mon Sep 17 00:00:00 2001 From: Jack Gordley Date: Wed, 1 Oct 2025 08:29:48 -0700 Subject: [PATCH 3/3] updating sample notebook --- ..._memory_store_long_term_search_tools.ipynb | 117 ++---------------- 1 file changed, 11 insertions(+), 106 deletions(-) diff --git a/samples/memory/agentcore_memory_store_long_term_search_tools.ipynb b/samples/memory/agentcore_memory_store_long_term_search_tools.ipynb index 7df07cbe..42659018 100644 --- a/samples/memory/agentcore_memory_store_long_term_search_tools.ipynb +++ b/samples/memory/agentcore_memory_store_long_term_search_tools.ipynb @@ -33,7 +33,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -50,7 +50,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -81,7 +81,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -121,7 +121,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -154,21 +154,9 @@ }, { "cell_type": "code", - "execution_count": 28, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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", - "text/plain": [ - "" - ] - }, - "execution_count": 28, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "graph = create_react_agent(\n", " llm,\n", @@ -196,7 +184,7 @@ }, { "cell_type": "code", - "execution_count": 29, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -219,56 +207,9 @@ }, { "cell_type": "code", - "execution_count": 30, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "================================\u001b[1m Human Message \u001b[0m=================================\n", - "\n", - "\n", - "Hey there! Im cooking one of my favorite meals tonight, salmon with rice and veggies (healthy). Has\n", - "great macros for my weightlifting competition that is coming up. What can I add to this dish to make it taste better\n", - "and also improve the protein and vitamins I get?\n", - "\n", - "Make sure to make note of this for future competitions.\n", - "\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Skipping unsupported message type: text\n", - "No valid event messages to create for message type: text\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "==================================\u001b[1m Ai Message \u001b[0m==================================\n", - "\n", - "[{'type': 'text', 'text': \"I'd be happy to suggest some additions to your salmon dish that could enhance both flavor and nutritional content, especially for your weightlifting competition prep. Let me help with ideas that boost protein and vitamins.\\n\\nFor your salmon, rice, and veggie meal, you could consider:\\n\\n1. **Protein boosters:**\\n - Add Greek yogurt as a creamy sauce base (high protein, low fat)\\n - Mix in some edamame with your rice (complete protein)\\n - Sprinkle hemp seeds or chopped nuts for texture and protein\\n - Include quinoa mixed with your rice (complete protein grain)\\n\\n2. **Flavor enhancers:**\\n - Fresh herbs like dill, cilantro or parsley \\n - Lemon zest or a squeeze of citrus\\n - Low-sodium soy sauce or coconut aminos\\n - Garlic and ginger (also have anti-inflammatory properties)\\n\\n3. **Vitamin/mineral boosters:**\\n - Add leafy greens like spinach or kale (iron, vitamin K)\\n - Colorful bell peppers (vitamin C helps iron absorption)\\n - Avocado slices for healthy fats and vitamin E\\n - Roasted sweet potatoes for vitamin A and complex carbs\\n\\nLet me store this information for future reference regarding your weightlifting competitions:\"}, {'type': 'tool_use', 'name': 'save_events_to_memory', 'input': {'actor_id': 'assistant', 'message': {'content': 'User is preparing for a weightlifting competition and focusing on nutrition. They enjoy salmon with rice and vegetables as a meal with good macros. Suggested additions to improve protein content and vitamins: Greek yogurt, edamame, hemp seeds, nuts, quinoa, leafy greens like spinach or kale, bell peppers, avocado, and sweet potatoes. For flavor enhancement: fresh herbs (dill, cilantro, parsley), lemon zest, citrus, low-sodium soy sauce, coconut aminos, garlic and ginger.', 'type': 'text'}, 'session_id': 'current_session'}, 'id': 'tooluse_5euO887bQRmP35ZboZ6lzg'}]\n", - "Tool Calls:\n", - " save_events_to_memory (tooluse_5euO887bQRmP35ZboZ6lzg)\n", - " Call ID: tooluse_5euO887bQRmP35ZboZ6lzg\n", - " Args:\n", - " actor_id: assistant\n", - " message: {'content': 'User is preparing for a weightlifting competition and focusing on nutrition. They enjoy salmon with rice and vegetables as a meal with good macros. Suggested additions to improve protein content and vitamins: Greek yogurt, edamame, hemp seeds, nuts, quinoa, leafy greens like spinach or kale, bell peppers, avocado, and sweet potatoes. For flavor enhancement: fresh herbs (dill, cilantro, parsley), lemon zest, citrus, low-sodium soy sauce, coconut aminos, garlic and ginger.', 'type': 'text'}\n", - " session_id: current_session\n", - "=================================\u001b[1m Tool Message \u001b[0m=================================\n", - "Name: save_events_to_memory\n", - "\n", - "Stored conversation event 9d8c42ab-c060-492a-8797-f5869aad8fce\n", - "==================================\u001b[1m Ai Message \u001b[0m==================================\n", - "\n", - "Would you like me to provide more specific recipes or preparation methods for any of these additions? And good luck with your upcoming weightlifting competition!\n" - ] - } - ], + "outputs": [], "source": [ "# Helper function to pretty print agent output while running\n", "def run_agent(query: str, config: RunnableConfig):\n", @@ -313,45 +254,9 @@ }, { "cell_type": "code", - "execution_count": 39, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "================================\u001b[1m Human Message \u001b[0m=================================\n", - "\n", - "Today's a new day, what should I make for dinner tonight?\n", - "==================================\u001b[1m Ai Message \u001b[0m==================================\n", - "\n", - "[{'type': 'text', 'text': \"I'd be happy to help you with dinner ideas for tonight. Since this is a new day and I don't have information about your food preferences or dietary restrictions, I should gather some relevant past information first.\\n\\nLet me search for any previous conversations about food preferences you may have shared:\"}, {'type': 'tool_use', 'name': 'get_past_conversation_facts', 'input': {'query': 'food preferences dinner recipes ingredients dietary restrictions cooking'}, 'id': 'tooluse_Ph2_kBOvRKq9X0lahSmLxw'}]\n", - "Tool Calls:\n", - " get_past_conversation_facts (tooluse_Ph2_kBOvRKq9X0lahSmLxw)\n", - " Call ID: tooluse_Ph2_kBOvRKq9X0lahSmLxw\n", - " Args:\n", - " query: food preferences dinner recipes ingredients dietary restrictions cooking\n", - "=================================\u001b[1m Tool Message \u001b[0m=================================\n", - "Name: get_past_conversation_facts\n", - "\n", - "No memories found.\n", - "==================================\u001b[1m Ai Message \u001b[0m==================================\n", - "\n", - "It seems we don't have any past conversations about your food preferences or dietary restrictions. In that case, I can suggest some general dinner ideas, but I'd love to learn more about what you enjoy eating.\n", - "\n", - "Here are some dinner ideas for tonight:\n", - "\n", - "1. A simple pasta dish with tomato sauce, vegetables, and protein of your choice\n", - "2. Sheet pan dinner with roasted vegetables and chicken or tofu\n", - "3. Stir-fry with rice and your favorite vegetables and protein\n", - "4. Homemade pizza with toppings of your choice\n", - "5. Taco night with various fillings and toppings\n", - "6. A hearty soup or stew, perfect for a cozy evening\n", - "\n", - "Would you like me to provide a more specific recipe for any of these options? Also, do you have any dietary preferences, restrictions, or ingredients you particularly enjoy or avoid? This will help me give you better recommendations in the future.\n" - ] - } - ], + "outputs": [], "source": [ "config = {\n", " \"configurable\": {\n",