Agents & Orchestration

Long-Term Memory with LangChain and Postgres

Add durable, Postgres-backed user memory to a LangGraph agent so it recalls preferences across threads and restarts, with list, summary, and delete endpoints.

Build it with an agent

Paste into Claude Code, Cursor, or any coding agent — it builds the recipe for you.

Clone `https://github.com/nebius/nebius-partner-cookbook` into an empty directory and work inside `cookbooks/06-long-term-memory-langchain-postgres`. Read its `README.md` before running anything.

Install the dependencies with `uv sync`. Read the Nebius API key from `NEBIUS_API_KEY` in the environment, or from a `.env` file in that directory if the variable is not set there. If neither is present, stop and ask the user for their Nebius API key before continuing. Do not search elsewhere on the machine for a key, and never print or commit it. This recipe needs no other partner keys.

Start local Postgres with `docker compose up -d postgres` and wait for its healthcheck to pass.

Run the offline test suite first, before starting the service: `make test`. These tests set `MEMORY_BACKEND=memory` and mock Nebius with `respx`, so they need neither Postgres nor network access. Report whether they pass.

Then start the service with `make dev`. In a second terminal, store a durable preference with:

```bash
curl -N -X POST http://localhost:8000/agent/run \
  -H 'content-type: application/json' \
  -d '{"thread_id":"thread-a","user_id":"reader-42","prompt":"Remember that I prefer concise science fiction recommendations."}'
```

Then, from a different thread, ask the agent to use that memory:

```bash
curl -N -X POST http://localhost:8000/agent/run \
  -H 'content-type: application/json' \
  -d '{"thread_id":"thread-b","user_id":"reader-42","prompt":"Recommend one book for me."}'
```

Also call `curl http://localhost:8000/memory/reader-42/summary` to confirm the stored memory is listed.

Report what you observed: whether the tests passed, whether the service started against Postgres, the SSE status events from both `agent/run` calls (in particular the `memory_loaded` and `longTermMemories` fields), and the summary endpoint's response.

Before starting the service, set `NEBIUS_BASE_URL=https://api.tokenfactory.nebius.com/v1/` and `NEBIUS_MODEL=Qwen/Qwen3.5-397B-A17B` in `.env`; the repository's `.env.example` still carries the older `api.studio.nebius.ai` hostname and a chat model that Token Factory no longer serves.

Recipe

Persist user preferences and facts across conversation threads with a Postgres-backed memory layer.

This is recipe 6 in a ten-part Agent Blueprint Recipes series that runs Foundation, Knowledge, Grounding, Orchestration, Thread Memory, User Memory, Observability, Guardrails, Actions, and Simulation in sequence. This recipe keeps the progression intact. It still uses the LangGraph orchestration from cookbook 4 and the short-term thread_id memory from cookbook 5, and adds durable user_id memory backed by Postgres so the agent can remember useful facts across new threads and process restarts.

The demo stays intentionally explicit. The request body includes user_id because this cookbook has no auth layer. In production, derive user_id from authenticated identity and ignore any user identifier supplied by the client.

What you will build

A FastAPI service that extends the orchestrated, thread-aware book agent:

  1. Inherited orchestration. The direct / deliberate LangGraph route still chooses the response path.
  2. Inherited thread memory. thread_id still loads and saves recent conversation turns.
  3. Postgres-backed user memory. Long-term facts and preferences survive process restarts.
  4. Bounded recall. A small set of relevant memories is injected before the model writes.
  5. Privacy operations. List, summarize, and delete endpoints let users inspect and remove stored context.
  6. Network-free tests. Tests use an in-memory backend and respx, so CI never calls Nebius or Postgres by default.
request ──► thread_id ──► short-term checkpoint ──┐
        └─► user_id ─────► Postgres memory store ─┼─► LangGraph route ──► Nebius stream
                                                    └─► bounded recall

Prerequisites

  • Python 3.12+
  • uv
  • Docker for local Postgres
  • A Nebius API key from the Nebius console

Run the cookbook

cp .env.example .env
# Open .env and fill NEBIUS_API_KEY.

While .env is open, replace the two Nebius defaults. The repo’s .env.example still ships the older api.studio.nebius.ai hostname and a chat model that Token Factory no longer serves, so the first request fails without this:

NEBIUS_BASE_URL=https://api.tokenfactory.nebius.com/v1/
NEBIUS_MODEL=Qwen/Qwen3.5-397B-A17B

Then bring up Postgres and start the app:

docker compose up -d postgres
uv sync
make dev

In another terminal, store a durable user preference:

curl -N -X POST http://localhost:8000/agent/run \
  -H 'content-type: application/json' \
  -d '{"thread_id":"thread-a","user_id":"reader-42","prompt":"Remember that I prefer concise science fiction recommendations."}'

Then ask from a different thread:

curl -N -X POST http://localhost:8000/agent/run \
  -H 'content-type: application/json' \
  -d '{"thread_id":"thread-b","user_id":"reader-42","prompt":"Recommend one book for me."}'

The SSE stream includes memory status events:

event: status
data: {"phase":"memory_loaded","threadId":"thread-b","userId":"reader-42","messages":0,"longTermMemories":1}

event: status
data: {"phase":"routed","route":"deliberate","contextNeed":"curated_recommendation"}

event: token
data: {"text":"..."}

Walk-through

API contract

POST /agent/run

Runs the agent and streams named SSE events.

Request:

{
  "thread_id": "thread-b",
  "user_id": "reader-42",
  "prompt": "Recommend one book for me.",
  "temperature": 0.4,
  "max_tokens": 1024,
  "history": []
}

Important request fields:

Field Required Purpose
thread_id yes Short-term conversation continuity.
user_id yes Long-term memory namespace for this demo.
prompt yes Current user request.
history no Optional client-provided context; server-side memory is loaded separately.

SSE events:

Event Meaning
status Phase transitions such as memory_loaded, routed, writing, and memory_saved.
token Nebius token deltas plus the final usage footer.
done Final usage payload.
error Recoverable API-level failure.
heartbeat Long-running connection heartbeat.

Memory endpoints

Method Path Purpose
GET /memory/{user_id} List stored long-term memories.
GET /memory/{user_id}/summary Summarize what the agent knows about the user.
DELETE /memory/{user_id} Delete stored long-term memories for privacy or account deletion.
DELETE /threads/{thread_id} Delete process-local short-term thread memory.

Example memory summary:

curl http://localhost:8000/memory/reader-42/summary

Configuration

Every setting is validated by Pydantic at boot. Copy .env.example and keep secrets out of code.

Env var Default Purpose
NEBIUS_API_KEY required Nebius API key.
NEBIUS_BASE_URL https://api.tokenfactory.nebius.com/v1/ OpenAI-compatible Nebius Token Factory endpoint. The repo’s .env.example still ships the older api.studio.nebius.ai value; override it.
NEBIUS_MODEL Qwen/Qwen3.5-397B-A17B Chat model. The repo’s .env.example still ships a model that Token Factory no longer serves; override it.
MEMORY_BACKEND postgres postgres for local/prod, memory for tests.
POSTGRESQL_ADDON_URI local Postgres URL Postgres connection string. Clever Cloud injects this when a Postgres add-on is linked.
LONG_TERM_MEMORY_LIMIT 5 Maximum memories recalled or listed.
CORS_ORIGINS http://localhost:3000 Browser allowlist.
LOG_LEVEL info Structured logging level.

Sharing one Postgres database

Postgres schemas let multiple cookbooks share one database without sharing tables. Think of a schema as a folder inside the database: cookbook 6 writes to dev_cbk_06.user_memories locally and prod_cbk_06.user_memories in production. The schema name is built from ENV and the cookbook number, so there is no extra environment variable to maintain. Cookbooks 7 through 10 inherit the same pattern with their own schemas, for example prod_cbk_07, prod_cbk_08, prod_cbk_09, and prod_cbk_10.

That gives you one managed Postgres instance, one connection URI, and isolated tables per cookbook. The app creates the configured schema and user_memories table on first use. For local development, keep ENV=development, which produces the dev_cbk_NN schemas. For Clever Cloud, link the same Postgres add-on to each cookbook app. Clever injects POSTGRESQL_ADDON_URI, which the app uses as its only Postgres connection string. Set ENV=production and the app automatically uses prod_cbk_06 for this cookbook.

Implementation notes

Long-term memory lives in app/core/long_term_memory.py. The production backend creates a schema-qualified user_memories table and indexes records by user_id and created_at. The test backend implements the same contract in memory.

Recall is bounded on purpose. The agent never injects the full memory store into a prompt. The route asks the long-term store for relevant memories, converts them into synthetic context, and then lets the inherited LangGraph route decide whether the prompt is direct or deliberate.

Memory extraction is deterministic in this cookbook. It stores explicit user facts such as “remember that…” and “my favorite author is…”. A production system can replace this with a model-assisted memory writer, and should keep the same privacy and deletion contracts.

Design decisions

  • Derive user_id from auth instead of trusting request JSON.
  • Use a managed Postgres instance with backups, migrations, and connection pooling.
  • Keep ENV=production in deployed cookbook apps so memory lands in prod_cbk_NN schemas.
  • Add row-level tenancy controls if multiple customers share the same database.
  • Decide retention windows for user memories and checkpoints.
  • Keep deletion paths tested because privacy workflows are product behavior, not admin utilities.
  • Review stored memories for PII before expanding extraction beyond explicit user preferences.

Failure modes

Symptom Likely cause Handling
Agent forgets within one thread Unstable thread_id Keep thread_id stable per conversation and expose reset explicitly.
Agent forgets across threads Missing or wrong user_id Derive user_id from authenticated identity in production.
Memory leaks between users Client-controlled user_id in a real app Never trust request JSON for identity outside this demo.
Token usage grows Too many recalled memories Lower LONG_TERM_MEMORY_LIMIT or summarize memories.
Stale preferences affect answers User changed their mind Prefer newer memories and expose delete and edit flows.

Test it

uv run pytest
uv run ruff check
uv run ruff format --check

The tests mock Nebius with respx. They use MEMORY_BACKEND=memory, so they do not require Postgres or network access.

Ship it

Going further

  • Replace deterministic extraction with a dedicated memory-writer node.
  • Store memory provenance so users can see which prompt created a memory.
  • Add edit endpoints for user-corrected memories.
  • Move thread checkpoints to Postgres alongside long-term memory.
  • Continue to cookbook 7, Observability with LangSmith, to trace the full memory path.
  • Read the LangChain long-term memory docs: docs.langchain.com/oss/python/langchain/long-term-memory.