feat: improve long-term memory docs and discoverability (#2957)

This commit is contained in:
Naomi Pentrel
2026-03-18 16:28:59 +01:00
committed by GitHub
parent bda9ab2314
commit fb79c64ed1
35 changed files with 1577 additions and 303 deletions
+37
View File
@@ -8,6 +8,7 @@ on:
pull_request:
paths:
- "src/code-samples/**"
- ".github/workflows/test-code-samples.yml"
schedule:
# Run every Sunday at 00:00 UTC
- cron: "0 0 * * 0"
@@ -20,6 +21,19 @@ jobs:
test-code-samples:
runs-on: ubuntu-latest
timeout-minutes: 15
services:
postgres:
image: pgvector/pgvector:pg17
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Checkout
uses: actions/checkout@v6
@@ -67,10 +81,33 @@ jobs:
with:
node-version: "20"
- name: Wait for PostgreSQL
run: |
for i in $(seq 1 30); do
if python3 -c "
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(2)
try:
s.connect(('127.0.0.1', 5432))
s.close()
except OSError:
exit(1)
"; then
echo "PostgreSQL is ready"
exit 0
fi
echo "Waiting for PostgreSQL... ($i/30)"
sleep 2
done
echo "PostgreSQL did not become ready"
exit 1
- name: Test code samples
id: test
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
POSTGRES_URI: postgresql://postgres:postgres@127.0.0.1:5432/postgres?sslmode=disable
run: |
if [[ "${{ steps.files.outputs.run_all }}" == "true" ]]; then
echo "Running all code samples..."
+2
View File
@@ -12,6 +12,8 @@ dependencies = [
"nbconvert>=7.16.6",
"langchain>=1.0.1",
"langchain-anthropic>=1.0.0",
"langgraph-checkpoint-postgres",
"psycopg[binary,pool]>=3.2.0",
]
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env python3
"""Filter mint broken-links output to only include blocks for specified files.
Reads from stdin, writes to stdout.
Usage: mint broken-links 2>&1 | python filter_broken_links_by_file.py [pattern1 pattern2 ...]
Patterns match if the build path (e.g. langsmith/admin.mdx) contains the pattern.
"""
import sys
def main() -> None:
patterns = sys.argv[1:]
lines = sys.stdin.read().split("\n")
out: list[str] = []
current_file: str | None = None
for line in lines:
if not line or line[0].isspace():
# Indented line or blank: belongs to current block
if patterns and current_file and any(p in current_file for p in patterns):
out.append(line)
else:
# File header
current_file = line.strip()
if not patterns or any(p in current_file for p in patterns):
out.append(line)
sys.stdout.write("\n".join(out))
if __name__ == "__main__":
main()
+4
View File
@@ -99,6 +99,8 @@ def main() -> int:
success = False
try:
# Pass full env so POSTGRES_URI, ANTHROPIC_API_KEY etc. reach child processes
env = os.environ.copy()
if lang == "python":
result = subprocess.run(
["uv", "run", "python", str(file_path)],
@@ -107,6 +109,7 @@ def main() -> int:
capture_output=True,
text=True,
timeout=TIMEOUT_SECONDS,
env=env,
)
success = result.returncode == 0
stdout = result.stdout or ""
@@ -120,6 +123,7 @@ def main() -> int:
capture_output=True,
text=True,
timeout=TIMEOUT_SECONDS,
env=env,
)
success = result.returncode == 0
stdout = result.stdout or ""
+128
View File
@@ -0,0 +1,128 @@
"""PostgreSQL setup for code samples.
This module provides PostgreSQL connection setup for code samples.
It attempts to use testcontainers if available, otherwise provides
utilities to work with environment-configured postgres.
"""
import os
import subprocess
import sys
import time
_DEFAULT_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable"
def get_postgres_uri() -> str:
"""Get PostgreSQL connection URI.
Tries multiple approaches in order:
1. Check POSTGRES_URI environment variable
2. Attempt to use testcontainers to spin up postgres
3. Try docker directly
4. Fall back to default local postgres connection
"""
# Check environment variable first
if env_uri := os.environ.get("POSTGRES_URI"):
return env_uri
# Try testcontainers
try:
from testcontainers.postgres import ( # type: ignore[import-not-found]
PostgresContainer,
)
# Store container in a global so it persists
if not hasattr(get_postgres_uri, "_container"):
# Use pgvector image which includes the vector extension
container = PostgresContainer("pgvector/pgvector:pg17")
container.start()
get_postgres_uri._container = container # type: ignore[attr-defined]
# Give it a moment to fully start
time.sleep(2)
return get_postgres_uri._container.get_connection_url() # type: ignore[attr-defined]
except ImportError:
print("conftest: testcontainers not installed, trying docker", file=sys.stderr)
# Try to use docker directly if testcontainers not available
try:
# Check if postgres container is already running
result = subprocess.run(
[
"docker",
"ps",
"--filter",
"name=langchain-docs-postgres",
"--format",
"{{.Names}}",
],
capture_output=True,
text=True,
check=True,
timeout=5,
)
if "langchain-docs-postgres" not in result.stdout:
# Start a postgres container with pgvector extension
subprocess.run(
[
"docker",
"run",
"-d",
"--name",
"langchain-docs-postgres",
"-e",
"POSTGRES_PASSWORD=postgres",
"-e",
"POSTGRES_DB=postgres",
"-p",
"5442:5432",
"pgvector/pgvector:pg17",
],
check=True,
capture_output=True,
timeout=30,
)
# Give it time to start
time.sleep(3)
return _DEFAULT_URI
except FileNotFoundError:
print("conftest: docker not found, using default URI", file=sys.stderr)
except subprocess.TimeoutExpired as e:
print(
f"conftest: docker timed out after {e.timeout}s, using default URI",
file=sys.stderr,
)
except subprocess.CalledProcessError as e:
print(
f"conftest: docker failed (exit {e.returncode}), using default URI",
file=sys.stderr,
)
# Fall back to default (assumes postgres is running locally)
print(f"conftest: falling back to default URI: {_DEFAULT_URI}", file=sys.stderr)
return _DEFAULT_URI
def prepare_postgres_store(uri: str) -> None:
"""Drop existing store tables so setup() creates a fresh schema.
Use before PostgresStore.from_conn_string when tests share a database
(e.g. CI) and may see leftover tables from a different schema version.
"""
import psycopg
try:
with psycopg.connect(uri, autocommit=True) as conn:
with conn.cursor() as cur:
cur.execute(
"DROP TABLE IF EXISTS public.store_vectors CASCADE; "
"DROP TABLE IF EXISTS public.store CASCADE; "
"DROP TABLE IF EXISTS public.store_migrations CASCADE;"
)
except psycopg.OperationalError as e:
print(f"conftest: could not connect to clean tables: {e}", file=sys.stderr)
except psycopg.Error as e:
print(f"conftest: failed to drop store tables: {e}", file=sys.stderr)
@@ -0,0 +1,21 @@
# :snippet-start: long-term-memory-create-agent-inmemory-py
from langchain.agents import create_agent
from langchain_core.runnables import Runnable
from langgraph.store.memory import InMemoryStore
# InMemoryStore saves data to an in-memory dictionary. Use a DB-backed store in production use.
store = InMemoryStore()
agent: Runnable = create_agent(
"claude-sonnet-4-6",
tools=[],
store=store,
)
# :snippet-end:
# :remove-start:
if __name__ == "__main__":
# Verify the agent was created successfully
assert agent is not None
print("✓ Agent with InMemoryStore created successfully")
# :remove-end:
@@ -0,0 +1,23 @@
// :snippet-start: long-term-memory-create-agent-inmemory-js
import { createAgent } from "langchain";
import { InMemoryStore } from "@langchain/langgraph";
// InMemoryStore saves data to an in-memory dictionary. Use a DB-backed store in production use.
const store = new InMemoryStore();
const agent = createAgent({
model: "claude-sonnet-4-6",
tools: [],
store,
});
// :snippet-end:
// :remove-start:
if (import.meta.url === `file://${process.argv[1]}`) {
// Verify the agent was created successfully
if (!agent) {
throw new Error("Agent creation failed");
}
console.log("✓ Agent with InMemoryStore created successfully");
}
// :remove-end:
@@ -0,0 +1,30 @@
# :snippet-start: long-term-memory-create-agent-postgres-py
from langchain.agents import create_agent
from langchain_core.runnables import Runnable
from langgraph.store.postgres import PostgresStore # type: ignore[import-not-found]
DB_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable"
# :remove-start:
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from conftest import get_postgres_uri, prepare_postgres_store
DB_URI = get_postgres_uri()
prepare_postgres_store(DB_URI)
# :remove-end:
with PostgresStore.from_conn_string(DB_URI) as store:
store.setup()
agent: Runnable = create_agent(
"claude-sonnet-4-6",
tools=[],
store=store,
)
# :snippet-end:
# :remove-start:
assert agent is not None
print("✓ Agent with PostgresStore created successfully")
# :remove-end:
@@ -0,0 +1,24 @@
// :snippet-start: long-term-memory-create-agent-postgres-js
import { createAgent } from "langchain";
import { PostgresStore } from "@langchain/langgraph-checkpoint-postgres/store";
const DB_URI =
process.env.POSTGRES_URI ??
"postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable";
const store = PostgresStore.fromConnString(DB_URI);
await store.setup();
const agent = createAgent({
model: "claude-sonnet-4-6",
tools: [],
store,
});
// :snippet-end:
// :remove-start:
if (!agent) {
throw new Error("Agent creation failed");
}
console.log("✓ Agent with PostgresStore created successfully");
await store.stop();
// :remove-end:
@@ -0,0 +1,65 @@
# :snippet-start: long-term-memory-read-tool-inmemory-py
from dataclasses import dataclass
from langchain.agents import create_agent
from langchain.tools import ToolRuntime, tool
from langchain_core.runnables import Runnable
from langgraph.store.memory import InMemoryStore
@dataclass
class Context:
user_id: str
# InMemoryStore saves data to an in-memory dictionary. Use a DB-backed store in production.
store = InMemoryStore()
# Write sample data to the store using the put method
store.put(
(
"users",
), # Namespace to group related data together (users namespace for user data)
"user_123", # Key within the namespace (user ID as key)
{
"name": "John Smith",
"language": "English",
}, # Data to store for the given user
)
@tool
def get_user_info(runtime: ToolRuntime[Context]) -> str:
"""Look up user info."""
# Access the store - same as that provided to `create_agent`
assert runtime.store is not None
user_id = runtime.context.user_id
# Retrieve data from store - returns StoreValue object with value and metadata
user_info = runtime.store.get(("users",), user_id)
return str(user_info.value) if user_info else "Unknown user"
agent: Runnable = create_agent(
model="claude-sonnet-4-6",
tools=[get_user_info],
# Pass store to agent - enables agent to access store when running tools
store=store,
context_schema=Context,
)
# Run the agent
agent.invoke(
{"messages": [{"role": "user", "content": "look up user information"}]},
context=Context(user_id="user_123"),
)
# :snippet-end:
# :remove-start:
if __name__ == "__main__":
# Verify the store has the data
result = store.get(("users",), "user_123")
assert result is not None
assert result.value["name"] == "John Smith"
print("✓ Read tool with InMemoryStore works correctly")
# :remove-end:
@@ -0,0 +1,85 @@
// :snippet-start: long-term-memory-read-tool-inmemory-js
import * as z from "zod";
import { createAgent, tool, type ToolRuntime } from "langchain";
import { InMemoryStore } from "@langchain/langgraph";
// InMemoryStore saves data to an in-memory dictionary. Use a DB-backed store in production.
const store = new InMemoryStore();
const contextSchema = z.object({
userId: z.string(),
});
// Write sample data to the store using the put method
await store.put(
["users"], // Namespace to group related data together (users namespace for user data)
"user_123", // Key within the namespace (user ID as key)
{
name: "John Smith",
language: "English",
}, // Data to store for the given user
);
const getUserInfo = tool(
// Look up user info.
async (_, runtime: ToolRuntime<unknown, z.infer<typeof contextSchema>>) => {
// Access the store - same as that provided to `createAgent`
const userId = runtime.context.userId;
if (!userId) {
throw new Error("userId is required");
}
// Retrieve data from store - returns StoreValue object with value and metadata
const userInfo = await runtime.store.get(["users"], userId);
return userInfo?.value ? JSON.stringify(userInfo.value) : "Unknown user";
},
{
name: "getUserInfo",
description: "Look up user info by userId from the store.",
schema: z.object({}),
},
);
const agent = createAgent({
model: "claude-sonnet-4-6",
tools: [getUserInfo],
contextSchema,
// Pass store to agent - enables agent to access store when running tools
store,
});
// Run the agent
const result = await agent.invoke(
{ messages: [{ role: "user", content: "look up user information" }] },
{ context: { userId: "user_123" } },
);
console.log(result.messages.at(-1)?.content);
/**
* Outputs:
* User Information:
* - **Name:** John Smith
* - **Language:** English
*/
// :snippet-end:
// :remove-start:
async function main() {
// Verify the store has the data
const storedData = await store.get(["users"], "user_123");
if (!storedData) {
throw new Error("Expected data to be in store");
}
if (storedData.value["name"] !== "John Smith") {
throw new Error('Expected name to be "John Smith"');
}
console.log("✓ Read tool with InMemoryStore works correctly");
}
if (import.meta.url === `file://${process.argv[1]}`) {
main().catch((error) => {
console.error(error);
process.exit(1);
});
}
// :remove-end:
@@ -0,0 +1,57 @@
# :snippet-start: long-term-memory-read-tool-postgres-py
from dataclasses import dataclass
from langchain.agents import create_agent
from langchain.tools import ToolRuntime, tool
from langchain_core.runnables import Runnable
from langgraph.store.postgres import PostgresStore # type: ignore[import-not-found]
@dataclass
class Context:
user_id: str
DB_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable"
# :remove-start:
import os
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from conftest import get_postgres_uri, prepare_postgres_store
DB_URI = get_postgres_uri()
prepare_postgres_store(DB_URI)
os.environ.setdefault("ANTHROPIC_API_KEY", "sk-ant-test-key")
# :remove-end:
with PostgresStore.from_conn_string(DB_URI) as store:
store.setup()
store.put(("users",), "user_123", {"name": "John Smith", "language": "English"})
@tool
def get_user_info(runtime: ToolRuntime[Context]) -> str:
"""Look up user info."""
assert runtime.store is not None
user_info = runtime.store.get(("users",), runtime.context.user_id)
return str(user_info.value) if user_info else "Unknown user"
agent: Runnable = create_agent(
"claude-sonnet-4-6",
tools=[get_user_info],
store=store,
context_schema=Context,
)
result = agent.invoke(
{"messages": [{"role": "user", "content": "look up user information"}]},
context=Context(user_id="user_123"),
)
# :snippet-end:
# :remove-start:
assert result is not None
assert "messages" in result
print("✓ Read tool with PostgresStore works correctly")
# :remove-end:
@@ -0,0 +1,93 @@
// :snippet-start: long-term-memory-read-tool-postgres-js
import * as z from "zod";
import { createAgent, tool, type ToolRuntime } from "langchain";
import { PostgresStore } from "@langchain/langgraph-checkpoint-postgres/store";
const DB_URI =
process.env.POSTGRES_URI ??
"postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable";
const store = PostgresStore.fromConnString(DB_URI);
// :remove-start:
// Drop old store tables if they exist (prior version used different schema).
// setup() uses CREATE TABLE IF NOT EXISTS and does not migrate existing tables.
await (
store as { core: { pool: { query: (q: string) => Promise<unknown> } } }
).core.pool.query(
"DROP TABLE IF EXISTS public.store_vectors CASCADE; DROP TABLE IF EXISTS public.store CASCADE; DROP TABLE IF EXISTS public.store_migrations CASCADE;",
);
// :remove-end:
await store.setup();
const contextSchema = z.object({ userId: z.string() });
await store.put(["users"], "user_123", {
name: "John Smith",
language: "English",
});
const getUserInfo = tool(
async (_, runtime: ToolRuntime<unknown, z.infer<typeof contextSchema>>) => {
const userId = runtime.context.userId;
if (!userId) throw new Error("userId is required");
const userInfo = await runtime.store.get(["users"], userId);
return userInfo?.value ? JSON.stringify(userInfo.value) : "Unknown user";
},
{
name: "getUserInfo",
description: "Look up user info by userId from the store.",
schema: z.object({}),
},
);
const agent = createAgent({
model: "claude-sonnet-4-6",
tools: [getUserInfo],
contextSchema,
store,
});
await agent.invoke(
{ messages: [{ role: "user", content: "look up user information" }] },
{ context: { userId: "user_123" } },
);
// :snippet-end:
// :remove-start:
async function main() {
const DB_URI =
process.env.POSTGRES_URI ||
"postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable";
const store = PostgresStore.fromConnString(DB_URI);
try {
await store.setup();
const contextSchema = z.object({ userId: z.string() });
await store.put(["users"], "user_123", {
name: "John Smith",
language: "English",
});
// Verify the store has the data
const storedData = await store.get(["users"], "user_123");
if (!storedData) {
throw new Error("Expected data to be in store");
}
if (storedData.value["name"] !== "John Smith") {
throw new Error('Expected name to be "John Smith"');
}
console.log("✓ Read tool with PostgresStore works correctly");
} finally {
await store.stop();
}
}
if (import.meta.url === `file://${process.argv[1]}`) {
main().catch((error) => {
console.error(error);
process.exit(1);
});
}
// :remove-end:
@@ -0,0 +1,50 @@
# :snippet-start: long-term-memory-storage-inmemory-py
from collections.abc import Sequence
from langgraph.store.base import IndexConfig
from langgraph.store.memory import InMemoryStore
def embed(texts: Sequence[str]) -> list[list[float]]:
# Replace with an actual embedding function or LangChain embeddings object
return [[1.0, 2.0] for _ in texts]
# InMemoryStore saves data to an in-memory dictionary. Use a DB-backed store in production use.
store = InMemoryStore(index=IndexConfig(embed=embed, dims=2))
user_id = "my-user"
application_context = "chitchat"
namespace = (user_id, application_context)
store.put(
namespace,
"a-memory",
{
"rules": [
"User likes short, direct language",
"User only speaks English & python",
],
"my-key": "my-value",
},
)
# get the "memory" by ID
item = store.get(namespace, "a-memory")
# search for "memories" within this namespace, filtering on content equivalence, sorted by vector similarity
items = store.search(
namespace, filter={"my-key": "my-value"}, query="language preferences"
)
# :snippet-end:
# :remove-start:
if __name__ == "__main__":
# Verify the operations work
assert item is not None
assert item.value["my-key"] == "my-value"
assert "rules" in item.value
assert len(item.value["rules"]) == 2
# Verify search returns results
assert len(items) > 0
assert items[0].value["my-key"] == "my-value"
print("✓ InMemoryStore operations work correctly")
# :remove-end:
@@ -0,0 +1,66 @@
// :snippet-start: long-term-memory-storage-inmemory-js
import { InMemoryStore } from "@langchain/langgraph";
const embed = (texts: string[]): number[][] => {
// Replace with an actual embedding function or LangChain embeddings object
return texts.map(() => [1.0, 2.0]);
};
// InMemoryStore saves data to an in-memory dictionary. Use a DB-backed store in production use.
const store = new InMemoryStore({ index: { embed, dims: 2 } });
const userId = "my-user";
const applicationContext = "chitchat";
const namespace = [userId, applicationContext];
await store.put(namespace, "a-memory", {
rules: [
"User likes short, direct language",
"User only speaks English & TypeScript",
],
"my-key": "my-value",
});
// get the "memory" by ID
const item = await store.get(namespace, "a-memory");
// search for "memories" within this namespace, filtering on content equivalence, sorted by vector similarity
const items = await store.search(namespace, {
filter: { "my-key": "my-value" },
query: "language preferences",
});
// :snippet-end:
// :remove-start:
async function main() {
// Verify the operations work
if (!item) {
throw new Error("Item should not be null");
}
if (item.value["my-key"] !== "my-value") {
throw new Error('Expected my-key to be "my-value"');
}
if (!item.value["rules"]) {
throw new Error("Expected rules to exist");
}
if ((item.value["rules"] as string[]).length !== 2) {
throw new Error("Expected 2 rules");
}
// Verify search returns results
if (items.length === 0) {
throw new Error("Expected search to return results");
}
if (items[0].value["my-key"] !== "my-value") {
throw new Error('Expected search result my-key to be "my-value"');
}
console.log("✓ InMemoryStore operations work correctly");
}
if (import.meta.url === `file://${process.argv[1]}`) {
main().catch((error) => {
console.error(error);
process.exit(1);
});
}
// :remove-end:
@@ -0,0 +1,58 @@
# :snippet-start: long-term-memory-storage-postgres-py
from collections.abc import Sequence
from langgraph.store.base import IndexConfig
from langgraph.store.postgres import PostgresStore # type: ignore[import-not-found]
def embed(texts: Sequence[str]) -> list[list[float]]:
# Replace with an actual embedding function or LangChain embeddings object
return [[1.0, 2.0] for _ in texts]
DB_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable"
# :remove-start:
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from conftest import get_postgres_uri, prepare_postgres_store
DB_URI = get_postgres_uri()
prepare_postgres_store(DB_URI)
# :remove-end:
with PostgresStore.from_conn_string(
DB_URI,
index=IndexConfig(embed=embed, dims=2), # type: ignore[arg-type]
) as store:
store.setup()
user_id = "my-user"
application_context = "chitchat"
namespace = (user_id, application_context)
store.put(
namespace,
"a-memory",
{
"rules": [
"User likes short, direct language",
"User only speaks English & python",
],
"my-key": "my-value",
},
)
item = store.get(namespace, "a-memory")
items = store.search(
namespace, filter={"my-key": "my-value"}, query="language preferences"
)
# :snippet-end:
# :remove-start:
if __name__ == "__main__":
assert item is not None
assert item.value["my-key"] == "my-value"
assert "rules" in item.value
assert len(item.value["rules"]) == 2
assert len(items) > 0
print("✓ PostgresStore operations work correctly")
# :remove-end:
@@ -0,0 +1,90 @@
// :snippet-start: long-term-memory-storage-postgres-js
import { PostgresStore } from "@langchain/langgraph-checkpoint-postgres/store";
const embed = (texts: string[]): number[][] => {
return texts.map(() => [1.0, 2.0]);
};
const DB_URI =
process.env.POSTGRES_URI ??
"postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable";
const store = PostgresStore.fromConnString(DB_URI, {
index: { embed, dims: 2 },
});
// :remove-start:
// Drop tables from prior runs (Python samples use different schema; CI shares one DB)
await (
store as { core: { pool: { query: (q: string) => Promise<unknown> } } }
).core.pool.query(
"DROP TABLE IF EXISTS public.store_vectors CASCADE; DROP TABLE IF EXISTS public.store CASCADE; DROP TABLE IF EXISTS public.store_migrations CASCADE;",
);
// :remove-end:
await store.setup();
const userId = "my-user";
const applicationContext = "chitchat";
const namespace = [userId, applicationContext];
await store.put(namespace, "a-memory", {
rules: [
"User likes short, direct language",
"User only speaks English & TypeScript",
],
"my-key": "my-value",
});
const item = await store.get(namespace, "a-memory");
const items = await store.search(namespace, {
filter: { "my-key": "my-value" },
query: "language preferences",
});
// :snippet-end:
// :remove-start:
try {
await store.setup();
const userId = "my-user";
const applicationContext = "chitchat";
const namespace = [userId, applicationContext];
await store.put(namespace, "a-memory", {
rules: [
"User likes short, direct language",
"User only speaks English & TypeScript",
],
"my-key": "my-value",
});
const item = await store.get(namespace, "a-memory");
const items = await store.search(namespace, {
filter: { "my-key": "my-value" },
query: "language preferences",
});
// Verify the operations work
if (!item) {
throw new Error("Item should not be null");
}
if (item.value["my-key"] !== "my-value") {
throw new Error('Expected my-key to be "my-value"');
}
if (!item.value["rules"]) {
throw new Error("Expected rules to exist");
}
if ((item.value["rules"] as string[]).length !== 2) {
throw new Error("Expected 2 rules");
}
// Verify search returns results
if (items.length === 0) {
throw new Error("Expected search to return results");
}
console.log("✓ PostgresStore operations work correctly");
} finally {
await store.stop();
}
// :remove-end:
@@ -0,0 +1,67 @@
# :snippet-start: long-term-memory-write-tool-inmemory-py
from dataclasses import dataclass
from typing import Any, cast
from langchain.agents import create_agent
from langchain.tools import ToolRuntime, tool
from langchain_core.runnables import Runnable
from langgraph.store.memory import InMemoryStore
from typing_extensions import TypedDict
# InMemoryStore saves data to an in-memory dictionary. Use a DB-backed store in production.
store = InMemoryStore()
@dataclass
class Context:
user_id: str
# TypedDict defines the structure of user information for the LLM
class UserInfo(TypedDict):
name: str
# Tool that allows agent to update user information (useful for chat applications)
@tool
def save_user_info(user_info: UserInfo, runtime: ToolRuntime[Context]) -> str:
"""Save user info."""
# Access the store - same as that provided to `create_agent`
assert runtime.store is not None
store = runtime.store
user_id = runtime.context.user_id
# Store data in the store (namespace, key, data)
store.put(("users",), user_id, cast("dict[str, Any]", user_info))
return "Successfully saved user info."
agent: Runnable = create_agent(
model="claude-sonnet-4-6",
tools=[save_user_info],
store=store,
context_schema=Context,
)
# Run the agent
agent.invoke(
{"messages": [{"role": "user", "content": "My name is John Smith"}]},
# user_id passed in context to identify whose information is being updated
context=Context(user_id="user_123"),
)
# You can access the store directly to get the value
item = store.get(("users",), "user_123")
# :snippet-end:
# :remove-start:
if __name__ == "__main__":
# Test by putting data directly into the store
store.put(("users",), "user_123", {"name": "John Smith"})
# Verify data was saved
saved_data = store.get(("users",), "user_123")
assert saved_data is not None
assert saved_data.value["name"] == "John Smith"
print("✓ Write tool with InMemoryStore works correctly")
# :remove-end:
@@ -0,0 +1,88 @@
// :snippet-start: long-term-memory-write-tool-inmemory-js
import * as z from "zod";
import { tool, createAgent, type ToolRuntime } from "langchain";
import { InMemoryStore } from "@langchain/langgraph";
// InMemoryStore saves data to an in-memory dictionary. Use a DB-backed store in production.
const store = new InMemoryStore();
const contextSchema = z.object({
userId: z.string(),
});
// Schema defines the structure of user information for the LLM
const UserInfo = z.object({
name: z.string(),
});
// Tool that allows agent to update user information (useful for chat applications)
const saveUserInfo = tool(
async (
userInfo: z.infer<typeof UserInfo>,
runtime: ToolRuntime<unknown, z.infer<typeof contextSchema>>,
) => {
const userId = runtime.context.userId;
if (!userId) {
throw new Error("userId is required");
}
// Store data in the store (namespace, key, data)
await runtime.store.put(["users"], userId, userInfo);
return "Successfully saved user info.";
},
{
name: "save_user_info",
description: "Save user info",
schema: UserInfo,
},
);
const agent = createAgent({
model: "claude-sonnet-4-6",
tools: [saveUserInfo],
contextSchema,
store,
});
// Run the agent
await agent.invoke(
{ messages: [{ role: "user", content: "My name is John Smith" }] },
// userId passed in context to identify whose information is being updated
{ context: { userId: "user_123" } },
);
// You can access the store directly to get the value
const result = await store.get(["users"], "user_123");
console.log(result?.value); // Output: { name: "John Smith" }
// :snippet-end:
// :remove-start:
async function main() {
// Test the tool directly - pass context and store in config (same shape as ToolRuntime)
const saveResult = await saveUserInfo.invoke(
{ name: "John Smith" },
{ context: { userId: "user_123" }, store },
);
if (saveResult !== "Successfully saved user info.") {
throw new Error("Expected save to succeed");
}
// Verify data was saved
const savedData = await store.get(["users"], "user_123");
if (!savedData) {
throw new Error("Expected data to be saved");
}
if (savedData.value["name"] !== "John Smith") {
throw new Error('Expected name to be "John Smith"');
}
console.log("✓ Write tool with InMemoryStore works correctly");
}
if (import.meta.url === `file://${process.argv[1]}`) {
main().catch((error) => {
console.error(error);
process.exit(1);
});
}
// :remove-end:
@@ -0,0 +1,69 @@
# :snippet-start: long-term-memory-write-tool-postgres-py
from dataclasses import dataclass
from typing import Any, cast
from langchain.agents import create_agent
from langchain.tools import ToolRuntime, tool
from langchain_core.runnables import Runnable
from langgraph.store.postgres import PostgresStore # type: ignore[import-not-found]
from typing_extensions import TypedDict
@dataclass
class Context:
user_id: str
class UserInfo(TypedDict):
name: str
@tool
def save_user_info(user_info: UserInfo, runtime: ToolRuntime[Context]) -> str:
"""Save user info."""
assert runtime.store is not None
runtime.store.put(
("users",), runtime.context.user_id, cast("dict[str, Any]", user_info)
)
return "Successfully saved user info."
DB_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable"
# :remove-start:
import os
import sys
from pathlib import Path
os.environ.setdefault("ANTHROPIC_API_KEY", "sk-ant-test-key")
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from conftest import get_postgres_uri, prepare_postgres_store
DB_URI = get_postgres_uri()
prepare_postgres_store(DB_URI)
# :remove-end:
with PostgresStore.from_conn_string(DB_URI) as store:
store.setup()
agent: Runnable = create_agent(
"claude-sonnet-4-6",
tools=[save_user_info],
store=store,
context_schema=Context,
)
agent.invoke(
{"messages": [{"role": "user", "content": "My name is John Smith"}]},
context=Context(user_id="user_123"),
)
# :snippet-end:
# :remove-start:
if __name__ == "__main__":
with PostgresStore.from_conn_string(DB_URI) as store:
store.setup()
store.put(("users",), "user_123", {"name": "John Smith"})
saved_data = store.get(("users",), "user_123")
assert saved_data is not None
assert saved_data.value["name"] == "John Smith"
print("✓ Write tool with PostgresStore works correctly")
# :remove-end:
@@ -0,0 +1,114 @@
// :snippet-start: long-term-memory-write-tool-postgres-js
import * as z from "zod";
import { tool, createAgent, type ToolRuntime } from "langchain";
import { PostgresStore } from "@langchain/langgraph-checkpoint-postgres/store";
const DB_URI =
process.env.POSTGRES_URI ??
"postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable";
const store = PostgresStore.fromConnString(DB_URI);
// :remove-start:
// Drop tables from prior runs (Python samples use different schema; CI shares one DB)
await (
store as { core: { pool: { query: (q: string) => Promise<unknown> } } }
).core.pool.query(
"DROP TABLE IF EXISTS public.store_vectors CASCADE; DROP TABLE IF EXISTS public.store CASCADE; DROP TABLE IF EXISTS public.store_migrations CASCADE;",
);
// :remove-end:
await store.setup();
const contextSchema = z.object({ userId: z.string() });
const UserInfo = z.object({ name: z.string() });
const saveUserInfo = tool(
async (
userInfo: z.infer<typeof UserInfo>,
runtime: ToolRuntime<unknown, z.infer<typeof contextSchema>>,
) => {
const userId = runtime.context.userId;
if (!userId) throw new Error("userId is required");
await runtime.store.put(["users"], userId, userInfo);
return "Successfully saved user info.";
},
{ name: "save_user_info", description: "Save user info", schema: UserInfo },
);
const agent = createAgent({
model: "claude-sonnet-4-6",
tools: [saveUserInfo],
contextSchema,
store,
});
await agent.invoke(
{ messages: [{ role: "user", content: "My name is John Smith" }] },
{ context: { userId: "user_123" } },
);
const result = await store.get(["users"], "user_123");
console.log(result?.value);
// :snippet-end:
// :remove-start:
async function main() {
const DB_URI =
process.env.POSTGRES_URI ||
"postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable";
const store = PostgresStore.fromConnString(DB_URI);
try {
await store.setup();
const contextSchema = z.object({ userId: z.string() });
const UserInfo = z.object({ name: z.string() });
const saveUserInfo = tool(
async (
userInfo: z.infer<typeof UserInfo>,
runtime: ToolRuntime<unknown, z.infer<typeof contextSchema>>,
) => {
const userId = runtime.context.userId;
if (!userId) throw new Error("userId is required");
await runtime.store.put(["users"], userId, userInfo);
return "Successfully saved user info.";
},
{
name: "save_user_info",
description: "Save user info",
schema: UserInfo,
},
);
// Test the tool directly - pass context and store in config (same shape as ToolRuntime)
const saveResult = await saveUserInfo.invoke(
{ name: "John Smith" },
{ context: { userId: "user_123" }, store },
);
if (saveResult !== "Successfully saved user info.") {
throw new Error("Expected save to succeed");
}
// Verify data was saved
const savedData = await store.get(["users"], "user_123");
if (!savedData) {
throw new Error("Expected data to be saved");
}
if (savedData.value["name"] !== "John Smith") {
throw new Error('Expected name to be "John Smith"');
}
console.log("✓ Write tool with PostgresStore works correctly");
} finally {
await store.stop();
}
}
if (import.meta.url === `file://${process.argv[1]}`) {
main().catch((error) => {
console.error(error);
process.exit(1);
});
}
// :remove-end:
+208 -2
View File
@@ -10,6 +10,8 @@
"license": "ISC",
"dependencies": {
"@langchain/anthropic": "^1.3.22",
"@langchain/langgraph-checkpoint-postgres": "^1.0.1",
"@langchain/openai": "^1.2.12",
"langchain": "^1.2.28",
"zod": "^3.23.0"
},
@@ -50,7 +52,8 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.1.1.tgz",
"integrity": "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.27.3",
@@ -573,6 +576,22 @@
"@langchain/core": "^1.0.1"
}
},
"node_modules/@langchain/langgraph-checkpoint-postgres": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@langchain/langgraph-checkpoint-postgres/-/langgraph-checkpoint-postgres-1.0.1.tgz",
"integrity": "sha512-cRXOLYZc0egMjyAQfBblWXdoBS+WKwEbCEuE+/f88XHJ1bq5jVz7aycbpjF/7F5EykG7xmybQOz5eUdg3neRzg==",
"license": "MIT",
"dependencies": {
"pg": "^8.12.0"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@langchain/core": "^1.0.1",
"@langchain/langgraph-checkpoint": "^1.0.0"
}
},
"node_modules/@langchain/langgraph-checkpoint/node_modules/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
@@ -674,6 +693,23 @@
"uuid": "dist/bin/uuid"
}
},
"node_modules/@langchain/openai": {
"version": "1.2.12",
"resolved": "https://registry.npmjs.org/@langchain/openai/-/openai-1.2.12.tgz",
"integrity": "sha512-Im6PPNujrfkZk4vpc9JAjbeERg+RbNtWRe3KSFOP7aNGa/yZ+XD69lxXwbsZGaZkbiUN/hwe9RYeisUfThb5wg==",
"license": "MIT",
"dependencies": {
"js-tiktoken": "^1.0.12",
"openai": "^6.24.0",
"zod": "^3.25.76 || ^4"
},
"engines": {
"node": ">=20"
},
"peerDependencies": {
"@langchain/core": "^1.1.30"
}
},
"node_modules/@standard-schema/spec": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
@@ -697,6 +733,7 @@
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
},
@@ -729,6 +766,7 @@
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
"integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
},
@@ -762,6 +800,7 @@
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -947,10 +986,32 @@
"resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
"integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==",
"license": "MIT",
"peer": true,
"bin": {
"mustache": "bin/mustache"
}
},
"node_modules/openai": {
"version": "6.27.0",
"resolved": "https://registry.npmjs.org/openai/-/openai-6.27.0.tgz",
"integrity": "sha512-osTKySlrdYrLYTt0zjhY8yp0JUBmWDCN+Q+QxsV4xMQnnoVFpylgKGgxwN8sSdTNw0G4y+WUXs4eCMWpyDNWZQ==",
"license": "Apache-2.0",
"bin": {
"openai": "bin/cli"
},
"peerDependencies": {
"ws": "^8.18.0",
"zod": "^3.25 || ^4.0"
},
"peerDependenciesMeta": {
"ws": {
"optional": true
},
"zod": {
"optional": true
}
}
},
"node_modules/p-finally": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
@@ -1003,6 +1064,134 @@
"node": ">=8"
}
},
"node_modules/pg": {
"version": "8.20.0",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz",
"integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==",
"license": "MIT",
"dependencies": {
"pg-connection-string": "^2.12.0",
"pg-pool": "^3.13.0",
"pg-protocol": "^1.13.0",
"pg-types": "2.2.0",
"pgpass": "1.0.5"
},
"engines": {
"node": ">= 16.0.0"
},
"optionalDependencies": {
"pg-cloudflare": "^1.3.0"
},
"peerDependencies": {
"pg-native": ">=3.0.1"
},
"peerDependenciesMeta": {
"pg-native": {
"optional": true
}
}
},
"node_modules/pg-cloudflare": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz",
"integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==",
"license": "MIT",
"optional": true
},
"node_modules/pg-connection-string": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz",
"integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==",
"license": "MIT"
},
"node_modules/pg-int8": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
"license": "ISC",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/pg-pool": {
"version": "3.13.0",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz",
"integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==",
"license": "MIT",
"peerDependencies": {
"pg": ">=8.0"
}
},
"node_modules/pg-protocol": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz",
"integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==",
"license": "MIT"
},
"node_modules/pg-types": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
"license": "MIT",
"dependencies": {
"pg-int8": "1.0.1",
"postgres-array": "~2.0.0",
"postgres-bytea": "~1.0.0",
"postgres-date": "~1.0.4",
"postgres-interval": "^1.1.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/pgpass": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
"license": "MIT",
"dependencies": {
"split2": "^4.1.0"
}
},
"node_modules/postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/postgres-bytea": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
"integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-date": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-interval": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
"license": "MIT",
"dependencies": {
"xtend": "^4.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/resolve-pkg-maps": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
@@ -1031,6 +1220,15 @@
"integrity": "sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw==",
"license": "MIT"
},
"node_modules/split2": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
"license": "ISC",
"engines": {
"node": ">= 10.x"
}
},
"node_modules/ts-algebra": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz",
@@ -1070,12 +1268,20 @@
"uuid": "dist/esm/bin/uuid"
}
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"license": "MIT",
"engines": {
"node": ">=0.4"
}
},
"node_modules/zod": {
"version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
+2
View File
@@ -11,6 +11,8 @@
"license": "ISC",
"dependencies": {
"@langchain/anthropic": "^1.3.22",
"@langchain/langgraph-checkpoint-postgres": "^1.0.1",
"@langchain/openai": "^1.2.12",
"langchain": "^1.2.28",
"zod": "^3.23.0"
},
+122 -282
View File
@@ -1,11 +1,74 @@
---
title: Long-term memory
description: Add long-term memory to LangChain agents to store and recall data across conversations and sessions
---
## Overview
import LongTermMemoryCreateAgentInmemoryPy from '/snippets/code-samples/long-term-memory-create-agent-inmemory-py.mdx';
import LongTermMemoryCreateAgentInmemoryJs from '/snippets/code-samples/long-term-memory-create-agent-inmemory-js.mdx';
import LongTermMemoryCreateAgentPostgresPy from '/snippets/code-samples/long-term-memory-create-agent-postgres-py.mdx';
import LongTermMemoryCreateAgentPostgresJs from '/snippets/code-samples/long-term-memory-create-agent-postgres-js.mdx';
import LongTermMemoryStorageInmemoryPy from '/snippets/code-samples/long-term-memory-storage-inmemory-py.mdx';
import LongTermMemoryStorageInmemoryJs from '/snippets/code-samples/long-term-memory-storage-inmemory-js.mdx';
import LongTermMemoryStoragePostgresPy from '/snippets/code-samples/long-term-memory-storage-postgres-py.mdx';
import LongTermMemoryStoragePostgresJs from '/snippets/code-samples/long-term-memory-storage-postgres-js.mdx';
import LongTermMemoryReadToolInmemoryPy from '/snippets/code-samples/long-term-memory-read-tool-inmemory-py.mdx';
import LongTermMemoryReadToolInmemoryJs from '/snippets/code-samples/long-term-memory-read-tool-inmemory-js.mdx';
import LongTermMemoryReadToolPostgresPy from '/snippets/code-samples/long-term-memory-read-tool-postgres-py.mdx';
import LongTermMemoryReadToolPostgresJs from '/snippets/code-samples/long-term-memory-read-tool-postgres-js.mdx';
import LongTermMemoryWriteToolInmemoryPy from '/snippets/code-samples/long-term-memory-write-tool-inmemory-py.mdx';
import LongTermMemoryWriteToolInmemoryJs from '/snippets/code-samples/long-term-memory-write-tool-inmemory-js.mdx';
import LongTermMemoryWriteToolPostgresPy from '/snippets/code-samples/long-term-memory-write-tool-postgres-py.mdx';
import LongTermMemoryWriteToolPostgresJs from '/snippets/code-samples/long-term-memory-write-tool-postgres-js.mdx';
LangChain agents use [LangGraph persistence](/oss/langgraph/persistence#memory-store) to enable long-term memory. This is a more advanced topic and requires knowledge of LangGraph to use.
Long-term memory lets your agent store and recall information across different conversations and sessions.
Unlike [short-term memory](/oss/langchain/short-term-memory), which is scoped to a single thread, long-term memory persists across threads and can be recalled at any time.
Long-term memory is built on [LangGraph stores](/oss/langgraph/persistence#memory-store), which save data as JSON documents organized by namespace and key.
## Usage
To add long-term memory to an agent, create a store and pass it to @[`create_agent`]:
<Tabs>
<Tab title="InMemoryStore">
:::python
<LongTermMemoryCreateAgentInmemoryPy />
:::
:::js
<LongTermMemoryCreateAgentInmemoryJs />
:::
</Tab>
<Tab title="PostgreSQL">
:::python
```shell
pip install langgraph-checkpoint-postgres
```
<LongTermMemoryCreateAgentPostgresPy />
:::
:::js
```shell
npm install @langchain/langgraph-checkpoint-postgres
```
<LongTermMemoryCreateAgentPostgresJs />
:::
</Tab>
</Tabs>
Tools can then read from and write to the store using the `runtime.store` parameter. See [Read long-term memory in tools](#read-long-term-memory-in-tools) and [Write long-term memory from tools](#write-long-term-memory-from-tools) for examples.
<Tip>
For a deeper dive into memory types (semantic, episodic, procedural) and strategies for writing memories, see the [Memory conceptual guide](/oss/concepts/memory#long-term-memory).
</Tip>
## Memory storage
@@ -15,319 +78,96 @@ Each memory is organized under a custom `namespace` (similar to a folder) and a
This structure enables hierarchical organization of memories. Cross-namespace searching is then supported through content filters.
<Tabs>
<Tab title="InMemoryStore">
:::python
```python
from langgraph.store.memory import InMemoryStore
<LongTermMemoryStorageInmemoryPy />
def embed(texts: list[str]) -> list[list[float]]:
# Replace with an actual embedding function or LangChain embeddings object
return [[1.0, 2.0] * len(texts)]
# InMemoryStore saves data to an in-memory dictionary. Use a DB-backed store in production use.
store = InMemoryStore(index={"embed": embed, "dims": 2}) # [!code highlight]
user_id = "my-user"
application_context = "chitchat"
namespace = (user_id, application_context) # [!code highlight]
store.put( # [!code highlight]
namespace,
"a-memory",
{
"rules": [
"User likes short, direct language",
"User only speaks English & python",
],
"my-key": "my-value",
},
)
# get the "memory" by ID
item = store.get(namespace, "a-memory") # [!code highlight]
# search for "memories" within this namespace, filtering on content equivalence, sorted by vector similarity
items = store.search( # [!code highlight]
namespace, filter={"my-key": "my-value"}, query="language preferences"
)
```
:::
:::js
```typescript
import { InMemoryStore } from "@langchain/langgraph";
const embed = (texts: string[]): number[][] => {
// Replace with an actual embedding function or LangChain embeddings object
return texts.map(() => [1.0, 2.0]);
};
<LongTermMemoryStorageInmemoryJs />
// InMemoryStore saves data to an in-memory dictionary. Use a DB-backed store in production use.
const store = new InMemoryStore({ index: { embed, dims: 2 } }); // [!code highlight]
const userId = "my-user";
const applicationContext = "chitchat";
const namespace = [userId, applicationContext]; // [!code highlight]
await store.put( // [!code highlight]
namespace,
"a-memory",
{
rules: [
"User likes short, direct language",
"User only speaks English & TypeScript",
],
"my-key": "my-value",
}
);
// get the "memory" by ID
const item = await store.get(namespace, "a-memory"); // [!code highlight]
// search for "memories" within this namespace, filtering on content equivalence, sorted by vector similarity
const items = await store.search( // [!code highlight]
namespace,
{
filter: { "my-key": "my-value" },
query: "language preferences"
}
);
```
:::
</Tab>
<Tab title="PostgreSQL">
:::python
<LongTermMemoryStoragePostgresPy />
:::
:::js
<LongTermMemoryStoragePostgresJs />
:::
</Tab>
</Tabs>
For more information about the memory store, see the [Persistence](/oss/langgraph/persistence#memory-store) guide.
## Read long-term memory in tools
<Tabs>
<Tab title="InMemoryStore">
:::python
```python A tool the agent can use to look up user information
from dataclasses import dataclass
from langchain_core.runnables import RunnableConfig
from langchain.agents import create_agent
from langchain.tools import tool, ToolRuntime
from langgraph.store.memory import InMemoryStore
@dataclass
class Context:
user_id: str
# InMemoryStore saves data to an in-memory dictionary. Use a DB-backed store in production.
store = InMemoryStore() # [!code highlight]
# Write sample data to the store using the put method
store.put( # [!code highlight]
("users",), # Namespace to group related data together (users namespace for user data)
"user_123", # Key within the namespace (user ID as key)
{
"name": "John Smith",
"language": "English",
} # Data to store for the given user
)
@tool
def get_user_info(runtime: ToolRuntime[Context]) -> str:
"""Look up user info."""
# Access the store - same as that provided to `create_agent`
store = runtime.store # [!code highlight]
user_id = runtime.context.user_id
# Retrieve data from store - returns StoreValue object with value and metadata
user_info = store.get(("users",), user_id) # [!code highlight]
return str(user_info.value) if user_info else "Unknown user"
agent = create_agent(
model="claude-sonnet-4-6",
tools=[get_user_info],
# Pass store to agent - enables agent to access store when running tools
store=store, # [!code highlight]
context_schema=Context
)
# Run the agent
agent.invoke(
{"messages": [{"role": "user", "content": "look up user information"}]},
context=Context(user_id="user_123") # [!code highlight]
)
```
<LongTermMemoryReadToolInmemoryPy />
:::
:::js
```typescript A tool the agent can use to look up user information
import * as z from "zod";
import { createAgent, tool, type ToolRuntime } from "langchain";
import { InMemoryStore } from "@langchain/langgraph";
// InMemoryStore saves data to an in-memory dictionary. Use a DB-backed store in production.
const store = new InMemoryStore(); // [!code highlight]
const contextSchema = z.object({
userId: z.string(),
});
// Write sample data to the store using the put method
await store.put( // [!code highlight]
["users"], // Namespace to group related data together (users namespace for user data)
"user_123", // Key within the namespace (user ID as key)
{
name: "John Smith",
language: "English",
} // Data to store for the given user
);
const getUserInfo = tool(
// Look up user info.
async (_, runtime: ToolRuntime<unknown, z.infer<typeof contextSchema>>) => {
// Access the store - same as that provided to `createAgent`
const userId = runtime.context.userId;
if (!userId) {
throw new Error("userId is required");
}
// Retrieve data from store - returns StoreValue object with value and metadata
const userInfo = await runtime.store.get(["users"], userId);
return userInfo?.value ? JSON.stringify(userInfo.value) : "Unknown user";
},
{
name: "getUserInfo",
description: "Look up user info by userId from the store.",
schema: z.object({}),
}
);
const agent = createAgent({
model: "gpt-4.1-mini",
tools: [getUserInfo],
contextSchema,
// Pass store to agent - enables agent to access store when running tools
store, // [!code highlight]
});
// Run the agent
const result = await agent.invoke(
{ messages: [{ role: "user", content: "look up user information" }] },
{ context: { userId: "user_123" } } // [!code highlight]
);
console.log(result.messages.at(-1)?.content);
/**
* Outputs:
* User Information:
* - Name: John Smith
* - Language: English
*/
```
<LongTermMemoryReadToolInmemoryJs />
:::
</Tab>
<Tab title="PostgreSQL">
:::python
<LongTermMemoryReadToolPostgresPy />
:::
:::js
<LongTermMemoryReadToolPostgresJs />
:::
</Tab>
</Tabs>
<a id="write-long-term"></a>
## Write long-term memory from tools
<Tabs>
<Tab title="InMemoryStore">
:::python
```python Example of a tool that updates user information
from dataclasses import dataclass
from typing_extensions import TypedDict
from langchain.agents import create_agent
from langchain.tools import tool, ToolRuntime
from langgraph.store.memory import InMemoryStore
# InMemoryStore saves data to an in-memory dictionary. Use a DB-backed store in production.
store = InMemoryStore() # [!code highlight]
@dataclass
class Context:
user_id: str
# TypedDict defines the structure of user information for the LLM
class UserInfo(TypedDict):
name: str
# Tool that allows agent to update user information (useful for chat applications)
@tool
def save_user_info(user_info: UserInfo, runtime: ToolRuntime[Context]) -> str:
"""Save user info."""
# Access the store - same as that provided to `create_agent`
store = runtime.store # [!code highlight]
user_id = runtime.context.user_id # [!code highlight]
# Store data in the store (namespace, key, data)
store.put(("users",), user_id, user_info) # [!code highlight]
return "Successfully saved user info."
agent = create_agent(
model="claude-sonnet-4-6",
tools=[save_user_info],
store=store, # [!code highlight]
context_schema=Context
)
# Run the agent
agent.invoke(
{"messages": [{"role": "user", "content": "My name is John Smith"}]},
# user_id passed in context to identify whose information is being updated
context=Context(user_id="user_123") # [!code highlight]
)
# You can access the store directly to get the value
store.get(("users",), "user_123").value
```
<LongTermMemoryWriteToolInmemoryPy />
:::
:::js
```typescript Example of a tool that updates user information
import * as z from "zod";
import { tool, createAgent, type ToolRuntime } from "langchain";
import { InMemoryStore } from "@langchain/langgraph";
// InMemoryStore saves data to an in-memory dictionary. Use a DB-backed store in production.
const store = new InMemoryStore(); // [!code highlight]
const contextSchema = z.object({
userId: z.string(),
});
// Schema defines the structure of user information for the LLM
const UserInfo = z.object({
name: z.string(),
});
// Tool that allows agent to update user information (useful for chat applications)
const saveUserInfo = tool(
async (
userInfo: z.infer<typeof UserInfo>,
runtime: ToolRuntime<unknown, z.infer<typeof contextSchema>>
) => {
const userId = runtime.context.userId;
if (!userId) {
throw new Error("userId is required");
}
// Store data in the store (namespace, key, data)
await runtime.store.put(["users"], userId, userInfo);
return "Successfully saved user info.";
},
{
name: "save_user_info",
description: "Save user info",
schema: UserInfo,
}
);
const agent = createAgent({
model: "gpt-4.1-mini",
tools: [saveUserInfo],
contextSchema,
store, // [!code highlight]
});
// Run the agent
await agent.invoke(
{ messages: [{ role: "user", content: "My name is John Smith" }] },
// userId passed in context to identify whose information is being updated
{ context: { userId: "user_123" } } // [!code highlight]
);
// You can access the store directly to get the value
const result = await store.get(["users"], "user_123");
console.log(result?.value); // Output: { name: "John Smith" }
```
<LongTermMemoryWriteToolInmemoryJs />
:::
</Tab>
<Tab title="PostgreSQL">
:::python
<LongTermMemoryWriteToolPostgresPy />
:::
:::js
<LongTermMemoryWriteToolPostgresJs />
:::
</Tab>
</Tabs>
+4
View File
@@ -18,6 +18,10 @@ Even if your model supports the full context length, most LLMs still perform poo
Chat models accept context using [messages](/oss/langchain/messages), which include instructions (a system message) and inputs (human messages). In chat applications, messages alternate between human inputs and model responses, resulting in a list of messages that grows longer over time. Because context windows are limited, many applications can benefit from using techniques to remove or "forget" stale information.
<Tip>
Need to remember information **across** conversations? Use [long-term memory](/oss/langchain/long-term-memory) to store and recall user-specific or application-level data across different threads and sessions.
</Tip>
## Usage
To add short-term memory (thread-level persistence) to an agent, you need to specify a `checkpointer` when creating an agent.
@@ -3,8 +3,10 @@ import { createAgent } from "langchain";
import { PostgresStore } from "@langchain/langgraph-checkpoint-postgres/store";
const DB_URI =
process.env.POSTGRES_URI ??
"postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable";
const store = PostgresStore.fromConnString(DB_URI);
await store.setup();
const agent = createAgent({
model: "claude-sonnet-4-6",
@@ -1,7 +1,7 @@
```python
from langchain.agents import create_agent
from langchain_core.runnables import Runnable
from langgraph.store.postgres import PostgresStore
from langgraph.store.postgres import PostgresStore # type: ignore[import-not-found]
DB_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable"
@@ -4,6 +4,7 @@ import { createAgent, tool, type ToolRuntime } from "langchain";
import { PostgresStore } from "@langchain/langgraph-checkpoint-postgres/store";
const DB_URI =
process.env.POSTGRES_URI ??
"postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable";
const store = PostgresStore.fromConnString(DB_URI);
await store.setup();
@@ -4,7 +4,7 @@ from dataclasses import dataclass
from langchain.agents import create_agent
from langchain.tools import ToolRuntime, tool
from langchain_core.runnables import Runnable
from langgraph.store.postgres import PostgresStore
from langgraph.store.postgres import PostgresStore # type: ignore[import-not-found]
@dataclass
@@ -7,7 +7,7 @@ from langgraph.store.memory import InMemoryStore
def embed(texts: Sequence[str]) -> list[list[float]]:
# Replace with an actual embedding function or LangChain embeddings object
return [[1.0, 2.0] * len(texts)]
return [[1.0, 2.0] for _ in texts]
# InMemoryStore saves data to an in-memory dictionary. Use a DB-backed store in production use.
@@ -6,10 +6,12 @@ const embed = (texts: string[]): number[][] => {
};
const DB_URI =
process.env.POSTGRES_URI ??
"postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable";
const store = PostgresStore.fromConnString(DB_URI, {
index: { embed, dims: 2 },
});
await store.setup();
const userId = "my-user";
const applicationContext = "chitchat";
@@ -1,9 +1,21 @@
```python
from langgraph.store.postgres import PostgresStore
from collections.abc import Sequence
from langgraph.store.base import IndexConfig
from langgraph.store.postgres import PostgresStore # type: ignore[import-not-found]
def embed(texts: Sequence[str]) -> list[list[float]]:
# Replace with an actual embedding function or LangChain embeddings object
return [[1.0, 2.0] for _ in texts]
DB_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable"
with PostgresStore.from_conn_string(DB_URI) as store:
with PostgresStore.from_conn_string(
DB_URI,
index=IndexConfig(embed=embed, dims=2), # type: ignore[arg-type]
) as store:
store.setup()
user_id = "my-user"
application_context = "chitchat"
@@ -20,5 +32,7 @@ with PostgresStore.from_conn_string(DB_URI) as store:
},
)
item = store.get(namespace, "a-memory")
items = store.search(namespace, filter={"my-key": "my-value"})
items = store.search(
namespace, filter={"my-key": "my-value"}, query="language preferences"
)
```
@@ -4,8 +4,10 @@ import { tool, createAgent, type ToolRuntime } from "langchain";
import { PostgresStore } from "@langchain/langgraph-checkpoint-postgres/store";
const DB_URI =
process.env.POSTGRES_URI ??
"postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable";
const store = PostgresStore.fromConnString(DB_URI);
await store.setup();
const contextSchema = z.object({ userId: z.string() });
@@ -5,7 +5,7 @@ from typing import Any, cast
from langchain.agents import create_agent
from langchain.tools import ToolRuntime, tool
from langchain_core.runnables import Runnable
from langgraph.store.postgres import PostgresStore
from langgraph.store.postgres import PostgresStore # type: ignore[import-not-found]
from typing_extensions import TypedDict
@@ -21,19 +21,16 @@ agent: Runnable = create_agent(
tools=[get_weather],
)
for chunk in agent.stream(
for token, metadata in agent.stream(
{"messages": [{"role": "user", "content": "What is the weather in SF?"}]},
stream_mode="messages", # [!code highlight]
version="v2", # [!code highlight]
):
if chunk["type"] == "messages": # [!code highlight]
token, metadata = chunk["data"] # [!code highlight]
if not isinstance(token, AIMessageChunk):
continue
reasoning = [b for b in token.content_blocks if b["type"] == "reasoning"]
text = [b for b in token.content_blocks if b["type"] == "text"]
if reasoning:
print(f"[thinking] {reasoning[0]['reasoning']}", end="")
if text:
print(text[0]["text"], end="")
if not isinstance(token, AIMessageChunk):
continue
reasoning = [b for b in token.content_blocks if b["type"] == "reasoning"]
text = [b for b in token.content_blocks if b["type"] == "text"]
if reasoning:
print(f"[thinking] {reasoning[0]['reasoning']}", end="")
if text:
print(text[0]["text"], end="")
```