Long Term Memory Across Conversations

Build agents with cross-thread long-term memory using PostgresStore — store user preferences, search semantically, and personalize responses across sessions.

Jun 15, 202621 min readFollow

Topics You Will Master

Understanding the difference between short-term memory (within a thread) and long-term memory (across all threads)
Setting up PostgresStore with an embedding index for semantic search using nomic-embed-text
Core store operations: store.put(), store.get(), store.search(), store.delete()
Building save_user_memory and get_user_memory tools for automatic preference storage

Long-term memory in LangGraph goes beyond conversation history. While short-term memory preserves messages within a single thread_id, long-term memory stores structured user preferences (diet, work interests, location) in a persistent store accessible from any thread. The agent retrieves relevant memories via semantic search before every response.

Diagram showing short-term memory scoped per thread while long-term memory is shared across all threads

This lesson uses PostgresStore from langgraph.store.postgres paired with OllamaEmbeddings using nomic-embed-text for 768-dimensional semantic search. Both short-term (PostgresSaver) and long-term (PostgresStore) persistence connect to the same PostgreSQL database.

Prerequisites: langgraph, langchain-ollama, langgraph-checkpoint-postgres, psycopg, python-dotenv installed. Ollama running with qwen3 and nomic-embed-text. A PostgreSQL database (e.g. Neon) with the connection string in POSTGRESQL_URL.

BASH
pip install -U langgraph langchain-ollama langgraph-checkpoint-postgres psycopg python-dotenv
ollama pull qwen3
ollama pull nomic-embed-text

LangGraph & Ollama — AI Agent Development

Build production-ready AI agents with persistent memory, tool calling, and multi-model workflows using LangGraph and local LLMs.

Enroll on Udemy →

Setup

PYTHON
from dotenv import load_dotenv

load_dotenv()
OUTPUT
True
PYTHON
from typing_extensions import TypedDict, Annotated
import operator
from langgraph.graph import StateGraph, START, END

# short term memory persistence
from langgraph.checkpoint.sqlite import SqliteSaver
from langgraph.checkpoint.postgres import PostgresSaver

# long term memory persistence
from langgraph.store.postgres import PostgresStore
from langgraph.store.sqlite import SqliteStore

from langchain_ollama import ChatOllama, OllamaEmbeddings
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_core.tools import tool
import psycopg
import os

# Configuration
BASE_URL = "http://localhost:11434"
MODEL_NAME = "qwen3"
EMBEDDING_MODEL = "nomic-embed-text"

llm = ChatOllama(model=MODEL_NAME, base_url=BASE_URL)

Store and Checkpointer Setup

Create an embedding function for semantic search. The PostgresStore uses this to index stored memories as 768-dimensional vectors:

PYTHON
embeddings = OllamaEmbeddings(model=EMBEDDING_MODEL, base_url=BASE_URL)

def embed_texts(texts: list[str]) -> list[list[float]]:

    return embeddings.embed_documents(texts)

db_url = os.getenv("POSTGRESQL_URL")

checkpointer_conn = psycopg.connect(db_url, autocommit=True, prepare_threshold=0)
checkpointer = PostgresSaver(checkpointer_conn)

store_conn = psycopg.connect(db_url, autocommit=True, prepare_threshold=0)
store = PostgresStore(store_conn, index = {'embed': embed_texts, 'dims': 768})


# first time setup
checkpointer.setup()
store.setup()

Important

checkpointer.setup() and store.setup() create the required database tables. Run these once on first use — they are safe to call again on subsequent runs.

Two separate psycopg connections are required: one for the checkpointer (short-term memory) and one for the store (long-term memory). Both connect to the same PostgreSQL database.


Memory Management Tools

Direct Store Operations

The store supports four core operations: put(), get(), search(), and delete(). Each memory is organized by a namespace tuple (e.g. (user_id, "preferences")) and a key (e.g. "food", "work"):

Diagram showing memories organized by namespace and key with the put, get, search, and delete operations

PYTHON
user_id = "demo-user"
namespace = (user_id, "preferences")

store.put(namespace, "food", {"diet": "veg", 
                              "likes": ["pasta", "pizza", "veggies"]})


store.put(namespace, "color", {"favorite": "blue", 
                               "dislike": "brown"})

store.put(namespace, "work", {
    "role": "Data Scientist",
    "interests": ["machine learning", "ai", "gen ai", "agents"]
})

Retrieve a specific memory by key:

PYTHON
store.get(namespace, "color")
OUTPUT
Item(namespace=['demo-user', 'preferences'], key='color', value={'dislike': 'brown', 'favorite': 'blue'}, created_at='2025-11-06T17:54:06.587068+00:00', updated_at='2025-11-06T17:54:06.587068+00:00')

Delete a memory:

PYTHON
store.delete(namespace, "color")

store.search() finds the most relevant memories by embedding the query and comparing against stored memory vectors:

Diagram showing a query embedded and matched against stored memory vectors by similarity score

PYTHON
query = "What does the user like to eat?"
results = store.search(namespace, query=query, limit=1)
results
OUTPUT
[Item(namespace=['demo-user', 'preferences'], key='food', value={'diet': 'veg', 'likes': ['pasta', 'pizza', 'veggies']}, created_at='2025-11-06T09:46:57.726747+00:00', updated_at='2025-11-06T17:54:06.276885+00:00', score=0.5786444826046763)]

The search correctly retrieved the food memory with a similarity score of 0.58 — even though the query did not use the exact key name. This is the power of semantic search: the embedding model understands that "like to eat" relates to "diet" and "likes" in the stored memory.

PYTHON
query = "What does the user like in colors?"
results = store.search(namespace, query=query, limit=1)
results
OUTPUT
[Item(namespace=['demo-user', 'preferences'], key='food', value={'diet': 'veg', 'likes': ['pasta', 'pizza', 'veggies']}, created_at='2025-11-06T09:46:57.726747+00:00', updated_at='2025-11-06T17:54:06.276885+00:00', score=0.42101958236507275)]

Since the color memory was deleted earlier, the search falls back to the closest match (food) with a lower score of 0.42. In a production system, you would filter results by a minimum score threshold.


State Definition

The state extends the standard message list with a user_id field — this identifies which user's memories to access:

PYTHON
class AgentState(TypedDict):
    messages: Annotated[list, operator.add]
    user_id: str

Memory Tools for the Agent

Two tools give the agent the ability to save and retrieve long-term memories during conversations:

save_user_memory

PYTHON
@tool
def save_user_memory(user_id:str, category:str, information:dict) -> str:
    """
    Save user preference or information to long-term memory.

    Args:
        user_id: User identifier
        category: Category of information (e.g., 'food', 'work', 'hobbies', 'schedule', 'location')
        information: Dictionary containing the information to save
    """

    namespace = (user_id, "preferences")

    store.put(namespace, category, information)

    return f"Saved {category} preferences."

get_user_memory

PYTHON
@tool
def get_user_memory(user_id:str, category:str) -> str:
    """
        Retrieve user preference or information from long-term memory.

        Args:
            user_id: User identifier
            category: Category of information to retrieve (e.g., 'food', 'work', 'hobbies')
        """
    
    namespace = (user_id, "preferences")

    item = store.get(namespace, category)

    if item:
        return f"{category}: {item.value}"
    else:
        return f"No '{category}' information found!"

Test the retrieval tool:

PYTHON
get_user_memory.invoke({"user_id": 'demo-user', "category": 'color'})
OUTPUT
"No 'color' information found!"
PYTHON
get_user_memory.invoke({"user_id": 'demo-user', "category": 'something'})
OUTPUT
"No 'something' information found!"

Agent with Automatic Memory Retrieval

Utility Tools

Load the standard tools from the ReAct Agent with Tools lesson:

PYTHON
import sys
sys.path.append("../05. LangGraph ReAct Agent with Tools")

import my_tools

my_tools.calculate.invoke({'expression': '2+2*1.4/23-34'})

all_tools = [my_tools.get_weather, my_tools.calculate]
OUTPUT
[TOOL] calculate ('2+2*1.4/23-34') -> '-31.878260869565217'

Agent Node with Memory Context

The agent node performs automatic memory retrieval before every response. It uses store.search() to find the top 3 most relevant memories based on the user's latest message, then injects them into the system prompt:

Diagram showing the agent searching memories and injecting them into the prompt before replying

PYTHON
def agent_node(state: AgentState):

    store_conn = psycopg.connect(db_url, autocommit=True, prepare_threshold=0)
    store = PostgresStore(store_conn, index = {'embed': embed_texts, 'dims': 768})

    user_id = state.get("user_id", "unknown")
    namespace = (user_id, "preferences")

    last_message = state['messages'][-1].content
    memories = store.search(namespace, query=last_message, limit=3)

    # build context memory for personalized answer
    context_line = []
    for mem in memories:
        text = f" -{mem.key}: {mem.value}"
        context_line.append(text)

    memory_text = "\n\n".join(context_line) if context_line else "No user preferences stored yet!"

    print(f"User Memory Retrieved: \n{memory_text}\n")

    tools = all_tools + [save_user_memory, get_user_memory]

    llm_with_tools = llm.bind_tools(tools)

    system_prompt = SystemMessage(f"""
                        You are a helpful assistant with long-term memory capabilities and access to utility tools.

                            User ID: {user_id}
                            Current User Memories:
                            {memory_text}

                            MEMORY TOOLS USAGE:

                            1. save_user_memory: Use when user shares NEW information
                            - Always pass user_id: "{user_id}"
                            - Food preferences (diet, likes, dislikes, allergies)
                            - Work information (role, company, interests)
                            - Hobbies and activities
                            - Schedule and availability
                            - Location and timezone

                            2. get_user_memory: Use when you need to recall specific category
                            - Always pass user_id: "{user_id}"
                            - When answering questions about past preferences
                            - When user asks "what do you know about me?"
                            - When making recommendations based on preferences

                            UTILITY TOOLS USAGE:

                            3. get_weather: Use to retrieve current weather information
                            - Pass location as parameter (city name, zip code, or coordinates)
                            - Use when user asks about weather conditions
                            - Use when planning activities that depend on weather

                            4. calculate: Use to perform mathematical calculations
                            - Pass mathematical expression as string parameter
                            - Supports basic arithmetic (+, -, *, /)
                            - Supports advanced operations (powers, roots, trigonometry)
                            - Use when user needs numerical computations

                            GUIDELINES:
                            - Always save when user shares personal information
                            - Retrieve specific categories when needed for context
                            - Use semantic search results shown above for general context
                            - Use get_weather when location-based weather info is needed
                            - Use calculate for any mathematical operations or conversions
                            - Be conversational and natural when using all tools
                            - Combine tools when appropriate (e.g., weather + saved location preference)
                        """)
    
    messages = [system_prompt] + state['messages']

    response = llm_with_tools.invoke(messages)

    if hasattr(response, 'tool_calls') and response.tool_calls:
        for tc in response.tool_calls:
            print(f"[AGENT] called Tool {tc.get('name', '?')} with args {tc.get('args', '?')}")
    else:
        print(f"[AGENT] Responding...")


    return {'messages': [response]}

Routing and Graph

PYTHON
def should_continue(state: AgentState):
    last = state['messages'][-1]
    
    if hasattr(last, 'tool_calls') and last.tool_calls:
        return "tools"
    else:
        return END
PYTHON
from langgraph.prebuilt import ToolNode

tools = all_tools + [save_user_memory, get_user_memory]

def create_agent():

    builder = StateGraph(AgentState)

    builder.add_node("agent", agent_node)
    builder.add_node("tools", ToolNode(tools))


    builder.add_edge(START, "agent")
    builder.add_conditional_edges("agent", should_continue, ["tools", END])

    builder.add_edge("tools", "agent")

    checkpointer_conn = psycopg.connect(db_url, autocommit=True, prepare_threshold=0)
    checkpointer = PostgresSaver(checkpointer_conn)
    graph = builder.compile(checkpointer=checkpointer)

    return graph
PYTHON
agent = create_agent()
agent
OUTPUT
<langgraph.graph.state.CompiledStateGraph object at 0x0000012749BCFAA0>

End-to-End Demo

Sharing New Information

PYTHON
user_id = "demo-user"
config = {'configurable': {'thread_id': f"{user_id}_longterm"}}

query = "I am John. I love AI and machine learning in python."

result = agent.invoke({'messages': [HumanMessage(query)],
                       'user_id': user_id}, config=config)
OUTPUT
User Memory Retrieved: 
 -work: {'role': 'Data Scientist', 'interests': ['machine learning', 'ai', 'gen ai', 'agents']}

 -food: {'diet': 'veg', 'likes': ['pasta', 'pizza', 'veggies']}

[AGENT] Responding...
PYTHON
result['messages'][-1].pretty_print()
OUTPUT
================================== Ai Message ==================================

It seems you're introducing yourself! However, the user ID associated with your previous interactions is "demo-user". If you'd like to update your profile or share new information (e.g., work details, food preferences, etc.), let me know! For now, I'll continue using "demo-user" as your identifier. How can I assist you today with AI, machine learning, or Python? 😊

The agent retrieved existing memories (work and food preferences) and used them as context for its response.

Personalized Recommendations

PYTHON
user_id = "demo-user"
config = {'configurable': {'thread_id': f"{user_id}_longterm"}}

query = "Could you please suggest me some diet plan?"

result = agent.invoke({'messages': [HumanMessage(query)],
                       'user_id': user_id}, config=config)

result['messages'][-1].pretty_print()
OUTPUT
User Memory Retrieved: 
 -food: {'diet': 'veg', 'likes': ['pasta', 'pizza', 'veggies']}

 -work: {'role': 'Data Scientist', 'interests': ['machine learning', 'ai', 'gen ai', 'agents']}

[AGENT] called Tool get_user_memory with args {'category': 'food', 'user_id': 'demo-user'}
User Memory Retrieved: 
 -food: {'diet': 'veg', 'likes': ['pasta', 'pizza', 'veggies']}

 -work: {'role': 'Data Scientist', 'interests': ['machine learning', 'ai', 'gen ai', 'agents']}

[AGENT] Responding...
================================== Ai Message ==================================

Here's a vegetarian diet plan tailored to your preferences (veg diet, loves pasta, pizza, and veggies):

### **Breakfast Ideas**  
- **Whole grain pasta with tomato sauce** + a side of steamed veggies (e.g., broccoli, carrots).  
- **Oatmeal** topped with fresh fruits (e.g., berries, banana) and a sprinkle of nuts.  
- **Veggie omelet** (using spinach, mushrooms, and bell peppers) with whole wheat toast.  

### **Lunch Options**  
- **Caprese salad** (tomato, mozzarella, basil) with a slice of whole grain bread.  
- **Mixed veggie stir-fry** (bell peppers, snap peas, zucchini) with tofu or chickpeas.  
- **Pasta salad** with cherry tomatoes, cucumbers, olives, and a lemon-herb dressing.  

### **Dinner Suggestions**  
- **Vegetable curry** (coconut milk base with spinach, cauliflower, and sweet potatoes) served over brown rice.  
- **Stuffed bell peppers** filled with quinoa, black beans, and diced tomatoes.  
- **Pasta with marinara sauce** and a side of garlic bread.  

Let me know if you'd like help customizing this plan for specific goals (e.g., weight loss, energy boost)!

The agent used the stored food preferences to generate a personalized vegetarian diet plan featuring pasta, pizza, and veggies — exactly matching the user's saved preferences.

Cross-Thread Memory Persistence

The critical test: using a different thread_id but the same user_id. Long-term memory should still be available:

PYTHON
user_id = "demo-user"
config = {'configurable': {'thread_id': f"{user_id}_this_is_another_test"}}

query = "Could you please suggest me some diet plan?"

result = agent.invoke({'messages': [HumanMessage(query)],
                       'user_id': user_id}, config=config)

result['messages'][-1].pretty_print()
OUTPUT
User Memory Retrieved: 
 -food: {'diet': 'veg', 'likes': ['pasta', 'pizza', 'veggies']}

 -work: {'role': 'Data Scientist', 'interests': ['machine learning', 'ai', 'gen ai', 'agents']}

[AGENT] called Tool get_user_memory with args {'category': 'food', 'user_id': 'demo-user'}
User Memory Retrieved: 
 -food: {'diet': 'veg', 'likes': ['pasta', 'pizza', 'veggies']}

 -work: {'role': 'Data Scientist', 'interests': ['machine learning', 'ai', 'gen ai', 'agents']}

[AGENT] Responding...
================================== Ai Message ==================================

Here's a vegetarian diet plan incorporating your preferences for pasta, pizza, and veggies:

**Breakfast Ideas:**
- Veggie omelet with whole grain toast (spinach, tomatoes, onions)
- Pasta breakfast scramble (whole wheat pasta with veggies and vegan cheese)
- Fruit smoothie with spinach or frozen berries

**Lunch Options:**
- Veggie pizza with whole wheat crust, tomato sauce, and your favorite toppings
- Quinoa salad with roasted vegetables, chickpeas, and a lemon-tahini dressing
- Stuffed bell peppers filled with rice, beans, and cheese

**Dinner Suggestions:**
- Pasta primavera with fresh veggies and light garlic sauce
- Veggie stir-fry with tofu or tempeh over brown rice
- Baked pizza with a whole grain crust, tomato sauce, and a mix of veggies

**Tips:**
1. Stay hydrated with water or herbal teas
2. Include a variety of colorful vegetables for nutrients
3. Use whole grains as base for meals
4. Experiment with different herbs and spices for flavor

Would you like specific recipes or meal prep ideas?

Even though this is a completely new thread (_this_is_another_test), the agent still retrieved the user's food and work preferences from long-term memory. This is the core distinction between short-term and long-term memory:

Memory Type Scope Storage Access Pattern
Short-term Per-thread conversation history PostgresSaver / SqliteSaver Same thread_id only
Long-term Cross-thread user preferences PostgresStore Same user_id, any thread_id

What You Built

In this lesson you built an agent with both short-term and long-term memory:

  • PostgresStore — persistent key-value store with semantic search via nomic-embed-text embeddings
  • Core operationsstore.put(), store.get(), store.search(), store.delete() for memory CRUD
  • Memory toolssave_user_memory and get_user_memory tools that the agent calls autonomously
  • Automatic retrieval — the agent node searches memories before every response, injecting relevant context into the system prompt
  • Cross-thread persistence — user preferences are accessible from any conversation thread, not just the one where they were saved
  • Dual persistencePostgresSaver for short-term conversation state + PostgresStore for long-term user preferences, both in the same PostgreSQL database

Found this useful? Keep building with me.

New tutorials every week on YouTube — or go deeper with a full structured course.

Find this tutorial useful?

Subscribe to our YouTube channels for more practical production walk-throughs.

Discussion & Comments