zep-ag2
Zep memory integration for AG2
Installation
pip install zep-ag2
Quick Start
System Message Injection
import asyncio
import os
from autogen import AssistantAgent, UserProxyAgent, LLMConfig
from zep_cloud.client import AsyncZep
from zep_ag2 import ZepMemoryManager, register_all_tools
async def main():
zep = AsyncZep(api_key=os.environ["ZEP_API_KEY"])
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
memory_mgr = ZepMemoryManager(zep, user_id="user123", session_id="session456")
await memory_mgr.enrich_system_message(assistant, query="conversation topic")
# Register memory tools (sync — AG2 calls them automatically)
register_all_tools(assistant, user_proxy, zep, user_id="user123", session_id="session456")
result = user_proxy.initiate_chat(
assistant, message="What do you remember about me?"
)
asyncio.run(main())
Tool-Based Memory Access
from zep_ag2 import create_search_graph_tool, create_add_graph_data_tool
# Create tools bound to a user's knowledge graph
search_tool = create_search_graph_tool(zep, user_id="user123")
add_tool = create_add_graph_data_tool(zep, user_id="user123")
# 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
manager = ZepMemoryManager(zep, user_id="user123", session_id="session456")
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 —
ensure_user/ensure_threadfor explicit, fail-loud setup before the first turn - Sync tool execution — Tools run synchronously via a single shared background event loop, compatible with AG2's execution model on Python 3.11–3.13
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 (viaprocess_user_messageinternally, bridged through the package's sync loop), 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
session_id, 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_id="user123", session_id="session456")
# 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 session_id="session456".
If both agents genuinely need their own automatic loop, give each its own manager with a
distinct session_id instead of sharing one thread:
assistant_manager = ZepMemoryManager(zep, user_id="user123", session_id="assistant-thread")
assistant_manager.attach_to_agent(assistant)
critic_manager = ZepMemoryManager(zep, user_id="user123", session_id="critic-thread")
critic_manager.attach_to_agent(critic)
Provisioning
The Zep user and (if session_id is set) thread are created lazily, on first use, by the
memory-path methods (process_user_message, get_memory_context, enrich_system_message,
add_messages, and the attach_to_agent hooks) -- you do not have to pre-create them.
Creation is idempotent and cached per ZepMemoryManager instance. Pass first_name,
last_name, and email to the constructor so Zep can anchor the user's identity node in the
graph, and on_created to run one-time setup (ontology, custom instructions) the first time
the user is actually created:
async def setup_new_user(zep, user_id: str) -> None:
await zep.user.add_ontology(user_id=user_id, ...) # example one-time setup
manager = ZepMemoryManager(
zep,
user_id="user123",
session_id="session456",
first_name="Jane",
last_name="Smith",
email="jane@example.com",
on_created=setup_new_user,
)
This lazy path is hot-path-wrapped: a genuine provisioning failure (or an on_created hook
failure) is logged and swallowed -- it is never raised into a memory-path method. If you want
provisioning failures to surface loudly (e.g. during account onboarding, before the first
turn), call ensure_user/ensure_thread directly, out-of-band:
from zep_ag2 import ensure_user, ensure_thread
await ensure_user(zep, user_id="user123", first_name="Jane", on_created=setup_new_user)
await ensure_thread(zep, thread_id="session456", user_id="user123")
Per-user manager instances. Like the sibling Zep integrations, a ZepMemoryManager is
scoped to one (user_id, session_id) 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_user_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:
results = await ctx.zep.graph.search(
user_id=ctx.user_id,
query=ctx.user_message,
scope="edges",
)
if not results.edges:
return None
return "\n".join(edge.fact for edge in results.edges)
manager = ZepMemoryManager(
zep, user_id="user123", session_id="session456", 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_id="user123", session_id="session456",
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_id, session_id=None, *, first_name=None, last_name=None, email=None, on_created=None, context_builder=None, context_template=DEFAULT_CONTEXT_TEMPLATE)— Initialize with Zep clientattach_to_agent(agent)— Register the automatic inject+persist loop (see above)await process_user_message(user_message, *, agent=None)— Persist a user turn and retrieve context in one callawait ensure_user_and_thread()— Lazily provision the user/thread, hot-path-wrapped (returnsFalseon failure, never raises)await 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_id)— Initialize with graph IDawait 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 ensure_user(client, *, user_id, first_name=None, last_name=None, email=None, on_created=None)— Idempotently create a Zep user; returnsTrueiff newly createdawait ensure_thread(client, *, thread_id, user_id)— Idempotently create a Zep thread; returnsTrueiff newly created
Tool Factories
All tool factories return synchronous callables (AG2 executes tools synchronously).
Internally they bridge to the async Zep SDK on a single shared background event loop,
reusing the AsyncZep client you pass in (no per-call client construction).
create_search_memory_tool and create_search_graph_tool follow a pin-or-expose
pattern: every graph.search parameter (scope, reranker, limit, mmr_lambda,
center_node_uuid) is exposed to the model by default. 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(zep, user_id="user123")
# Pin scope to "nodes" and limit to 5 -- hidden from the model, always sent as given
tool = create_search_graph_tool(
zep, user_id="user123", 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(zep, user_id="user123", hidden_params={"reranker"})
create_search_memory_tool(client, user_id, session_id=None, *, pinned_params=None, hidden_params=None, search_filters=None, bfs_origin_node_uuids=None, scope=None, limit=None)— Search conversation memorycreate_add_memory_tool(client, user_id, session_id=None)— Add conversation memorycreate_search_graph_tool(client, user_id=None, graph_id=None, *, pinned_params=None, hidden_params=None, search_filters=None, bfs_origin_node_uuids=None, scope=None, limit=None)— Search knowledge graphcreate_add_graph_data_tool(client, user_id=None, graph_id=None)— Add graph dataregister_all_tools(agent, executor, client, user_id, ...)— 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>=3.23.0
License
Apache-2.0 — see LICENSE for details.
Support
Contributing
Contributions are welcome! Please see our Contributing Guide for details.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file zep_ag2-0.2.0.tar.gz.
File metadata
- Download URL: zep_ag2-0.2.0.tar.gz
- Upload date:
- Size: 53.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a9329abc3b15972a25df78d4f44b433eb6ce091464c56675fc4d5d28a970a7c8
|
|
| MD5 |
0a90ac249c857d3332930f2cec0364fc
|
|
| BLAKE2b-256 |
893af5690780f1933af898f5ef9cbb3ae326c52c38a8a005f1cc59266c65be4b
|
Provenance
The following attestation bundles were made for zep_ag2-0.2.0.tar.gz:
Publisher:
release-integrations.yml on getzep/zep
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
zep_ag2-0.2.0.tar.gz -
Subject digest:
a9329abc3b15972a25df78d4f44b433eb6ce091464c56675fc4d5d28a970a7c8 - Sigstore transparency entry: 2138785683
- Sigstore integration time:
-
Permalink:
getzep/zep@ace6d3501dbffa80d522752ad862fe43ac6bcda0 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/getzep
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release-integrations.yml@ace6d3501dbffa80d522752ad862fe43ac6bcda0 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file zep_ag2-0.2.0-py3-none-any.whl.
File metadata
- Download URL: zep_ag2-0.2.0-py3-none-any.whl
- Upload date:
- Size: 29.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
aacce2ad5f6f05a64df9221d3c005dded2a3efccf99fb2d57343f29323b5b46d
|
|
| MD5 |
afa0accb5e9c20d50d518a9b1dc5512f
|
|
| BLAKE2b-256 |
148c95d4578fa961a5ed0f2f75482da4a120e82c8287bf7ed775db420d3cc6b4
|
Provenance
The following attestation bundles were made for zep_ag2-0.2.0-py3-none-any.whl:
Publisher:
release-integrations.yml on getzep/zep
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
zep_ag2-0.2.0-py3-none-any.whl -
Subject digest:
aacce2ad5f6f05a64df9221d3c005dded2a3efccf99fb2d57343f29323b5b46d - Sigstore transparency entry: 2138785782
- Sigstore integration time:
-
Permalink:
getzep/zep@ace6d3501dbffa80d522752ad862fe43ac6bcda0 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/getzep
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release-integrations.yml@ace6d3501dbffa80d522752ad862fe43ac6bcda0 -
Trigger Event:
workflow_dispatch
-
Statement type: