From 024a14ad8dbf850d3e7f5691ff3178dc22f73c52 Mon Sep 17 00:00:00 2001 From: DK09876 Date: Tue, 19 May 2026 10:54:15 -0700 Subject: [PATCH 1/2] feat: add BaseStore pattern to LangGraph cookbook notebook Extends 06-langgraph-react-agent.ipynb with Pattern 3 demonstrating HindsightStore as a LangGraph BaseStore: aput, semantic asearch, and store-backed graph with dependency injection. Co-Authored-By: Claude Opus 4.6 --- notebooks/06-langgraph-react-agent.ipynb | 148 ++++++++++++++++++++++- 1 file changed, 147 insertions(+), 1 deletion(-) diff --git a/notebooks/06-langgraph-react-agent.ipynb b/notebooks/06-langgraph-react-agent.ipynb index 7f050b4..bf34678 100644 --- a/notebooks/06-langgraph-react-agent.ipynb +++ b/notebooks/06-langgraph-react-agent.ipynb @@ -8,9 +8,10 @@ "\n", "Build a ReAct agent with LangGraph that remembers user preferences and past interactions across conversations using Hindsight memory.\n", "\n", - "This notebook demonstrates two patterns:\n", + "This notebook demonstrates three patterns:\n", "- **Tools pattern**: The agent decides when to store/retrieve memories\n", "- **Nodes pattern**: Memory injection and storage happen automatically as graph steps\n", + "- **BaseStore pattern**: Use Hindsight as a LangGraph `BaseStore` for cross-thread persistent memory with semantic search\n", "\n", "## Prerequisites\n", "\n", @@ -378,6 +379,149 @@ "print(result[\"messages\"][-1].content)" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Pattern 3: BaseStore — Drop-in Persistent Memory\n", + "\n", + "Use Hindsight as a LangGraph [`BaseStore`](https://langchain-ai.github.io/langgraph/reference/store/#langgraph.store.base.BaseStore), giving your graph cross-thread persistent memory with semantic search. This is useful when you want LangGraph's built-in `store` parameter to automatically persist data across conversations.\n", + "\n", + "Key characteristics:\n", + "- **Namespace tuples become Hindsight bank IDs** — `(\"user\", \"123\", \"prefs\")` maps to bank `user.123.prefs`\n", + "- **`aput()` stores via retain** with the key as `document_id` for upsert semantics\n", + "- **`asearch()` uses recall** for semantic search — not just exact key lookup\n", + "- **`adelete()` is a no-op** — Hindsight's memory model is append-oriented; fact superseding is automatic" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Create the Store and Graph" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from hindsight_langgraph import HindsightStore\n", + "from langgraph.checkpoint.memory import MemorySaver\n", + "from langgraph.graph import StateGraph, MessagesState, START, END\n", + "from langchain_core.messages import HumanMessage\n", + "from langgraph.store.base import BaseStore\n", + "\n", + "\n", + "store = HindsightStore(client=client)\n", + "checkpointer = MemorySaver()\n", + "\n", + "\n", + "async def store_aware_agent(state: MessagesState, *, store: BaseStore):\n", + " \"\"\"An agent node that reads and writes memories via the store.\"\"\"\n", + " # Search for relevant memories\n", + " namespace = (\"user\", \"charlie\", \"prefs\")\n", + " query = state[\"messages\"][-1].content if state[\"messages\"] else \"\"\n", + " memories = await store.asearch(namespace, query=query, limit=5)\n", + "\n", + " memory_text = \"\"\n", + " if memories:\n", + " lines = [f\"- {item.key}: {item.value}\" for item in memories]\n", + " memory_text = \"Known preferences:\\n\" + \"\\n\".join(lines) + \"\\n\\n\"\n", + "\n", + " model = ChatOpenAI(model=\"gpt-4o-mini\")\n", + " system = f\"You are a helpful assistant.\\n\\n{memory_text}\" if memory_text else \"You are a helpful assistant.\"\n", + " response = await model.ainvoke(\n", + " [{\"role\": \"system\", \"content\": system}] + state[\"messages\"]\n", + " )\n", + " return {\"messages\": [response]}\n", + "\n", + "\n", + "builder = StateGraph(MessagesState)\n", + "builder.add_node(\"agent\", store_aware_agent)\n", + "builder.add_edge(START, \"agent\")\n", + "builder.add_edge(\"agent\", END)\n", + "\n", + "store_graph = builder.compile(checkpointer=checkpointer, store=store)\n", + "print(\"Store-backed graph compiled.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Store Preferences via the BaseStore API\n", + "\n", + "Use `aput()` to store key-value pairs. Each key becomes a Hindsight `document_id`, so putting the same key again updates the memory rather than duplicating it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "namespace = (\"user\", \"charlie\", \"prefs\")\n", + "\n", + "await store.aput(namespace, \"theme\", {\"value\": \"dark mode\", \"category\": \"display\"})\n", + "await store.aput(namespace, \"language\", {\"value\": \"Python\", \"category\": \"dev\"})\n", + "await store.aput(namespace, \"editor\", {\"value\": \"Neovim\", \"category\": \"dev\"})\n", + "\n", + "print(\"Stored 3 preferences for charlie.\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "await asyncio.sleep(3)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Semantic Search\n", + "\n", + "Unlike a regular key-value store, `asearch()` uses Hindsight's recall engine for semantic matching — you don't need the exact key, just a related query." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "results = await store.asearch(namespace, query=\"what tools does the user code with?\")\n", + "for item in results:\n", + " print(f\" {item.key}: {item.value}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Use the Store-Backed Graph\n", + "\n", + "The graph's agent node receives the `store` automatically via LangGraph's dependency injection. It searches for Charlie's preferences and includes them in the system prompt." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "result = await store_graph.ainvoke(\n", + " {\"messages\": [HumanMessage(content=\"What editor and language do I use?\")]},\n", + " config={\"configurable\": {\"thread_id\": \"charlie-1\"}},\n", + ")\n", + "print(result[\"messages\"][-1].content)" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -393,6 +537,7 @@ "source": [ "await client.adelete_bank(\"user-alice\")\n", "await client.adelete_bank(\"user-bob\")\n", + "await client.adelete_bank(\"user.charlie.prefs\")\n", "print(\"Banks deleted.\")" ] }, @@ -404,6 +549,7 @@ "\n", "- **Tools pattern**: The agent decides when to store/retrieve. Best for complex reasoning flows.\n", "- **Nodes pattern**: Memory happens automatically. Best when you always want context injection.\n", + "- **BaseStore pattern**: Drop-in replacement for LangGraph's built-in store. Best for cross-thread memory with semantic search.\n", "- **Dynamic banks**: Use `bank_id_from_config` or parameterized `bank_id` for per-user isolation.\n", "- **Tags**: Scope memories by source, conversation, or topic for precise recall.\n", "\n", From 09d2992c4cdad238d9b9adff042ddcafaf00d0f0 Mon Sep 17 00:00:00 2001 From: DK09876 Date: Wed, 20 May 2026 08:46:01 -0700 Subject: [PATCH 2/2] LangGraph notebook: replace BaseStore with memory_instructions pattern - Replace Pattern 3 (BaseStore) with Memory Instructions pattern - Add "Which pattern should I use?" decision table - Update intro, cleanup, and takeaways Co-Authored-By: Claude Opus 4.6 --- notebooks/06-langgraph-react-agent.ipynb | 148 +++++++---------------- 1 file changed, 43 insertions(+), 105 deletions(-) diff --git a/notebooks/06-langgraph-react-agent.ipynb b/notebooks/06-langgraph-react-agent.ipynb index bf34678..36194f1 100644 --- a/notebooks/06-langgraph-react-agent.ipynb +++ b/notebooks/06-langgraph-react-agent.ipynb @@ -11,7 +11,15 @@ "This notebook demonstrates three patterns:\n", "- **Tools pattern**: The agent decides when to store/retrieve memories\n", "- **Nodes pattern**: Memory injection and storage happen automatically as graph steps\n", - "- **BaseStore pattern**: Use Hindsight as a LangGraph `BaseStore` for cross-thread persistent memory with semantic search\n", + "- **Memory Instructions pattern**: Pre-fetch memories into a system prompt for standalone LangChain use (no graph needed)\n", + "\n", + "## Which pattern should I use?\n", + "\n", + "| Pattern | Best for | Requires LangGraph? |\n", + "|---------|----------|---------------------|\n", + "| **Tools** | ReAct agents that reason about when memory is relevant | No (works with any LangChain ChatModel) |\n", + "| **Nodes** | Graphs where you always want memory context, no LLM decision needed | Yes |\n", + "| **Memory Instructions** | Simple chains, standalone LangChain, or any context where you want a memory-enriched system prompt | No |\n", "\n", "## Prerequisites\n", "\n", @@ -383,22 +391,18 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Pattern 3: BaseStore — Drop-in Persistent Memory\n", + "## Pattern 3: Memory Instructions — Standalone LangChain\n", "\n", - "Use Hindsight as a LangGraph [`BaseStore`](https://langchain-ai.github.io/langgraph/reference/store/#langgraph.store.base.BaseStore), giving your graph cross-thread persistent memory with semantic search. This is useful when you want LangGraph's built-in `store` parameter to automatically persist data across conversations.\n", + "Use `memory_instructions()` to pre-fetch memories and inject them into a system prompt. This works with any LangChain model — no LangGraph graph needed.\n", "\n", - "Key characteristics:\n", - "- **Namespace tuples become Hindsight bank IDs** — `(\"user\", \"123\", \"prefs\")` maps to bank `user.123.prefs`\n", - "- **`aput()` stores via retain** with the key as `document_id` for upsert semantics\n", - "- **`asearch()` uses recall** for semantic search — not just exact key lookup\n", - "- **`adelete()` is a no-op** — Hindsight's memory model is append-oriented; fact superseding is automatic" + "Best when you want memory context in a simple chain or don't need the agent to decide when to use memory." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### Create the Store and Graph" + "### Create the Instructions Function" ] }, { @@ -407,53 +411,35 @@ "metadata": {}, "outputs": [], "source": [ - "from hindsight_langgraph import HindsightStore\n", - "from langgraph.checkpoint.memory import MemorySaver\n", - "from langgraph.graph import StateGraph, MessagesState, START, END\n", - "from langchain_core.messages import HumanMessage\n", - "from langgraph.store.base import BaseStore\n", - "\n", - "\n", - "store = HindsightStore(client=client)\n", - "checkpointer = MemorySaver()\n", - "\n", - "\n", - "async def store_aware_agent(state: MessagesState, *, store: BaseStore):\n", - " \"\"\"An agent node that reads and writes memories via the store.\"\"\"\n", - " # Search for relevant memories\n", - " namespace = (\"user\", \"charlie\", \"prefs\")\n", - " query = state[\"messages\"][-1].content if state[\"messages\"] else \"\"\n", - " memories = await store.asearch(namespace, query=query, limit=5)\n", + "from hindsight_langgraph import memory_instructions\n", "\n", - " memory_text = \"\"\n", - " if memories:\n", - " lines = [f\"- {item.key}: {item.value}\" for item in memories]\n", - " memory_text = \"Known preferences:\\n\" + \"\\n\".join(lines) + \"\\n\\n\"\n", - "\n", - " model = ChatOpenAI(model=\"gpt-4o-mini\")\n", - " system = f\"You are a helpful assistant.\\n\\n{memory_text}\" if memory_text else \"You are a helpful assistant.\"\n", - " response = await model.ainvoke(\n", - " [{\"role\": \"system\", \"content\": system}] + state[\"messages\"]\n", - " )\n", - " return {\"messages\": [response]}\n", - "\n", - "\n", - "builder = StateGraph(MessagesState)\n", - "builder.add_node(\"agent\", store_aware_agent)\n", - "builder.add_edge(START, \"agent\")\n", - "builder.add_edge(\"agent\", END)\n", + "# memory_instructions() returns an async callable that recalls memories\n", + "# and appends them to your base instructions. If recall fails or returns\n", + "# nothing, it gracefully falls back to base_instructions alone.\n", + "get_instructions = memory_instructions(\n", + " client=client,\n", + " bank_id=\"user-alice\", # Reuse Alice's bank from Pattern 1\n", + " base_instructions=(\n", + " \"You are a helpful assistant with long-term memory. \"\n", + " \"Use what you know about the user to personalize your responses.\"\n", + " ),\n", + " budget=\"mid\",\n", + " max_results=5,\n", + ")\n", "\n", - "store_graph = builder.compile(checkpointer=checkpointer, store=store)\n", - "print(\"Store-backed graph compiled.\")" + "# Fetch instructions with memories injected\n", + "instructions = await get_instructions()\n", + "print(\"Instructions with memories:\")\n", + "print(instructions)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### Store Preferences via the BaseStore API\n", + "### Use in a Simple LangChain Call\n", "\n", - "Use `aput()` to store key-value pairs. Each key becomes a Hindsight `document_id`, so putting the same key again updates the memory rather than duplicating it." + "No graph, no tools — just a model call with memory-enriched instructions." ] }, { @@ -462,64 +448,17 @@ "metadata": {}, "outputs": [], "source": [ - "namespace = (\"user\", \"charlie\", \"prefs\")\n", - "\n", - "await store.aput(namespace, \"theme\", {\"value\": \"dark mode\", \"category\": \"display\"})\n", - "await store.aput(namespace, \"language\", {\"value\": \"Python\", \"category\": \"dev\"})\n", - "await store.aput(namespace, \"editor\", {\"value\": \"Neovim\", \"category\": \"dev\"})\n", - "\n", - "print(\"Stored 3 preferences for charlie.\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "await asyncio.sleep(3)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Semantic Search\n", + "from langchain_openai import ChatOpenAI\n", "\n", - "Unlike a regular key-value store, `asearch()` uses Hindsight's recall engine for semantic matching — you don't need the exact key, just a related query." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "results = await store.asearch(namespace, query=\"what tools does the user code with?\")\n", - "for item in results:\n", - " print(f\" {item.key}: {item.value}\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Use the Store-Backed Graph\n", + "model = ChatOpenAI(model=\"gpt-4o-mini\")\n", "\n", - "The graph's agent node receives the `store` automatically via LangGraph's dependency injection. It searches for Charlie's preferences and includes them in the system prompt." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "result = await store_graph.ainvoke(\n", - " {\"messages\": [HumanMessage(content=\"What editor and language do I use?\")]},\n", - " config={\"configurable\": {\"thread_id\": \"charlie-1\"}},\n", - ")\n", - "print(result[\"messages\"][-1].content)" + "# Each call to get_instructions() re-fetches memories, so it stays up to date\n", + "instructions = await get_instructions()\n", + "response = await model.ainvoke([\n", + " {\"role\": \"system\", \"content\": instructions},\n", + " {\"role\": \"user\", \"content\": \"What programming language and IDE do I use?\"},\n", + "])\n", + "print(response.content)" ] }, { @@ -537,7 +476,6 @@ "source": [ "await client.adelete_bank(\"user-alice\")\n", "await client.adelete_bank(\"user-bob\")\n", - "await client.adelete_bank(\"user.charlie.prefs\")\n", "print(\"Banks deleted.\")" ] }, @@ -549,7 +487,7 @@ "\n", "- **Tools pattern**: The agent decides when to store/retrieve. Best for complex reasoning flows.\n", "- **Nodes pattern**: Memory happens automatically. Best when you always want context injection.\n", - "- **BaseStore pattern**: Drop-in replacement for LangGraph's built-in store. Best for cross-thread memory with semantic search.\n", + "- **Memory Instructions**: Pre-fetches memories into a system prompt. Best for standalone LangChain or simple chains.\n", "- **Dynamic banks**: Use `bank_id_from_config` or parameterized `bank_id` for per-user isolation.\n", "- **Tags**: Scope memories by source, conversation, or topic for precise recall.\n", "\n",