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)
| LangGraph | Gnarl memory |
|---|---|
| the store | one memory namespace (default langgraph-store) |
| item namespace tuple | user — labels joined with . (LangGraph forbids . in labels) |
| item key | session |
| item value | stored 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
userserver-side, so("memories", "alice")never recalls Bob's items. searchwith 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 ownindex=[...]) narrows that.index=Falseis not supported: the node embeds every memory. - TTL is not supported.
- A
putis 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.putis 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