|
| 1 | +# Zep LangGraph Integration |
| 2 | + |
| 3 | +Give [LangGraph](https://github.com/langchain-ai/langgraph) agents durable, |
| 4 | +cross-session memory backed by [Zep](https://www.getzep.com)'s temporal Context |
| 5 | +Graph. The package ships two layers: |
| 6 | + |
| 7 | +- **Node / tool helpers (primary)** — call Zep directly inside your graph nodes: |
| 8 | + inject the user's Context Block into the system prompt, persist each turn, and |
| 9 | + expose a graph-search tool. This matches Zep's own LangGraph guide. |
| 10 | +- **`ZepStore` (secondary)** — a hybrid-delegate |
| 11 | + [`BaseStore`](https://langchain-ai.github.io/langgraph/reference/store/) for |
| 12 | + `create_react_agent(store=...)` and langmem's memory tools. |
| 13 | + |
| 14 | +## Installation |
| 15 | + |
| 16 | +```bash |
| 17 | +pip install zep-langgraph |
| 18 | +``` |
| 19 | + |
| 20 | +See [SETUP.md](SETUP.md) for creating a Zep account, getting an API key, and |
| 21 | +running the example end to end. |
| 22 | + |
| 23 | +## Quick Start (primary path) |
| 24 | + |
| 25 | +Inject Zep context with a `prompt` callable, expose a graph-search tool, and |
| 26 | +persist each turn. Identity (the user, thread, and the user's real name) is yours |
| 27 | +to manage — create the Zep user and thread out-of-band before the first turn. |
| 28 | + |
| 29 | +```python |
| 30 | +import os |
| 31 | +from langchain_core.messages import AIMessage, HumanMessage |
| 32 | +from langchain_openai import ChatOpenAI |
| 33 | +from langgraph.prebuilt import create_react_agent |
| 34 | +from zep_cloud import Message |
| 35 | +from zep_cloud.client import AsyncZep |
| 36 | +from zep_langgraph import build_system_message, create_graph_search_tool, persist_messages |
| 37 | + |
| 38 | +zep = AsyncZep(api_key=os.environ["ZEP_API_KEY"]) |
| 39 | + |
| 40 | +async def prompt(state): |
| 41 | + system = await build_system_message( |
| 42 | + zep, thread_id="thread-1", base_instructions="You are a helpful assistant." |
| 43 | + ) |
| 44 | + return [system, *state["messages"]] |
| 45 | + |
| 46 | +agent = create_react_agent( |
| 47 | + model=ChatOpenAI(model="gpt-5"), |
| 48 | + tools=[create_graph_search_tool(zep, user_id="user-1")], |
| 49 | + prompt=prompt, |
| 50 | +) |
| 51 | + |
| 52 | +result = await agent.ainvoke({"messages": [HumanMessage(content="Where do I work?")]}) |
| 53 | +reply = result["messages"][-1] |
| 54 | +await persist_messages( |
| 55 | + zep, |
| 56 | + thread_id="thread-1", |
| 57 | + messages=[Message(role="user", content="Where do I work?", name="Alice Smith"), reply], |
| 58 | +) |
| 59 | +``` |
| 60 | + |
| 61 | +A complete runnable version is in |
| 62 | +[examples/react_agent.py](examples/react_agent.py). |
| 63 | + |
| 64 | +## How It Works |
| 65 | + |
| 66 | +The Zep loop is the same everywhere: **create user → create thread → add |
| 67 | +messages → retrieve context**. This package wraps each step as a helper you call |
| 68 | +from inside a graph node. |
| 69 | + |
| 70 | +### Context injection — `build_system_message` / `get_zep_context` |
| 71 | + |
| 72 | +`thread.get_user_context(thread_id)` returns a token-efficient **Context Block** |
| 73 | +assembled from the *entire user graph* (the thread only scopes what is relevant |
| 74 | +right now). `build_system_message` fetches it and folds it into a |
| 75 | +`SystemMessage` together with your base instructions, ready to prepend to the |
| 76 | +model's message list. `get_zep_context` returns just the raw block. |
| 77 | + |
| 78 | +Implemented in [src/zep_langgraph/context.py](src/zep_langgraph/context.py). |
| 79 | + |
| 80 | +### Persistence — `persist_messages` |
| 81 | + |
| 82 | +Wraps `thread.add_messages`. Accepts LangChain `BaseMessage` objects (converted |
| 83 | +automatically — `human`→`user`, `ai`→`assistant`, …) or native Zep `Message` |
| 84 | +objects, flattens multimodal content to text, truncates over-long messages, and |
| 85 | +maps names so Zep can resolve identity. Pass `return_context=True` to fold |
| 86 | +persist + retrieve into one round-trip. |
| 87 | + |
| 88 | +Implemented in |
| 89 | +[src/zep_langgraph/persistence.py](src/zep_langgraph/persistence.py). |
| 90 | + |
| 91 | +### On-demand search — `create_graph_search_tool` |
| 92 | + |
| 93 | +Returns a LangChain `StructuredTool` over `graph.search`. Bind it to a model or |
| 94 | +pass it to `create_react_agent(tools=[...])` and the model decides when to search |
| 95 | +the graph. The target (`user_id` for a personal graph, `graph_id` for a shared |
| 96 | +standalone graph) and the search parameters (`scope`, `reranker`, `limit`) are |
| 97 | +fixed at construction so the model only supplies the query. |
| 98 | + |
| 99 | +Implemented in [src/zep_langgraph/tools.py](src/zep_langgraph/tools.py). |
| 100 | + |
| 101 | +### `ZepStore` — a `BaseStore` for the langmem audience |
| 102 | + |
| 103 | +`BaseStore` is LangGraph's cross-thread long-term-memory interface; |
| 104 | +`create_react_agent(store=...)` and langmem's |
| 105 | +`create_manage_memory_tool` / `create_search_memory_tool` require one. Zep is a |
| 106 | +temporal knowledge graph, not a KV store, so `ZepStore` uses a **hybrid-delegate** |
| 107 | +design: a backing KV `BaseStore` (default `InMemoryStore`) serves exact-key |
| 108 | +`get` / `put` / `delete` / `list_namespaces` faithfully and synchronously, while |
| 109 | +every `put` is *also* ingested into Zep and `search` is routed to Zep's semantic |
| 110 | +`graph.search`. Only the two abstract methods (`batch` / `abatch`) are |
| 111 | +implemented; everything else is inherited and delegates to them. |
| 112 | + |
| 113 | +```python |
| 114 | +from zep_langgraph import ZepStore |
| 115 | + |
| 116 | +store = ZepStore(zep) # default backing store: InMemoryStore |
| 117 | +await store.aput(("memories", "user-1"), "m1", {"text": "Alice works at Acme."}) |
| 118 | +item = await store.aget(("memories", "user-1"), "m1") # exact-key, synchronous |
| 119 | +hits = await store.asearch(("memories", "user-1"), query="where does Alice work?") |
| 120 | +``` |
| 121 | + |
| 122 | +> **Zep ingestion is asynchronous.** A value written with `put` is available |
| 123 | +> immediately for exact-key `get` (served by the backing store), but its |
| 124 | +> extracted facts are **not** instantly returned by `search` — there is no |
| 125 | +> read-after-write of graph facts within a turn. `ZepStore` is the long-term |
| 126 | +> memory layer, not the checkpointer, so graph execution and short-term state are |
| 127 | +> unaffected. |
| 128 | +
|
| 129 | +Implemented in [src/zep_langgraph/store.py](src/zep_langgraph/store.py); see |
| 130 | +[examples/store_agent.py](examples/store_agent.py). |
| 131 | + |
| 132 | +## Public API |
| 133 | + |
| 134 | +| Symbol | Kind | Purpose | |
| 135 | +|--------|------|---------| |
| 136 | +| `get_zep_context` / `get_zep_context_sync` | async / sync fn | Fetch the Context Block for a thread | |
| 137 | +| `build_system_message` / `build_system_message_sync` | async / sync fn | Build a `SystemMessage` with the Context Block | |
| 138 | +| `format_context_block` | fn | Combine base instructions with a Context Block | |
| 139 | +| `persist_messages` / `persist_messages_sync` | async / sync fn | Persist a turn (LangChain or Zep messages) | |
| 140 | +| `to_zep_message` / `to_zep_messages` | fn | Convert LangChain messages to Zep messages | |
| 141 | +| `create_graph_search_tool` / `create_graph_search_tool_sync` | fn | Build a `graph.search` `StructuredTool` | |
| 142 | +| `ZepStore` | class | Hybrid-delegate `BaseStore` | |
| 143 | + |
| 144 | +Both an `AsyncZep` (async helpers, recommended) and a synchronous `Zep` client |
| 145 | +are supported. Reuse a single client instance. |
| 146 | + |
| 147 | +## Error Handling |
| 148 | + |
| 149 | +Every helper handles Zep failures gracefully: context retrieval and persistence |
| 150 | +log a warning and return `None`/an empty result, the search tool returns an error |
| 151 | +string, and `ZepStore` keeps serving KV operations from its backing store. **A |
| 152 | +Zep failure never crashes the host agent.** |
| 153 | + |
| 154 | +## Configuration |
| 155 | + |
| 156 | +```bash |
| 157 | +export ZEP_API_KEY="your-zep-api-key" |
| 158 | +export OPENAI_API_KEY="your-openai-api-key" # for the example's model |
| 159 | +``` |
| 160 | + |
| 161 | +## Examples |
| 162 | + |
| 163 | +- [examples/react_agent.py](examples/react_agent.py) — `create_react_agent` with |
| 164 | + Zep context injection, the graph-search tool, and per-turn persistence. |
| 165 | +- [examples/store_agent.py](examples/store_agent.py) — `ZepStore` as a |
| 166 | + `BaseStore`, showing the KV round-trip and Zep-routed semantic search. |
| 167 | + |
| 168 | +## Development |
| 169 | + |
| 170 | +```bash |
| 171 | +git clone https://github.com/getzep/zep.git |
| 172 | +cd zep/integrations/langgraph/python |
| 173 | +make install # uv sync --extra dev |
| 174 | +make format # ruff format . |
| 175 | +make lint # ruff check . |
| 176 | +make type-check # mypy src/ |
| 177 | +make test # pytest tests/ -v |
| 178 | +make all # all of the above |
| 179 | +make build # uv build |
| 180 | +``` |
| 181 | + |
| 182 | +## Requirements |
| 183 | + |
| 184 | +- Python 3.11+ |
| 185 | +- `zep-cloud>=3.23.0` |
| 186 | +- `langgraph>=1.2.5` (pulls in `langchain-core`) |
| 187 | + |
| 188 | +## Support |
| 189 | + |
| 190 | +- [Zep Documentation](https://help.getzep.com) |
| 191 | +- [Zep LangGraph Guide](https://help.getzep.com/langgraph-memory) |
| 192 | +- [LangGraph Documentation](https://langchain-ai.github.io/langgraph/) |
| 193 | +- [GitHub Issues](https://github.com/getzep/zep/issues) |
| 194 | + |
| 195 | +## License |
| 196 | + |
| 197 | +Apache 2.0 — see [LICENSE](../../../LICENSE) for details. |
| 198 | + |
| 199 | +## Contributing |
| 200 | + |
| 201 | +Contributions are welcome! Please see our |
| 202 | +[Contributing Guide](../../../CONTRIBUTING.md) for details. |
0 commit comments