|
| 1 | +# Zep Pydantic AI Integration |
| 2 | + |
| 3 | +A memory integration package that gives [Pydantic AI](https://ai.pydantic.dev) agents long-term memory powered by [Zep](https://www.getzep.com). User turns are persisted to Zep and relevant context from Zep's temporal knowledge graph is injected into the model prompt on every turn -- using Pydantic AI's native `ProcessHistory` capability -- plus an on-demand graph-search tool. |
| 4 | + |
| 5 | +## Installation |
| 6 | + |
| 7 | +```bash |
| 8 | +pip install zep-pydantic-ai |
| 9 | +``` |
| 10 | + |
| 11 | +See [SETUP.md](SETUP.md) for how to sign up for Zep, create an API key, configure your environment, and run the example. |
| 12 | + |
| 13 | +## Quick Start |
| 14 | + |
| 15 | +```python |
| 16 | +import asyncio |
| 17 | +from pydantic_ai import Agent |
| 18 | +from pydantic_ai.capabilities import ProcessHistory |
| 19 | +from zep_cloud.client import AsyncZep |
| 20 | +from zep_pydantic_ai import ( |
| 21 | + ZepDeps, |
| 22 | + zep_history_processor, |
| 23 | + create_zep_search_tool, |
| 24 | + persist_run, |
| 25 | +) |
| 26 | + |
| 27 | +zep = AsyncZep(api_key="your-zep-api-key") |
| 28 | + |
| 29 | +agent = Agent( |
| 30 | + "openai:gpt-4o-mini", |
| 31 | + deps_type=ZepDeps, |
| 32 | + capabilities=[ProcessHistory(zep_history_processor)], |
| 33 | + tools=[create_zep_search_tool()], |
| 34 | + instructions="You are a helpful assistant with long-term memory.", |
| 35 | +) |
| 36 | + |
| 37 | +async def main() -> None: |
| 38 | + deps = ZepDeps( |
| 39 | + client=zep, |
| 40 | + user_id="user_123", |
| 41 | + thread_id="thread_abc", |
| 42 | + first_name="Jane", |
| 43 | + last_name="Smith", |
| 44 | + ) |
| 45 | + result = await agent.run("What did I tell you about my project?", deps=deps) |
| 46 | + print(result.output) |
| 47 | + # Persist the assistant's reply (the user turn was already persisted). |
| 48 | + await persist_run(deps, result.new_messages()) |
| 49 | + |
| 50 | +asyncio.run(main()) |
| 51 | +``` |
| 52 | + |
| 53 | +## How It Works |
| 54 | + |
| 55 | +The integration plugs into Pydantic AI through three components. |
| 56 | + |
| 57 | +### `ZepDeps` |
| 58 | + |
| 59 | +A dataclass used as the agent's `deps_type`. It carries the Zep client and the |
| 60 | +user/thread identity (plus optional name, email, and display names). Construct |
| 61 | +one per conversation and pass it to `agent.run(..., deps=deps)`; the history |
| 62 | +processor and the search tool both reach it via `RunContext.deps`. The Zep user |
| 63 | +and thread are created lazily on first use -- you do not have to pre-create |
| 64 | +them. |
| 65 | + |
| 66 | +### `zep_history_processor` |
| 67 | + |
| 68 | +Registered via `capabilities=[ProcessHistory(zep_history_processor)]`. Pydantic |
| 69 | +AI runs a history processor immediately before **every** model request. On the |
| 70 | +user's turn this processor: |
| 71 | + |
| 72 | +1. resolves the Zep client and identity from `ctx.deps`; |
| 73 | +2. lazily creates the Zep user and thread; |
| 74 | +3. persists the latest user message via `thread.add_messages(return_context=True)` -- folding the write and context retrieval into a single round-trip; |
| 75 | +4. prepends Zep's returned context block to the message history as a system message. |
| 76 | + |
| 77 | +A subtle but important detail (see [SPIKE_FINDINGS](../../SPIKE_FINDINGS.md)): |
| 78 | +`ProcessHistory` fires **once per model request, not once per run**. A single |
| 79 | +`agent.run` that makes a tool call invokes the processor more than once with the |
| 80 | +same user turn. The processor therefore **dedupes by the latest user message |
| 81 | +text** per `(user_id, thread_id)`: it persists and retrieves on the first sight |
| 82 | +of a turn, caches the context, and replays the cached context on re-invocations |
| 83 | +without writing to Zep again. This prevents duplicate episodes. |
| 84 | + |
| 85 | +### `create_zep_search_tool` |
| 86 | + |
| 87 | +A factory that returns a model-callable `@agent.tool` over `graph.search`. The |
| 88 | +model decides when to search the knowledge graph for specific facts, entities, |
| 89 | +or prior episodes. By default it searches the current user's graph; pass |
| 90 | +`graph_id=...` to target a shared standalone graph (e.g. a documentation |
| 91 | +knowledge base). Search parameters (`scope`, `reranker`, `limit`) are pinned at |
| 92 | +construction time. |
| 93 | + |
| 94 | +### `persist_run` |
| 95 | + |
| 96 | +Call after `agent.run` with `result.new_messages()` to persist the assistant's |
| 97 | +reply to the Zep thread. Only assistant text is sent -- the user turn (already |
| 98 | +persisted by the processor) and any tool-call/tool-return scaffolding are |
| 99 | +skipped, so Zep sees one clean assistant message per turn. |
| 100 | + |
| 101 | +## Public API |
| 102 | + |
| 103 | +### `ZepDeps` |
| 104 | + |
| 105 | +| Field | Type | Required | Default | Description | |
| 106 | +|-------|------|----------|---------|-------------| |
| 107 | +| `client` | `AsyncZep` | Yes | -- | Initialised Zep async client (caller owns its lifecycle) | |
| 108 | +| `user_id` | `str` | Yes | -- | Zep user ID (one user graph) | |
| 109 | +| `thread_id` | `str` | Yes | -- | Zep thread ID for the conversation | |
| 110 | +| `first_name` | `str` | No | `None` | User first name (recommended; anchors the user node) | |
| 111 | +| `last_name` | `str` | No | `None` | User last name | |
| 112 | +| `email` | `str` | No | `None` | User email (helps identity resolution) | |
| 113 | +| `user_name` | `str` | No | `None` | Display name for persisted user messages (defaults to first + last) | |
| 114 | +| `assistant_name` | `str` | No | `"Assistant"` | Display name for persisted assistant messages | |
| 115 | +| `ignore_roles` | `list[str]` | No | `None` | Roles to exclude from graph ingestion | |
| 116 | + |
| 117 | +### `create_zep_search_tool` |
| 118 | + |
| 119 | +| Parameter | Type | Default | Description | |
| 120 | +|-----------|------|---------|-------------| |
| 121 | +| `graph_id` | `str` | `None` | Standalone graph to search; when unset, searches the current user's graph | |
| 122 | +| `scope` | `"edges" \| "nodes" \| "episodes" \| "observations" \| "thread_summaries" \| "auto"` | `"edges"` | What to search | |
| 123 | +| `reranker` | `"rrf" \| "mmr" \| "node_distance" \| "episode_mentions" \| "cross_encoder"` | `"rrf"` | Result ordering (ignored for `scope="auto"`) | |
| 124 | +| `limit` | `int` | `10` | Maximum results (clamped to Zep's ceiling of 50) | |
| 125 | +| `name` | `str` | `"zep_search"` | Tool name exposed to the model | |
| 126 | + |
| 127 | +## Features |
| 128 | + |
| 129 | +- **Native `ProcessHistory` capability** -- the current Pydantic AI hook, not the deprecated `history_processors=` kwarg |
| 130 | +- **Single round-trip** -- persist + retrieve context in one `add_messages` call |
| 131 | +- **Once-per-request dedupe** -- correct under tool-calling runs that re-invoke the processor |
| 132 | +- **Lazy resource creation** -- Zep user and thread created on first use |
| 133 | +- **On-demand graph search** -- model-callable tool over `graph.search` |
| 134 | +- **Graceful error handling** -- Zep failures are logged but never crash the agent run |
| 135 | +- **Fully typed** -- ships type hints; passes `mypy --strict`-style checks |
| 136 | + |
| 137 | +## Error Handling |
| 138 | + |
| 139 | +Every Zep call is wrapped: a Zep outage, auth failure, or transient error is |
| 140 | +logged and the agent run continues. When persistence fails the turn is not |
| 141 | +cached, so the next model request retries it. |
| 142 | + |
| 143 | +## Configuration |
| 144 | + |
| 145 | +```bash |
| 146 | +export ZEP_API_KEY="your-zep-api-key" |
| 147 | +export OPENAI_API_KEY="your-openai-api-key" # or another provider supported by Pydantic AI |
| 148 | +``` |
| 149 | + |
| 150 | +## Examples |
| 151 | + |
| 152 | +See the [examples/](examples/) directory: |
| 153 | + |
| 154 | +- **[basic_agent.py](examples/basic_agent.py)** -- fact seeding and memory recall with the history processor + search tool. |
| 155 | + |
| 156 | +## Development |
| 157 | + |
| 158 | +```bash |
| 159 | +make install # uv sync --extra dev |
| 160 | +make format # ruff format |
| 161 | +make lint # ruff check |
| 162 | +make type-check # mypy src/ |
| 163 | +make test # pytest |
| 164 | +make all # format + lint + type-check + test |
| 165 | +make build # uv build |
| 166 | +``` |
| 167 | + |
| 168 | +## Requirements |
| 169 | + |
| 170 | +- Python 3.11+ |
| 171 | +- `pydantic-ai>=1.107,<2` |
| 172 | +- `zep-cloud>=3.23.0` |
| 173 | + |
| 174 | +## Support |
| 175 | + |
| 176 | +- [Zep Documentation](https://help.getzep.com) |
| 177 | +- [Pydantic AI Documentation](https://ai.pydantic.dev) |
| 178 | +- [GitHub Issues](https://github.com/getzep/zep/issues) |
| 179 | + |
| 180 | +## License |
| 181 | + |
| 182 | +Apache 2.0 - see [LICENSE](../../../LICENSE) for details. |
| 183 | + |
| 184 | +## Contributing |
| 185 | + |
| 186 | +Contributions are welcome! Please see our [Contributing Guide](../../../CONTRIBUTING.md) for details. |
0 commit comments