Skip to content

Commit d1a41e3

Browse files
danielchalefclaude
andauthored
Add zep-pydantic-ai integration (#523)
* Add zep-pydantic-ai integration Zep memory for Pydantic AI (Python) via the current capabilities API. - capabilities=[ProcessHistory(zep_history_processor)] (async) persists the latest user turn and prepends Zep's Context Block; guards the once-per-model- request re-invocation by deduping on the latest user content. - ZepDeps (deps_type) for per-run client/user/thread; create_zep_search_tool over graph.search; persist_run helper for result.new_messages(). - Depends on pydantic-ai>=1.107,<2 and zep-cloud>=3.23.0. Python >=3.11. - README, SETUP, example, 57 mock tests, Makefile, CHANGELOG. CI filter added. Verified: ruff + ruff format + mypy + pytest (57 passed); live example smoke test passed. See integrations/SPIKE_FINDINGS.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Fix message truncation, run-scoped dedupe, and graph.search clamping in zep-pydantic-ai - Set MAX_MESSAGE_CHARS to 4000 (Zep rejects messages >4096) and truncate the user turn on the history-processor hot path before add_messages; warn with lengths only (no content/PII) via a shared truncate_message_content helper. - Re-scope turn dedupe to the RunContext.run_id (falling back to user/thread) so identical consecutive runs each persist; replace the unbounded module-global cache with a bounded, lock-guarded LRU. - graph.search: clamp limit to <=50 and omit reranker when scope="auto" (Zep rejects node_distance/episode_mentions there). - Add 'observations' and 'thread_summaries' to the Scope literal (zep-cloud 3.23). - Add regression tests for each fix. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Render observations and thread_summaries scopes in zep_search results _format_results advertised the "observations" and "thread_summaries" scopes (in the Scope literal and docstring) but had no rendering branch for them, so those scopes always returned "No results found." regardless of API results. Add rendering branches using the correct zep_cloud types: - observations -> DerivedNode.name (+ optional summary) - thread_summaries -> GraphitiSagaNode.summary (falling back to name) Add unit tests asserting both scopes render their items. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent a789a83 commit d1a41e3

17 files changed

Lines changed: 2595 additions & 0 deletions

File tree

.github/workflows/test-integrations.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ jobs:
3636
- 'integrations/livekit/python/**'
3737
ag2:
3838
- 'integrations/ag2/python/**'
39+
pydantic-ai:
40+
- 'integrations/pydantic-ai/python/**'
3941
4042
- uses: dorny/paths-filter@d1c1ffe0248fe513906c8e24db8ea791d46f8590 # v3
4143
id: typescript
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Changelog
2+
3+
## 0.1.0 (2026-06-16)
4+
5+
### Added
6+
7+
- `ZepDeps` -- dataclass carrying the Zep client and user/thread identity, used as the agent's `deps_type`.
8+
- `zep_history_processor` -- a Pydantic AI history processor (registered via `capabilities=[ProcessHistory(...)]`) that persists each user turn via `thread.add_messages(return_context=True)` and prepends Zep's context block to the prompt. Dedupes by latest user message to handle `ProcessHistory`'s once-per-model-request invocation, preventing duplicate episodes during tool-calling runs.
9+
- `persist_run` -- helper to persist the assistant reply from `result.new_messages()` to the Zep thread, skipping tool-call scaffolding.
10+
- `create_zep_search_tool` -- factory producing a model-callable `@agent.tool` over `graph.search`, targeting the current user's graph or a standalone graph.
11+
- Lazy Zep user and thread creation on first use.
12+
- Graceful error handling throughout: Zep failures are logged and never crash the agent run; failed persists are retried.
13+
- Mock-based test suite plus end-to-end wiring tests using Pydantic AI's `TestModel`.
14+
- Working example demonstrating fact seeding and memory recall.
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Makefile for zep-pydantic-ai development
2+
3+
.PHONY: help install format lint lint-fix type-check test test-cov clean build all pre-commit ci
4+
5+
# Default target
6+
help:
7+
@echo "Available commands:"
8+
@echo " install - Install package and dependencies in development mode"
9+
@echo " format - Format code with ruff"
10+
@echo " lint - Run linting checks"
11+
@echo " type-check - Run type checking with mypy"
12+
@echo " test - Run tests"
13+
@echo " test-cov - Run tests with coverage report"
14+
@echo " all - Run format, lint, type-check, and test"
15+
@echo " build - Build the package"
16+
@echo " clean - Clean build artifacts"
17+
18+
# Install package in development mode
19+
install:
20+
uv sync --extra dev
21+
22+
# Format code
23+
format:
24+
uv run ruff format .
25+
26+
# Run linting checks
27+
lint:
28+
uv run ruff check .
29+
30+
# Fix linting issues automatically
31+
lint-fix:
32+
uv run ruff check --fix .
33+
34+
# Run type checking
35+
type-check:
36+
uv run mypy src/
37+
38+
# Run tests
39+
test:
40+
uv run pytest tests/ -v
41+
42+
# Run tests with coverage
43+
test-cov:
44+
uv run pytest tests/ -v --cov=zep_pydantic_ai --cov-report=term-missing --cov-report=xml
45+
46+
# Run all checks (the order matters: format first, then lint, then type-check, then test)
47+
all: format lint type-check test
48+
49+
# Build the package
50+
build:
51+
uv build
52+
53+
# Clean build artifacts
54+
clean:
55+
rm -rf dist/
56+
rm -rf build/
57+
rm -rf *.egg-info/
58+
rm -rf .pytest_cache/
59+
rm -rf .mypy_cache/
60+
rm -rf .ruff_cache/
61+
find . -type d -name __pycache__ -exec rm -rf {} +
62+
find . -type f -name "*.pyc" -delete
63+
rm -f coverage.xml
64+
rm -f .coverage
65+
66+
# Development workflow - run this before committing
67+
pre-commit: lint-fix format lint type-check test
68+
@echo "All checks passed! Ready to commit."
69+
70+
# CI workflow - strict checks without auto-fixing
71+
ci: lint type-check test
72+
@echo "CI checks passed!"
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
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.
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# Setup Guide
2+
3+
This guide walks you from a fresh machine to running the example agent with Zep memory.
4+
5+
## 1. Sign up for Zep and create an API key
6+
7+
1. Go to [https://www.getzep.com](https://www.getzep.com) and create an account.
8+
2. Open the [Zep dashboard](https://app.getzep.com) and select (or create) a project.
9+
3. In the project settings, go to **API Keys** and create a new key.
10+
4. Copy the key — you will set it as `ZEP_API_KEY` below.
11+
12+
Zep is a paid product; see [getzep.com](https://www.getzep.com) for plan details.
13+
14+
## 2. Get an OpenAI API key (for the example)
15+
16+
The integration itself is model-agnostic — it works with any model
17+
[Pydantic AI supports](https://ai.pydantic.dev/models/). The bundled example and
18+
live tests drive the agent with OpenAI. Create a key at
19+
[platform.openai.com/api-keys](https://platform.openai.com/api-keys) and copy it
20+
for `OPENAI_API_KEY`.
21+
22+
## 3. Install
23+
24+
Using `pip`:
25+
26+
```bash
27+
pip install zep-pydantic-ai
28+
```
29+
30+
Or, to work from the repository with `uv`:
31+
32+
```bash
33+
git clone https://github.com/getzep/zep.git
34+
cd zep/integrations/pydantic-ai/python
35+
make install # uv sync --extra dev
36+
```
37+
38+
Requirements: Python 3.11+, `pydantic-ai>=1.107,<2`, `zep-cloud>=3.23.0`.
39+
40+
## 4. Configure environment variables
41+
42+
```bash
43+
export ZEP_API_KEY="your-zep-api-key"
44+
export OPENAI_API_KEY="your-openai-api-key"
45+
```
46+
47+
## 5. Run the example
48+
49+
From the repository:
50+
51+
```bash
52+
uv run python examples/basic_agent.py
53+
```
54+
55+
Or, if you installed with `pip`:
56+
57+
```bash
58+
python examples/basic_agent.py
59+
```
60+
61+
The example:
62+
63+
1. Seeds facts about a user across two turns in one thread.
64+
2. Waits for Zep to process the knowledge graph (ingestion is asynchronous).
65+
3. Asks recall questions — the agent answers using facts fused into the user's
66+
graph, injected automatically by the history processor.
67+
68+
## 6. Run the tests
69+
70+
Mock-based tests (no API keys needed):
71+
72+
```bash
73+
make test
74+
```
75+
76+
These include end-to-end wiring tests that run a real Pydantic AI agent against
77+
Pydantic AI's built-in `TestModel` (no LLM API key required) with a mocked Zep
78+
client.
79+
80+
## Troubleshooting
81+
82+
- **`ZepDependencyError` on import** — Pydantic AI is not installed. Run
83+
`pip install zep-pydantic-ai` (which pulls `pydantic-ai`).
84+
- **Recall returns nothing** — Zep ingestion is asynchronous; a just-added fact
85+
is not instantly retrievable. The example waits ~15s; increase the wait if your
86+
graph is large or under load.
87+
- **Authentication errors** — confirm `ZEP_API_KEY` is set in the same shell and
88+
belongs to the intended project.
89+
- **`PydanticAIDeprecationWarning` about `openai:`** — this is a forward-compat
90+
notice from Pydantic AI itself, not from this integration. The example runs
91+
correctly; pin a fully-qualified model string (e.g. `openai-chat:gpt-4o-mini`)
92+
to silence it.

0 commit comments

Comments
 (0)