Use with LangChain

Add verified conversation persistence to a LangChain 1.x create_agent agent with Neo4jMemoryMiddleware. The middleware owns message writes in this recipe, so the application does not save the same turn a second time.

1. Prepare the selected environment

Use Python 3.10+ and a POSIX shell. The recipe uses BoltSettings to select Neo4j explicitly and disables entity extraction so that message persistence can be checked independently. Use a dedicated AuraDB instance with vector-index support and no incompatible existing vectors. Follow the Aura connection setup and copy its connection values into the exports below. These recipes use the published Python 0.7.0 package; provider access and database execution must be verified in your environment.

Create a local folder and virtual environment for the examples on this page. The commands reuse an existing environment without changing its files:

mkdir -p ~/agent-memory-tutorials
cd ~/agent-memory-tutorials
if [ -e .venv ]; then
  printf '%s\n' 'Using the existing virtual environment.'
else
  python3 -m venv .venv
fi
source .venv/bin/activate

Expected: ~/agent-memory-tutorials is your working directory and its virtual environment is active. Install the published SDK with the command below.

Each complete code block labelled Save as names a file to create in this folder using your editor. Copy the entire block, including imports and the entry point. Expand each helper disclosure and use Copy code to copy its full source. Keep all files together so their imports resolve.

When continuing from another tutorial or guide, retain the existing environment, configuration, session files, and .tutorial-state/. Reuse unchanged helper files; compare an existing file before replacing it, and finish any pending cleanup or recovery before changing the code that owns its state.

python -m pip install 'neo4j-agent-memory[langchain-agents,openai]==0.7.0' langchain-openai
export NEO4J_URI='neo4j+s://<instance-id>.databases.neo4j.io'
export NEO4J_USERNAME='neo4j'
export NEO4J_PASSWORD='replace-with-your-Aura-password'
export NEO4J_DATABASE='neo4j'
export OPENAI_API_KEY='replace-with-your-provider-key'
export OPENAI_MODEL='replace-with-an-available-model-id'

The OpenAI-backed recipes require access to text-embedding-3-small; it produces 1536-dimensional vectors in this configuration. Configure the selected chat model separately where required. Credentials are read from the environment, never printed by the programs.

2. Read the complete recipe

Create the subdirectory below, then save both complete files under the displayed names. Run commands from agent-memory-tutorials/; Python resolves common from the script’s integrations/ directory.

mkdir -p integrations

The script imports this shared helper from the same directory. It supplies explicit database settings, closes clients through the calling context manager, and fails when expected records are absent. Task traces record the observable outcome of the call; they do not expose hidden model reasoning.

Complete integrations/common.py
Save as integrations/common.py
"""Shared Aura connection through Bolt and readback for integration recipes."""

import os


def settings(embedding=None):
    from neo4j_agent_memory import BoltSettings
    from neo4j_agent_memory.llm import from_provider

    return BoltSettings(
        neo4j={
            "uri": os.environ["NEO4J_URI"],
            "username": os.environ["NEO4J_USERNAME"],
            "password": os.environ["NEO4J_PASSWORD"],
            "database": os.getenv("NEO4J_DATABASE", "neo4j"),
        },
        embedding=embedding or from_provider("openai/text-embedding-3-small", kind="embedding"),
        extraction={"extractor_type": "none"},
    )


async def verify_messages(client, session_id, expected):
    conversation = await client.short_term.get_conversation(session_id)
    contents = [message.content for message in conversation.messages]
    if not all(text in contents for text in expected):
        raise RuntimeError(f"Message readback failed for {session_id}")
    print(f"Verified stored messages; session={session_id}")


async def recorded_turn(client, session_id, prompt, respond):
    """Record a task outcome; this does not claim to capture hidden model reasoning."""
    trace = await client.reasoning.start_trace(session_id=session_id, task=prompt)
    try:
        await client.short_term.add_message(session_id, "user", prompt, extract_entities=False)
        context = await client.get_context(prompt, session_id=session_id)
        reply = str(await respond(context))
        if not reply.strip():
            raise RuntimeError("The framework returned no text")
        await client.short_term.add_message(session_id, "assistant", reply, extract_entities=False)
        await verify_messages(client, session_id, [prompt, reply])
    except Exception as exc:
        await client.reasoning.complete_trace(
            trace.id, success=False, outcome=f"Run failed: {type(exc).__name__}"
        )
        raise
    await client.reasoning.complete_trace(
        trace.id, success=True, outcome="Reply stored and read back"
    )
    stored = await client.reasoning.get_trace_with_steps(trace.id)
    if stored is None or stored.success is not True:
        raise RuntimeError(f"Trace readback failed for {trace.id}")
    print(f"Verified task trace: {trace.id}")
    return reply
Complete integrations/langchain_recipe.py
Save as integrations/langchain_recipe.py
"""Use the LangChain 1.x middleware as the sole writer of conversation turns."""

import asyncio
import os
from uuid import uuid4

from common import settings, verify_messages


async def exercise(client, model):
    from langchain.agents import create_agent

    from neo4j_agent_memory.integrations.langchain import Neo4jMemoryMiddleware

    session_id = f"docs-langchain-{uuid4().hex[:8]}"
    prompt = "Suggest a simple project planning checklist."
    agent = create_agent(
        model,
        tools=[],
        system_prompt="Answer briefly using the retrieved context.",
        middleware=[Neo4jMemoryMiddleware(client, session_id=session_id, extract_entities=False)],
    )
    result = await agent.ainvoke({"messages": [("user", prompt)]})
    reply = result["messages"][-1].content
    if not reply:
        raise RuntimeError("LangChain returned an empty assistant message")
    await verify_messages(client, session_id, [prompt, reply])
    return reply


async def main():
    from langchain_openai import ChatOpenAI

    from neo4j_agent_memory import MemoryClient

    async with MemoryClient(settings()) as client:
        print(await exercise(client, ChatOpenAI(model=os.environ["OPENAI_MODEL"])))


if __name__ == "__main__":
    asyncio.run(main())

3. Run and verify persistence

python integrations/langchain_recipe.py

Expected: Verified stored messages followed by the response. abefore_model persists the user input; awrap_model_call injects a retrieved memory block into the model request; aafter_model persists the reply. A failed model request can leave the user input stored without a reply. The async exception is allowed to propagate.

Troubleshooting and cleanup

Stop when the program raises an exception; a printed model response alone does not verify persistence. Check the configured database, provider access and vector dimensions before retrying. The program prints the session or trace IDs needed for inspection and closes the client. Retained example records remain in the dedicated database; follow the Aura tutorial’s cleanup procedure for that dedicated instance only when you no longer need them. Reusing a session groups records; it does not authorize a user to read them.

Adapt the integration

Use agent.ainvoke(…​) with this middleware: its persistence and context hooks are asynchronous. Middleware options lists its settings, including store_messages=False for a read-only agent. The memory block is appended to the request’s system message, not repeatedly accumulated in graph state. The middleware tracks message IDs to avoid repeating the same write within its instance; this is not a durable cross-process exactly-once guarantee. The middleware does not select a chat model itself; pass your own configured model as the first argument to create_agent, for example create_agent(ChatOpenAI(model=os.environ["OPENAI_MODEL"]), tools=[…​], middleware=[Neo4jMemoryMiddleware(client, session_id=session_id)]).

For a RunnableWithMessageHistory application, Neo4jAgentMemory implements BaseChatMessageHistory; see Chat history for a chain. Use aget_messages() / aadd_messages() / aclear() in an async context. That adapter’s aget_messages() is a framework method; the underlying library readback is (await client.short_term.get_conversation(session_id)).messages. Do not assume a LangGraph checkpointer and Neo4j transcript memory are the same persistence layer.

Neo4jMemoryRetriever implements BaseRetriever for memory RAG and can be invoked asynchronously; see Retriever fields. Keep retrieval limits and the prompt’s context budget explicit. To add a tool that reads the graph, wrap a memory call in LangChain’s @tool decorator and register it on the agent:

from langchain_core.tools import tool


@tool
async def search_entities(query: str) -> list[str]:
    """Search the knowledge graph for entities matching the query."""
    return [e.name for e in await client.long_term.search_entities(query)]

LangChain uses the docstring as the tool description; a @tool function with neither a docstring nor description= raises ValueError. Then pass it as create_agent(model, tools=[search_entities], …​).

llm_provider_from_langchain(model) configures an extraction provider separately; see Configure providers.

This repository supports the 1.x packages declared in pyproject.toml. The old ConversationChain, AgentExecutor and BaseMemory recipes are not the selected path. See the maintained LangChain example and the separate hosted example for application assembly. Financial/product data loaders and domain tools are application-specific.

Adapter reference

This section lists the adapter options of the published 0.7.0 package. Import the classes from neo4j_agent_memory.integrations.langchain.

Middleware options

Neo4jMemoryMiddleware(client, session_id, …​) takes these keyword-only options:

Option Default Effect

include_short_term

True

Include conversation history in the injected block.

include_long_term

True

Include matching preferences and entities.

include_reasoning

True

Include similar past traces. Leave it False until the application records traces.

max_items

10

Cap for each memory layer.

store_messages

True

Persist user and assistant messages. Set False for a read-only agent.

extract_entities

True

Run entity extraction on persisted messages.

context_header

"# Memory"

Heading placed above the injected block.

Only the async hooks are implemented. The synchronous before_model and after_model hooks raise NotImplementedError, so call the agent with ainvoke or astream.

Chat history for a chain

Neo4jAgentMemory(memory_client, session_id, …​) is a BaseChatMessageHistory, so it plugs into RunnableWithMessageHistory for chains that are not agents. This fragment assumes a connected client and a configured chat model:

from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables.history import RunnableWithMessageHistory

from neo4j_agent_memory.integrations.langchain import Neo4jAgentMemory

prompt = ChatPromptTemplate.from_messages(
    [
        ("system", "You are a helpful assistant."),
        MessagesPlaceholder("history"),
        ("human", "{input}"),
    ]
)
chain_with_history = RunnableWithMessageHistory(
    prompt | model | StrOutputParser(),
    lambda session_id: Neo4jAgentMemory(memory_client=client, session_id=session_id),
    input_messages_key="input",
    history_messages_key="history",
)
answer = await chain_with_history.ainvoke(
    {"input": "I'm looking for running shoes"},
    config={"configurable": {"session_id": "user-123"}},
)

RunnableWithMessageHistory reads the history with aget_messages() and writes the new turn with aadd_messages(). LangChain deprecated RunnableWithMessageHistory in langchain-core 1.3.3 and plans to remove it in 2.0. It still works across the supported 1.x range but emits a LangChainDeprecationWarning; for new agent code, prefer Neo4jMemoryMiddleware with create_agent. Neo4jAgentMemory also accepts include_short_term, include_long_term, include_reasoning (all True), max_messages (10), max_preferences (5), max_traces (3), extract_entities and generate_embeddings (both True). Its aload_memory_variables({"input": query}) returns history, context, preferences and similar_tasks for a custom prompt.

Retriever fields

Neo4jMemoryRetriever(memory_client=client, …​) returns LangChain Document objects sorted by similarity. Each document’s metadata["type"] is message, entity, preference or trace, alongside id and similarity.

Field Default Effect

session_id

None

Passed to message search. On the hosted backend it scopes the search to one conversation and is required for message results; Bolt message search ignores it and searches every stored message.

search_short_term

True

Search messages.

search_long_term

True

Search entities and preferences.

search_reasoning

True

Search reasoning traces.

k

10

Maximum number of documents returned.

threshold

0.7

Minimum similarity for every layer’s search.

Hosted backend behavior

On the hosted NAMS backend (MemorySettings(backend="nams")) the adapters skip unsupported calls instead of raising:

Call on NAMS Adapter behavior

long_term.get_context returns ""

Neo4jAgentMemory builds context from entity search instead.

long_term.search_preferences is not supported

preferences is []; the retriever returns no preference documents.

reasoning.get_similar_traces is not supported

similar_tasks is empty; the retriever returns no trace documents.

short_term.search_messages needs a conversation

Pass session_id to the retriever. Without it the message layer is skipped with a warning.

See also

See Backend capabilities and scoping before adding hosted or multi-user behavior. Neo4j Agent Memory is a Neo4j Labs project with community support.