mirror of
https://github.com/langchain-ai/docs.git
synced 2026-07-20 00:46:14 -04:00
feat: improve long-term memory docs and discoverability (#2957)
This commit is contained in:
@@ -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..."
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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:
|
||||
Generated
+208
-2
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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="")
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user