LangGraph
Give a LangGraph graph a memory of each of your users. Memory is two nodes in your graph: recall before
the model, and remember after it. The user comes from the graph's runtime context, so one compiled graph
serves every user.
Install
pip install langgraph langchain langchain-anthropic geniffyuv add langgraph langchain langchain-anthropic geniffySet ANTHROPIC_API_KEY, and GENIFFY_API_KEY from API keys in the Geniffy app.
Recall, respond, remember
from dataclasses import dataclass
from geniffy import Geniffy
from langchain.chat_models import init_chat_model
from langgraph.graph import END, START, MessagesState, StateGraph
from langgraph.runtime import Runtime
geniffy = Geniffy() # reads GENIFFY_API_KEY
model = init_chat_model("anthropic:claude-opus-5-5")
@dataclass
class User:
id: str
class State(MessagesState):
memory: str
def recall(state: State, runtime: Runtime[User]) -> dict:
mem = geniffy.space(f"user_{runtime.context.id}")
return {"memory": mem.context(state["messages"][-1].text)}
def respond(state: State) -> dict:
system = f"You are a helpful assistant.\n\n<memory>\n{state['memory']}\n</memory>"
reply = model.invoke([{"role": "system", "content": system}, *state["messages"]])
return {"messages": [reply]}
def remember(state: State, runtime: Runtime[User]) -> dict:
asked, answer = state["messages"][-2], state["messages"][-1]
geniffy.space(f"user_{runtime.context.id}").memories.add(messages=[
{"role": "user", "content": asked.text},
{"role": "assistant", "content": answer.text},
])
return {}
builder = StateGraph(State, context_schema=User)
builder.add_sequence([recall, respond, remember])
builder.add_edge(START, "recall")
builder.add_edge("remember", END)
graph = builder.compile()Pass the user with each run, from your own sign-in:
result = graph.invoke(
{"messages": [{"role": "user", "content": "Who signs the Lumen renewal?"}]},
context=User(id=user.id),
)
print(result["messages"][-1].text)recall puts what is known about this user in the state, each line with where it came from, and respond
gives it to the model. When nothing is known, the block says so in one sentence, so the model says it
doesn't know instead of guessing.
Let the model look things up
To let the model decide when to look something up, give it a recall tool, loop through a ToolNode, and
remember once the model has its answer.
from dataclasses import dataclass
from geniffy import Geniffy
from langchain.chat_models import init_chat_model
from langchain.tools import ToolRuntime, tool
from langgraph.graph import END, START, MessagesState, StateGraph
from langgraph.prebuilt import ToolNode, tools_condition
from langgraph.runtime import Runtime
geniffy = Geniffy()
INSTRUCTIONS = ("You are a helpful assistant. Use recall before answering anything "
"that depends on what the user said before.")
@dataclass
class User:
id: str
@tool
def recall(query: str, runtime: ToolRuntime[User]) -> str:
"""Look up what is known about the user, with where it came from."""
return geniffy.space(f"user_{runtime.context.id}").context(query)
model = init_chat_model("anthropic:claude-opus-5-5").bind_tools([recall])
def respond(state: MessagesState) -> dict:
system = {"role": "system", "content": INSTRUCTIONS}
return {"messages": [model.invoke([system, *state["messages"]])]}
def remember(state: MessagesState, runtime: Runtime[User]) -> dict:
asked = next(m for m in reversed(state["messages"]) if m.type == "human")
geniffy.space(f"user_{runtime.context.id}").memories.add(messages=[
{"role": "user", "content": asked.text},
{"role": "assistant", "content": state["messages"][-1].text},
])
return {}
builder = StateGraph(MessagesState, context_schema=User)
builder.add_node(respond)
builder.add_node("tools", ToolNode([recall]))
builder.add_node(remember)
builder.add_edge(START, "respond")
builder.add_conditional_edges(
"respond", tools_condition, {"tools": "tools", END: "remember"},
)
builder.add_edge("tools", "respond")
builder.add_edge("remember", END)
graph = builder.compile()Memory across threads
A LangGraph checkpointer keeps one thread going, and its store holds what you put in it. Geniffy is what the graph knows about the user across every thread: what they said last month, in another app, or in a file you added, with where each fact came from. Use the checkpointer for the thread, and Geniffy for the person.