Skip to main content

LangChain and LangGraph

langchain-gnarl gives a LangGraph or LangChain agent a memory that lives on the user's own node: a LangGraph long-term memory store, a LangChain chat history, and a LangChain retriever. They are thin wrappers over the Python client; the node does the embedding, so there is no embedding model to configure.

pip install langchain-gnarl

With the Gnarly app running there is no address to configure either: the client finds the node through $GNARL_URL, then the endpoint the node on this machine recorded, then http://127.0.0.1:43300.

from langchain_gnarl import GnarlStore, GnarlChatMessageHistory, GnarlRetriever

store = GnarlStore() # LangGraph BaseStore
store.put(("memories", "alice"), "diet", {"text": "Alice is vegetarian"})
hits = store.search(("memories", "alice"), query="what does alice eat?", limit=3)

history = GnarlChatMessageHistory("thread-42", user="alice")
history.add_user_message("I moved to Lisbon last spring")

docs = GnarlRetriever(namespace="langchain-chat", user="alice").invoke("where does alice live?")

Use the store in a graph like any other: builder.compile(store=GnarlStore()), or create_react_agent(model, tools, store=GnarlStore()).

GnarlStore — long-term memory for LangGraph​

GnarlStore(client=None, *, async_client=None, namespace="langgraph-store", index_fields=None)
LangGraphGnarl memory
the storeone memory namespace (default langgraph-store)
item namespace tupleuser — labels joined with . (LangGraph forbids . in labels)
item keysession
item valuestored verbatim (JSON); its text is embedded by the node
search(query=...)memory.recall, scoped to user — semantic
  • Isolation is enforced by the node. Recall is scoped to user server-side, so ("memories", "alice") never recalls Bob's items.
  • search with a query over a namespace that has sub-namespaces recalls each one and merges by score. Without a query it lists items, newest first.
  • filter= supports equality and $eq, $ne, $gt, $gte, $lt, $lte, applied to the recalled candidates.
  • Values are embedded from every string they contain; index_fields=["text"] (or a put's own index=[...]) narrows that. index=False is not supported: the node embeds every memory.
  • TTL is not supported.
  • A put is visible as soon as it returns. A replaced or deleted item drops out of other processes' reads after the node's next refresh, about a second, and immediately in this process. put is not atomic: two concurrent puts of one key both land, and reads return the newer.

GnarlChatMessageHistory — chat history​

GnarlChatMessageHistory(session_id, *, client=None, async_client=None, namespace="langchain-chat", user=None)

One memory per turn, with session set to session_id and the full message kept verbatim. Turns are therefore also recallable by meaning across sessions. clear() deletes the session's turns.

GnarlRetriever — a retriever over memory​

GnarlRetriever(client=None, async_client=None, namespace=None, space=None, user=None, session=None, k=4, require_complete=False)

Returns Documents whose metadata carries id, score, knn_score, fact_type, session, agent, created, tags, namespace, and the recall's partial and coverage. When part of the memory did not answer — a device holding it was asleep — partial is True: the documents are a subset, and a missing memory may still exist. require_complete=True raises IncompleteRecall instead.

Pointing somewhere else​

Every class takes an optional gnarl.Client and gnarl.AsyncClient. With neither, one is built on first use and finds the node as above, with $GNARL_TOKEN supplying a token. With only a sync client, async methods run it in a worker thread.

from gnarl import Client
from langchain_gnarl import GnarlStore

store = GnarlStore(client=Client("https://node.example.com", token="..."))

Source​

langchain-gnarl on GitHub · Apache-2.0