# LangGraph

> Give a LangGraph graph a memory of each of your users, with a node that recalls before the model and a node that remembers after it.

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

```bash
pip install langgraph langchain langchain-anthropic geniffy
```

```bash
uv add langgraph langchain langchain-anthropic geniffy
```

Set `ANTHROPIC_API_KEY`, and `GENIFFY_API_KEY` from **API keys** in the Geniffy app.

## Recall, respond, remember

```python
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:

```python
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.

```python
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.

Source: https://docs.geniffy.com/integrations/langgraph
