zep-ag2
Zep memory integration for AG2
Installation
pip install zep-ag2
Identifiers in Zep v4
Zep v4 addresses every user, thread, and graph by a server-generated UUID. A
create call takes no client-chosen name. The public API of this package takes
user_uuid, thread_uuid, and graph_uuid.
For a new user, create the user and the thread one time with create_user and create_thread,
read the UUIDs from the responses, and store them in your own database. The
package does not resolve a name at run time.
from zep_ag2 import create_thread, create_user
user = await create_user(zep, first_name="Jane", email="jane@example.com")
thread = await create_thread(zep, user_uuid=user.uuid_)
# Store these three values in your application database.
user.uuid_, user.graph_uuid, thread.uuid_
Migrating from V3
Follow the V3 to V4 migration guide for the general procedure. The API changes that are specific to this package are:
ensure_user(client, *, user_id, ...)andensure_thread(client, *, thread_id, user_id)are removed.create_user(client, *, first_name=None, last_name=None, email=None, on_created=None)andcreate_thread(client, *, user_uuid)replace them. They take no client-chosen name, and they return the createdUserandThread. Theon_createdhook now receives the UUID of the user.ZepMemoryManager(client, user_id, session_id=None, ...)is nowZepMemoryManager(client, user_uuid, thread_uuid=None, *, graph_uuid=None, sync_client=None, ...). Give the stored user UUID, thread UUID, and user graph UUID here. Theuser_idandsession_idproperties are nowuser_uuidandthread_uuid.- The lazy provisioning path of
ZepMemoryManageris removed:ensure_user_and_thread(), and the constructor argumentsfirst_name,last_name,email, andon_created. ZepGraphMemoryManager(client, graph_id)is nowZepGraphMemoryManager(client, graph_uuid, *, sync_client=None). Thegraph_idproperty is nowgraph_uuid.create_search_memory_tool(client, user_id, session_id=None, ...)is nowcreate_search_memory_tool(client, graph_uuid, ...). Give the stored user graph UUID.create_add_memory_tool(client, user_id, session_id=None)is nowcreate_add_memory_tool(client, graph_uuid, thread_uuid=None). Give the stored user graph UUID and thread UUID.create_search_graph_tool(client, user_id=None, graph_id=None, ...)andcreate_add_graph_data_tool(client, user_id=None, graph_id=None)now take onegraph_uuid. Give the stored user graph UUID or the stored UUID of a named graph.- The
search_filtersargument ofcreate_search_memory_toolandcreate_search_graph_toolis nowfilters. register_all_tools(agent, executor, client, user_id, session_id=None, graph_id=None)is nowregister_all_tools(agent, executor, client, graph_uuid, thread_uuid=None, memory_graph_uuid=None). The graph tools usegraph_uuid. The memory tools usememory_graph_uuid, which defaults tograph_uuid. Whengraph_uuidis a named graph, give the stored user graph UUID asmemory_graph_uuid.- The
user_idandthread_idfields ofContextInputare nowuser_uuidandthread_uuid.
For an existing V3 user, thread, or graph, do not call create_user, create_thread, or graph.create. Get its UUID one time with the backfill in the migration guide, and store it.
Quick Start
System Message Injection
import asyncio
import os
from autogen import AssistantAgent, UserProxyAgent, LLMConfig
from zep_cloud.client import AsyncZep, Zep
from zep_ag2 import ZepMemoryManager, create_thread, create_user, register_all_tools
async def main():
# The async helpers use AsyncZep. The synchronous AG2 tools use Zep.
zep = AsyncZep(api_key=os.environ["ZEP_API_KEY"])
sync_zep = Zep(api_key=os.environ["ZEP_API_KEY"])
# Create these one time and store the UUIDs in your own database.
user = await create_user(zep, first_name="Jane", email="jane@example.com")
thread = await create_thread(zep, user_uuid=user.uuid_)
user_uuid, graph_uuid, thread_uuid = user.uuid_, user.graph_uuid, thread.uuid_
llm_config = LLMConfig({"model": "gpt-5-mini", "api_key": os.environ["OPENAI_API_KEY"]})
assistant = AssistantAgent(
name="assistant",
llm_config=llm_config,
system_message="You are a helpful assistant with long-term memory.",
)
user_proxy = UserProxyAgent(
name="user",
human_input_mode="NEVER",
code_execution_config=False,
is_termination_msg=lambda msg: "TERMINATE" in (msg.get("content") or ""),
)
# Enrich agent with memory context. The UUIDs come from your database.
memory_mgr = ZepMemoryManager(zep, user_uuid, thread_uuid, graph_uuid=graph_uuid)
await memory_mgr.enrich_system_message(assistant, query="conversation topic")
# Register memory tools. AG2 calls them synchronously, so they take the sync client.
register_all_tools(assistant, user_proxy, sync_zep, graph_uuid, thread_uuid)
result = user_proxy.initiate_chat(assistant, message="What do you remember about me?")
asyncio.run(main())
Tool-Based Memory Access
from zep_cloud.client import Zep
from zep_ag2 import create_search_graph_tool, create_add_graph_data_tool
# The tools take the synchronous Zep client.
sync_zep = Zep(api_key=os.environ["ZEP_API_KEY"])
# Create tools bound to the graph of one user
search_tool = create_search_graph_tool(sync_zep, graph_uuid)
add_tool = create_add_graph_data_tool(sync_zep, graph_uuid)
# Register with AG2's decorator pattern
assistant.register_for_llm(description="Search knowledge graph")(search_tool)
user_proxy.register_for_execution()(search_tool)
assistant.register_for_llm(description="Add to knowledge graph")(add_tool)
user_proxy.register_for_execution()(add_tool)
Automatic Memory Loop (attach_to_agent)
AG2 has no native memory interface -- ConversableAgent.register_hook is the framework's
closest thing to a per-turn seam. attach_to_agent wires a fully automatic inject+persist
loop onto that seam, so you don't have to call enrich_system_message/add_messages
yourself on every turn:
from zep_ag2 import ZepMemoryManager
# The hooks run in synchronous AG2 code, so they call the synchronous Zep client.
manager = ZepMemoryManager(zep, user_uuid, thread_uuid, graph_uuid=graph_uuid, sync_client=sync_zep)
manager.attach_to_agent(assistant)
# Every message the agent receives is persisted and used to refresh its system
# message; every reply the agent sends is persisted automatically too.
user_proxy.initiate_chat(assistant, message="My name is Alice.")
See "The automatic memory loop, precisely" below for the exact hook wiring and what is (and isn't) covered.
Features
- Tool-based memory access — Register Zep search/add as AG2 tools via
@register_for_llm - System message injection — Automatically enrich agent context with relevant memories
- Automatic memory loop —
attach_to_agentwires inject-on-receive and persist-on-send onto AG2'sregister_hookseam - Knowledge graph — Access Zep's knowledge graph from AG2 agents
- Conversation memory — Store and retrieve thread-based conversation history
- Out-of-band provisioning —
create_user/create_threadfor explicit, fail-loud setup before the first turn - Sync tool execution — Tools call the synchronous
Zepclient, which matches AG2's synchronous tool execution
The automatic memory loop, precisely
ZepMemoryManager.attach_to_agent(agent) registers two hooks on ConversableAgent:
process_last_received_messagefires for every message the agent receives. It persists the message and retrieves context with the synchronoussync_clientof the manager, then replaces the agent's system message with its original text plus the freshly-renderedcontext_template. The hook always returns the message content unmodified -- this is a side channel, not a message transform.process_message_before_sendfires for every message the agent sends. AG2's contract here is clean enough to persist through: the hook receives the outgoing message (astr, or a dict with acontentkey) and returns it unchanged after persisting it as anassistantmessage. This completes the loop -- previously (manual invocation only), persisting the assistant's reply required an explicitadd_messages()call.
Both hooks wrap their entire body in try/except, so a Zep outage never breaks the
agent's conversation loop -- on failure, the incoming hook simply skips the system-message
update and the outgoing hook skips persistence, in both cases still returning the message
unchanged.
attach_to_agent is optional and additive: enrich_system_message/add_messages remain
available for manual, explicit control (e.g. if you want to persist only some turns, or
inject context at a different point than "on receive").
Multi-agent caveat: one manager per Zep thread
Attach attach_to_agent to exactly one agent per Zep thread -- normally the user-facing
agent. Each attached manager registers both hooks, so if two agents in a two-agent
conversation each call attach_to_agent with managers pointing at the same
thread_uuid, every turn is persisted twice with conflicting roles: agent A's outgoing
hook (process_message_before_send) persists its reply as role="assistant", and agent B's
incoming hook (process_last_received_message) persists that same content again as
role="user" (and vice versa for the reverse direction). This silently doubles and
mislabels the thread's history -- it is not caught or deduplicated by the package.
Correct wiring -- attach only to the user-facing agent, leaving the other agent unmanaged:
manager = ZepMemoryManager(zep, user_uuid, thread_uuid, graph_uuid=graph_uuid, sync_client=sync_zep)
# Attach to ONE agent only (e.g. the user-facing assistant) ...
manager.attach_to_agent(assistant)
# ... and leave the other agent in the conversation unattached.
# Do NOT also call some_other_manager.attach_to_agent(other_agent) with a
# manager pointing at the same thread_uuid.
If both agents genuinely need their own automatic loop, create a second thread and give
each manager a distinct thread_uuid instead of sharing one thread:
assistant_thread = await create_thread(zep, user_uuid=user_uuid)
critic_thread = await create_thread(zep, user_uuid=user_uuid)
assistant_manager = ZepMemoryManager(zep, user_uuid, assistant_thread.uuid_, sync_client=sync_zep)
assistant_manager.attach_to_agent(assistant)
critic_manager = ZepMemoryManager(zep, user_uuid, critic_thread.uuid_, sync_client=sync_zep)
critic_manager.attach_to_agent(critic)
Provisioning
Create the Zep user and thread out-of-band, before the first turn. ZepMemoryManager takes
the UUIDs of resources that already exist, and it never creates a resource on a memory path.
Both helpers raise on every failure, so a misconfiguration is caught early. Pass
first_name, last_name, and email so Zep can anchor the identity node of the user in
the graph, and on_created to run one-time setup (ontology, custom instructions) after the
user is created:
from zep_ag2 import create_thread, create_user
async def setup_new_user(
zep, user_uuid: str
) -> None: ... # example one-time setup, such as an ontology
user = await create_user(
zep,
first_name="Jane",
last_name="Smith",
email="jane@example.com",
on_created=setup_new_user,
)
thread = await create_thread(zep, user_uuid=user.uuid_)
manager = ZepMemoryManager(zep, user.uuid_, thread.uuid_, graph_uuid=user.graph_uuid)
The manager needs the UUID of the graph of the user for graph search. Pass graph_uuid from
user.graph_uuid. When you do not pass it, the manager reads it one time with
user.get(user_uuid) and caches it.
Per-user manager instances. Like the sibling Zep integrations, a ZepMemoryManager is
scoped to one (user_uuid, thread_uuid) pair for the lifetime of the instance -- create one
manager per user/thread rather than sharing a single instance across users.
Custom context retrieval with context_builder
By default, context is retrieved via thread.get_context(...) (or, inside
process_user_message, via thread.add_messages(..., return_context=True)). Pass
context_builder to replace this with custom logic -- e.g. a filtered graph search, or a
different graph entirely:
from zep_ag2.memory import ContextInput
async def my_builder(ctx: ContextInput) -> str | None:
user = await ctx.zep.user.get(ctx.user_uuid)
pager = await ctx.zep.graph.search_edges(
user.graph_uuid,
query=ctx.user_message,
)
edges = pager.items or []
if not edges:
return None
return "\n".join(edge.fact for edge in edges if edge.fact)
manager = ZepMemoryManager(
zep,
user_uuid,
thread_uuid,
context_builder=my_builder,
)
ContextInput.agent is the AG2 agent in scope when the builder is invoked via
attach_to_agent's automatic loop, and None otherwise (e.g. a manual
process_user_message/enrich_system_message call with no agent involved). If the builder
raises, a warning is logged and context injection is skipped for that call -- the builder
never raises into process_user_message/get_memory_context/enrich_system_message.
Concurrency. Inside process_user_message, when context_builder is set, persistence
(thread.add_messages without return_context) and the builder run concurrently via
asyncio.gather(..., return_exceptions=True), with per-side isolation: a builder failure
never blocks the message from being persisted, and a persistence failure never prevents the
builder's context from being returned.
Customizing the injected context template
The retrieved context (whether from the default retrieval or a context_builder) is wrapped
in context_template before being injected into the agent's system message. Override it
with your own wording, as long as it contains a literal {context} placeholder:
manager = ZepMemoryManager(
zep,
user_uuid,
thread_uuid,
context_template="Relevant background:\n{context}",
)
The template is rendered via template.replace("{context}", context_text) -- never
str.format -- so context text containing {, }, or % is always safe to inject.
API Reference
ZepMemoryManager
Manages Zep memory for AG2 agents via system message injection and an optional automatic loop.
ZepMemoryManager(client, user_uuid, thread_uuid=None, *, graph_uuid=None, sync_client=None, context_builder=None, context_template=DEFAULT_CONTEXT_TEMPLATE)— Initialize with anAsyncZepclient. The synchronous methods callsync_client, azep_cloud.client.Zepclient, and raiseTypeErrorwhen it is not set.attach_to_agent(agent)— Register the automatic inject+persist loop (see above). Needssync_client.get_memory_context_sync(query=None, limit=5)andenrich_system_message_sync(agent, query=None, limit=5)— Synchronous forms. They needsync_client.await process_user_message(user_message, *, agent=None)— Persist a user turn and retrieve context in one callawait resolve_graph_uuid()— Return the UUID of the graph of the user, and cache itawait enrich_system_message(agent, query=None, limit=5)— Inject memory contextawait get_memory_context(query=None, limit=5)— Get formatted context stringawait add_messages(messages)— Store messages in Zep threadawait get_session_facts()— Get extracted session facts
ZepGraphMemoryManager
Manages Zep knowledge graph for AG2 agents.
ZepGraphMemoryManager(client, graph_uuid, *, sync_client=None)— Initialize with anAsyncZepclient and the UUID of a graph.search_syncandadd_data_synccallsync_client, azep_cloud.client.Zepclient, and raiseTypeErrorwhen it is not set.await search(query, limit=5, scope="edges")— Search the graphawait add_data(data, data_type="text")— Add data to the graphawait enrich_system_message(agent, query=None, limit=5)— Inject graph context
Provisioning
await create_user(client, *, first_name=None, last_name=None, email=None, on_created=None)— Create a Zep user and return it; readuser.uuid_anduser.graph_uuidawait create_thread(client, *, user_uuid)— Create a Zep thread and return it; readthread.uuid_
Tool Factories
All tool factories return synchronous callables (AG2 executes tools synchronously).
Each factory and register_all_tools take a synchronous zep_cloud.client.Zep client and
use it for every call. An AsyncZep client raises TypeError when you create the tools.
The async helpers (create_user, create_thread, and the manager methods) keep AsyncZep.
create_search_memory_tool and create_search_graph_tool follow a pin-or-expose
pattern: every search parameter (scope, reranker, limit, mmr_lambda,
center_node_uuid) is exposed to the model by default. The scope selects the v4 search
method: edges, nodes, episodes, observations, thread_summaries, or auto. The
auto scope calls graph.get_context, which accepts neither a reranker nor a limit. The package sends
limit as the page_size of the v4 search call.
Each scope accepts only some rerankers. edges accepts all five, nodes accepts all but
episode_mentions, episodes accepts rrf and mmr, and observations and
thread_summaries accept rrf, mmr, and cross_encoder. The tool schema offers only valid
choices: episode_mentions appears only when filters sets episode_uuids, and
node_distance needs center_node_uuid. If the model sends a pair that Zep rejects, the tool
does not call Zep. It returns an error message that names the pair and lists the valid
rerankers for the scope, so the model can call the tool again. The auto scope takes no reranker: if the model sends
a reranker with auto, the tool returns an error and does not call Zep. For mmr with no mmr_lambda, the tool sends 0.5. A pinned
pair that is not valid, which includes a pinned reranker with the auto scope, raises
ValueError when you create the tool.
Use pinned_params to fix a
parameter to a constant (hidden from the model), or hidden_params to remove it from the
schema without pinning (Zep's own default applies):
# Model chooses scope/reranker/limit/mmr_lambda/center_node_uuid freely (all exposed by default)
tool = create_search_graph_tool(sync_zep, graph_uuid)
# Pin scope to "nodes" and limit to 5 -- hidden from the model, always sent as given
tool = create_search_graph_tool(sync_zep, graph_uuid, pinned_params={"scope": "nodes", "limit": 5})
# Hide reranker entirely -- omitted from the schema AND the SDK call (Zep's default applies)
tool = create_search_graph_tool(sync_zep, graph_uuid, hidden_params={"reranker"})
create_search_memory_tool(client, graph_uuid, *, pinned_params=None, hidden_params=None, filters=None, bfs_origin_node_uuids=None, scope=None, limit=None)— Search conversation memorycreate_add_memory_tool(client, graph_uuid, thread_uuid=None)— Add conversation memorycreate_search_graph_tool(client, graph_uuid, *, pinned_params=None, hidden_params=None, filters=None, bfs_origin_node_uuids=None, scope=None, limit=None)— Search knowledge graphcreate_add_graph_data_tool(client, graph_uuid)— Add graph dataregister_all_tools(agent, executor, client, graph_uuid, thread_uuid=None, memory_graph_uuid=None)— Register all tools at once
Examples
- Basic Memory — System message injection + memory tools
- Graph Memory — Knowledge graph with ZepGraphMemoryManager
- Search Tools — Read-only search tool registration
- Full Tools — All tools in a GroupChat with multiple agents
- Manual Test — End-to-end integration test with real APIs
Configuration
Environment Variables
# Required
export ZEP_API_KEY="your-zep-cloud-api-key"
# Required for examples that use LLM
export OPENAI_API_KEY="your-openai-api-key"
Development
make install # Install dev dependencies
make pre-commit # Format, lint, type-check, test
make ci # CI validation
Requirements
- Python 3.11+
ag2>=0.9.0zep-cloud>=4.0.0,<5
License
Apache-2.0 — see LICENSE for details.
Support
Contributing
Contributions are welcome! Please see our Contributing Guide for details.
Metadata
Release files for zep-ag2 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| zep_ag2-1.0.0.tar.gz | 60.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| zep_ag2-1.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 92.5 kB
Release files / zep_ag2-1.0.0.tar.gz
| Download URL | zep_ag2-1.0.0.tar.gz |
|---|---|
| Size | 60.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
7f39fc7bd758f196bb02a66260225e0f7e103658aa43d3917e7021935e2222f5
|
|
BLAKE2b-256 checksum How to use checksums |
f8dfc2c3867f9861b27faa31e5c0ae1434b4f88f0c5ef72df85063cd4e16cda3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.
Transparency logRelease files / zep_ag2-1.0.0-py3-none-any.whl
| Download URL | zep_ag2-1.0.0-py3-none-any.whl |
|---|---|
| Size | 32.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a61c2fb04836b12ce43a80f1e986b5ad4291f511eccb633217f2c995e64515ef
|
|
BLAKE2b-256 checksum How to use checksums |
648dd9b39d29f5d2e2a6f077003bd5764d6027a481ce2cdc6a40515b41aa8e32
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.
Transparency log