Integrate with LangChain JS

The @neo4j-labs/agent-memory/integrations/langchain subpath exposes two framework-free adapters over MemoryClient:

  • Neo4jChatMessageHistory — read and write a conversation’s messages (getMessages, addMessage, addUserMessage, addAIChatMessage, clear).

  • Neo4jEntityRetriever — entity look-up that returns { pageContent, metadata } documents (invoke, getRelevantDocuments).

Neither imports @langchain/core, so the SDK installs without it. They are shapes you adapt, not subclasses: Neo4jChatMessageHistory does not extend BaseChatMessageHistory and does not implement addMessages, so it cannot be dropped into RunnableWithMessageHistory — that class calls addMessages() on the history it is given. In LangChain JS v1 the idiom to reach for is createAgent() with middleware anyway, which is what the runnable example below does.

A runnable example lives at typescript/examples/langchain — a two-turn createAgent() agent with no checkpointer, plus a mocked test that runs without an API key.

Prerequisites: a LangChain JS agent, Node.js 22+ and NAMS test-workspace credentials.

Procedure

  1. Build/install the source example using its README and select a conversation id.

  2. Convert the adapter’s plain messages/documents to the framework classes below.

  3. Wire context retrieval and message persistence into the agent middleware, then add the entity lookup tool.

Bridging the adapters to @langchain/core

Three small functions cover the whole gap. No casts are needed.

import { AIMessage, HumanMessage, SystemMessage, Document, type BaseMessage } from "langchain";

interface AdapterMessage {
  type: "human" | "ai" | "system";
  content: string;
}

function toLangChainMessage(message: AdapterMessage): BaseMessage {
  if (message.type === "ai") return new AIMessage(message.content);
  if (message.type === "system") return new SystemMessage(message.content);
  return new HumanMessage(message.content);
}

function fromLangChainMessage(message: BaseMessage): AdapterMessage | null {
  const content = message.text.trim();
  if (content === "") return null;
  if (AIMessage.isInstance(message)) {
    // An assistant turn that only requests a tool call is not an utterance.
    if ((message.tool_calls?.length ?? 0) > 0) return null;
    return { type: "ai", content };
  }
  if (message.getType() === "human") return { type: "human", content };
  return null;
}

function toLangChainDocument(doc: { pageContent: string; metadata: Record<string, unknown> }) {
  return new Document(doc);
}

Memory middleware for createAgent

wrapModelCall injects graph memory into the system message; afterAgent persists the turn through the history adapter. Because the graph holds the history, the agent needs no checkpointer — invoke it with only the new user message.

import { MemoryClient } from "@neo4j-labs/agent-memory";
import { Neo4jChatMessageHistory } from "@neo4j-labs/agent-memory/integrations/langchain";
import { ChatOpenAI } from "@langchain/openai";
import { HumanMessage, SystemMessage, createAgent, createMiddleware } from "langchain";

const memory = new MemoryClient();                    // reads MEMORY_API_KEY
const conversationId = (await memory.shortTerm.createConversation({ userId })).id;
const history = new Neo4jChatMessageHistory(memory, conversationId);

const namsMemory = createMiddleware({
  name: "NamsMemoryMiddleware",
  wrapModelCall: async (request, handler) => {
    const ctx = await memory.shortTerm.getContext(conversationId);
    const recalled = [
      ...ctx.reflections.map((r) => `- ${r.content}`),
      ...ctx.recentMessages.map((m) => `- ${m.role}: ${m.content}`),
    ].join("\n");
    if (recalled === "") return handler(request);
    return handler({
      ...request,
      systemMessage: new SystemMessage(`${request.systemMessage.text}\n\n## Memory\n${recalled}`),
    });
  },
  afterAgent: async (state) => {
    for (const message of state.messages) {
      const stored = fromLangChainMessage(message);
      if (stored) await history.addMessage(stored);     // append a stored message; this adapter does not deduplicate ids
    }
  },
});

const agent = createAgent({
  model: new ChatOpenAI({ model: process.env.OPENAI_MODEL ?? "gpt-5-mini" }),
  systemPrompt: "You are a concise assistant with a long memory.",
  middleware: [namsMemory],
});

const result = await agent.invoke({ messages: [new HumanMessage("Hello!")] });

Entity retriever as a tool

Neo4jEntityRetriever takes { type, topK } (not limit), and returns pageContent of the form "<name> — <description>" with metadata carrying id, type, confidence, sourceStage and canonicalName.

import { Neo4jEntityRetriever } from "@neo4j-labs/agent-memory/integrations/langchain";
import { tool } from "langchain";
import { z } from "zod";

const retriever = new Neo4jEntityRetriever(memory, { topK: 5 });

const searchMemoryEntities = tool(
  async ({ query }) => {
    const docs = await retriever.invoke(query);
    if (docs.length === 0) return "No entities in memory match that query.";
    return docs.map((d) => `- ${d.pageContent} (id=${String(d.metadata.id)})`).join("\n");
  },
  {
    name: "search_memory_entities",
    description: "Search long-term memory for entities the user discussed before.",
    schema: z.object({ query: z.string() }),
  },
);

NAMS extracts entities in a background pipeline, so a write is not immediately searchable. Await consistency explicitly rather than sleeping:

const ready = await memory.longTerm.waitForExtraction({
  query: "graph database",
  expectedNames: ["Neo4j"],
  timeoutMs: 20_000,
});
if (!ready) console.warn("extraction has not caught up yet");

Version compatibility

The adapters do not import LangChain, but changes to LangChain’s expected message/document shapes can still require application glue changes. The current-source example declares LangChain 1.x and checks its bridge functions against installed typings. That is separate from npm release or live-service verification. Include the installed LangChain version when reporting drift.

Verify the integration

In typescript/examples/langchain/, run npm run lint and npm test. The offline suite verifies prior-turn context, persisted user/assistant messages and entity lookup through the supported API. A separate live run is needed to verify extraction results; shared workspace entities are not a private profile.