Zep Microsoft Agent Framework Integration
Long-term memory for Microsoft Agent Framework agents, backed by Zep's temporal Context Graph. Persists conversation turns and injects relevant context into the model on every run.
Installation
pip install zep-ms-agent-framework
The package depends only on agent-framework-core. The runnable example also uses a model provider:
pip install zep-ms-agent-framework agent-framework-openai
Identifiers
Zep v4 addresses a user, a thread, and a graph by a server-generated UUID. A v4 create call takes no client-chosen name, and the create response carries the UUID. The provider takes user_uuid, thread_uuid, and an optional graph_uuid.
Create the user and the thread one time, read the UUIDs from the create responses, and store the UUIDs in your own database. The provider does not look up a UUID at run time.
Quick Start
Attach a ZepContextProvider to an agent through the context_providers keyword argument:
import asyncio
from agent_framework import Agent
from agent_framework.openai import OpenAIChatClient
from zep_cloud.client import AsyncZep
from zep_ms_agent_framework import ZepContextProvider, create_thread, create_user
zep = AsyncZep(api_key="your-zep-api-key")
async def main() -> None:
# One-time provisioning. Store these UUIDs in your own database.
user = await create_user(zep, first_name="Jane", last_name="Smith")
thread = await create_thread(zep, user_uuid=user.uuid_)
agent = Agent(
OpenAIChatClient(model="gpt-5-mini"),
instructions="You are a helpful assistant with long-term memory.",
context_providers=[
ZepContextProvider(
zep_client=zep,
user_uuid=user.uuid_,
thread_uuid=thread.uuid_,
graph_uuid=user.graph_uuid,
)
],
)
result = await agent.run("Hi, I'm a data scientist in Portland.")
print(result.text)
asyncio.run(main())
How It Works
The integration ships one class — ZepContextProvider — that subclasses Agent Framework's ContextProvider and overrides the two lifecycle hooks the framework calls around every agent.run(...). See src/zep_ms_agent_framework/context_provider.py.
before_run
Runs before the model is invoked. On each turn it:
- Registers the graph-search tool via
context.extend_tools(...), ifexpose_search_tool=True(seecreate_zep_search_toolbelow). - Extracts the latest user message from
context.input_messages. - Persists the message — via
thread.add_messages(return_context=True)by default (a single round-trip), or concurrently with a customcontext_builderif one is set (see Custom context building). - Injects the resulting context block, wrapped in
context_template, into the model's instructions viacontext.extend_instructions(...).
after_run
Runs after the model responds. It reads the assistant reply from context.response.messages and persists it to the same Zep thread, so both sides of the conversation are captured.
Because thread.get_context (and add_messages(return_context=True)) assemble context from the entire user graph, the thread only scopes relevance — an agent on a new thread still recalls facts the same user shared earlier.
Custom context building
Set context_builder on ZepContextProvider to replace the default context retrieval with custom logic — for example, searching a different graph, applying filters, or combining multiple sources:
from zep_ms_agent_framework import ContextInput, ZepContextProvider
async def my_builder(ctx: ContextInput) -> str | None:
if ctx.graph_uuid is None:
return None
pager = await ctx.zep.graph.search_edges(
ctx.graph_uuid,
query=ctx.user_message,
page_size=10,
)
edges = pager.items or []
if not edges:
return None
return "\n".join(edge.fact for edge in edges if edge.fact)
provider = ZepContextProvider(
zep_client=zep,
user_uuid=user.uuid_,
thread_uuid=thread.uuid_,
graph_uuid=user.graph_uuid,
context_builder=my_builder,
)
ContextInput bundles zep (the AsyncZep client), user_uuid, thread_uuid, graph_uuid, user_message, and session_context (the Agent Framework SessionContext for the turn).
When context_builder is set, message persistence (add_messages without return_context) and the builder run concurrently, with per-side failure isolation:
- If the builder raises, a warning is logged and context injection is skipped for that turn — but persistence still completes and the turn is marked as persisted.
- If persistence raises, a warning is logged and the turn is not marked as persisted (so
after_runskips writing the assistant reply, and the turn can be retried next invocation) — but a successful builder result is still injected.
context_template
Controls how retrieved context is wrapped before injection. Must contain a literal {context} placeholder, rendered via plain string replacement (template.replace("{context}", context), never str.format) — so context text containing {, }, or % is always safe to inject:
provider = ZepContextProvider(
zep_client=zep,
user_uuid=user.uuid_,
thread_uuid=thread.uuid_,
context_template="Relevant memory:\n{context}",
)
Defaults to DEFAULT_CONTEXT_TEMPLATE, an explicit <ZEP_CONTEXT>...</ZEP_CONTEXT> block — the same canonical wording used across zep-adk's Python, Go, and TypeScript implementations, and identical to this package's previous hardcoded output.
Provisioning
create_user and create_thread (in zep_ms_agent_framework.provisioning) provision the Zep user and thread out-of-band, before the first run. A v4 create call takes no client-chosen name. The server generates the UUIDs, and the create response carries them:
from zep_ms_agent_framework import create_thread, create_user
async def setup_user(zep_client, user_uuid: str) -> None: ... # e.g. configure per-user ontology
user = await create_user(
zep,
first_name="Jane",
last_name="Smith",
email="jane@example.com",
on_created=setup_user, # runs one time, after the user is created
)
thread = await create_thread(zep, user_uuid=user.uuid_)
# Store user.uuid_, user.graph_uuid, and thread.uuid_ in your own database.
A failure of the create call, or of the on_created hook, propagates to the caller. Make the hook idempotent so that you can run it again after a failure.
The provider does not create a user or a thread. Give it the UUIDs that these helpers return.
create_zep_search_tool / expose_search_tool
create_zep_search_tool (in zep_ms_agent_framework.search) returns a model-callable agent_framework.FunctionTool over the v4 graph search methods. The model decides when to search the knowledge graph for specific facts, entities, or prior episodes. The tool searches the graph that graph_uuid identifies: give the graph_uuid of a user for personal memory, or the uuid_ of a standalone graph, such as a documentation knowledge base, for shared knowledge.
The easiest way to use it is expose_search_tool=True on ZepContextProvider, which builds the tool once at construction and registers it on every run via context.extend_tools(...):
provider = ZepContextProvider(
zep_client=zep,
user_uuid=user.uuid_,
thread_uuid=thread.uuid_,
graph_uuid=user.graph_uuid,
expose_search_tool=True,
search_pinned_params={"scope": "nodes", "limit": 5},
)
expose_search_tool=True requires a graph_uuid.
Pin-or-expose. Every search parameter (scope, reranker, limit, mmr_lambda, center_node_uuid) is exposed to the model in the tool's JSON schema by default, with documented defaults. Use search_pinned_params to fix a parameter to a constant value and hide it from the schema; use search_hidden_params to hide a parameter without pinning it, so Zep's own server-side default applies:
from zep_ms_agent_framework.search import create_zep_search_tool
# Model chooses scope/reranker/limit/mmr_lambda/center_node_uuid freely.
tool = create_zep_search_tool(zep_client=zep, graph_uuid=user.graph_uuid)
# Pin scope to "nodes" and limit to 5 -- hidden from the model, always sent.
tool = create_zep_search_tool(
zep_client=zep,
graph_uuid=user.graph_uuid,
search_pinned_params={"scope": "nodes", "limit": 5},
)
# Hide mmr_lambda from the schema; Zep applies its own default when omitted.
tool = create_zep_search_tool(
zep_client=zep,
graph_uuid=user.graph_uuid,
search_hidden_params={"mmr_lambda"},
)
search_filters and bfs_origin_node_uuids are always constructor-only (their complex shapes are not exposed to the model).
Search filters
Each search scope accepts only its own search_filters keys. The default scope is edges. To use a filter of a different scope, pin that scope:
# Edge filters on the default "edges" scope.
tool = create_zep_search_tool(
zep_client=zep,
graph_uuid=user.graph_uuid,
search_filters={"edge_types": ["WORKS_AT"]},
)
# Node filters need the "nodes" scope.
tool = create_zep_search_tool(
zep_client=zep,
graph_uuid=user.graph_uuid,
search_pinned_params={"scope": "nodes"},
search_filters={"node_labels": ["Person"]},
)
| Scope | Supported search_filters keys |
|---|---|
edges |
edge_types, exclude_edge_types, edge_uuids, episode_uuids, connected_node_uuids, source_node_uuids, target_node_uuids, metadata_filters, date_filters, property_filters |
nodes |
node_labels, exclude_node_labels, episode_uuids, metadata_filters, property_filters |
episodes |
metadata_filters, content_policy_violated |
observations |
None |
thread_summaries |
metadata_filters |
auto |
The keys of edges and nodes |
If a key does not suit the pinned scope, or the default edges scope when the scope is not pinned, create_zep_search_tool raises ValueError. The error names the key, the scope, and the scopes that support the key. If the model selects a scope that does not support a key, the tool returns the same message to the model and does not call Zep.
Identity and Threads
Memory is scoped per ZepContextProvider instance to one user_uuid + thread_uuid. For a multi-user application, construct one provider (and one agent, or one agent per request) per user and conversation. Give real names to create_user so that Zep can resolve the user's identity node in the graph.
Per-run identity is bound at construction, not resolved per-run. The Agent Framework's
AgentSession/SessionContextcarry no identity field, and there is no framework convention for stashing identity insession.state(unlike e.g. Google ADK'stool_context.state["zep_user_id"]pattern) -- this was investigated and is documented in theZepContextProviderclass docstring. If a future Agent Framework release adds per-run identity, this is the extension point to revisit.
| Field | Required | Default | Description |
|---|---|---|---|
zep_client |
Yes | — | Initialised AsyncZep client (caller owns its lifecycle) |
user_uuid |
Yes | — | UUID of the Zep user this provider's memory is scoped to |
thread_uuid |
Yes | — | UUID of the Zep thread the conversation is recorded in |
graph_uuid |
Optional | None |
UUID of the graph to search; required when expose_search_tool is True |
user_message_name |
Optional | full name of the Zep user | Display name on persisted user messages. The provider reads the name one time with user.get on the first turn |
assistant_message_name |
Optional | "Assistant" |
Display name on persisted assistant messages |
source_id |
Optional | "zep" |
Agent Framework attribution ID for injected instructions/tools |
ignore_roles |
Optional | None |
Roles to exclude from graph ingestion (still stored in thread history) |
context_builder |
Optional | None |
Custom async context-retrieval callable; see Custom context building |
context_template |
Optional | DEFAULT_CONTEXT_TEMPLATE |
Template wrapping injected context; see context_template |
expose_search_tool |
Optional | False |
Register a model-callable graph-search tool every run; see create_zep_search_tool |
search_pinned_params |
Optional | None |
Fix a search parameter to a value; hidden from the model schema |
search_hidden_params |
Optional | None |
Hide a search parameter from the schema without pinning (Zep's default applies) |
search_filters |
Optional | None |
Constructor-only Zep search filters. Each scope accepts only its own keys; see Search filters |
bfs_origin_node_uuids |
Optional | None |
Constructor-only node UUIDs for BFS seeding |
Features
- Native context-provider hook — uses Agent Framework's own
before_run/after_runpipeline, the same surface as the framework's built-in memory providers. - Single round-trip — persists the user turn and retrieves the Context Block in one call (or concurrently, with a custom
context_builder). - UUID addressing — the provider takes the server-generated UUIDs and makes no lookup call on the turn path.
- Whole-user-graph recall — context is fused across all of the user's threads and data.
- Per-user setup hook —
create_user(on_created=...)for configuring ontology, custom instructions, or user summary instructions. - Pin-or-expose search tool —
expose_search_tool/create_zep_search_toolfor on-demand graph search, with every search parameter model-exposed by default or pinned/hidden per deployment. - Graceful error handling — a Zep failure is logged but never crashes the host agent; the agent degrades to memoryless for that turn.
- Async-only, client-agnostic — requires
AsyncZep; works with any Agent Framework chat client.
Configuration
# Required
export ZEP_API_KEY="your-zep-api-key"
export OPENAI_API_KEY="your-openai-api-key" # for the example / live tests
See SETUP.md for signing up, creating an API key, and running the example end to end.
Examples
- examples/basic_agent.py — a single agent seeding facts in one thread and recalling them in a new thread (cross-thread recall).
Development
git clone https://github.com/getzep/zep.git
cd zep/integrations/ms-agent-framework/python
make install # uv sync --extra dev
make all # format + lint + type-check + test
| Command | Description |
|---|---|
make format |
Format code with ruff |
make lint |
Run linting checks |
make type-check |
Run mypy type checking |
make test |
Run the test suite (integration tests skip without API keys) |
make all |
Run all checks |
make build |
Build the package |
Live integration tests run only when ZEP_API_KEY is set; the agent-driven lifecycle test additionally requires OPENAI_API_KEY (it is skipped, not failed, when that key is absent):
uv run pytest tests/test_integration.py -v -s -m integration
Requirements
- Python 3.11+
zep-cloud>=4.0.0,<5agent-framework-core>=1.8.1
Support
License
Apache 2.0 — see LICENSE for details.
Contributing
Contributions are welcome! Please see our Contributing Guide for details.
Metadata
Release files for zep-ms-agent-framework 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_ms_agent_framework-1.0.0.tar.gz | 41.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| zep_ms_agent_framework-1.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 66.5 kB
Release files / zep_ms_agent_framework-1.0.0.tar.gz
| Download URL | zep_ms_agent_framework-1.0.0.tar.gz |
|---|---|
| Size | 41.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
2e0713f3b8d55c434d8ddf7524f44cbe2682d0062cabd05e710da5ed232dd92b
|
|
BLAKE2b-256 checksum How to use checksums |
347109bc95dd1b19176c311e43ba50fa15d4828836ac97bf50aeabd6e262fd0d
|
| 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_ms_agent_framework-1.0.0-py3-none-any.whl
| Download URL | zep_ms_agent_framework-1.0.0-py3-none-any.whl |
|---|---|
| Size | 25.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b4faa51b1d0280995a735912038ff2922b17704b1a9d5c80e81ffe2d2622818b
|
|
BLAKE2b-256 checksum How to use checksums |
d7cba92f158ba2746fa88ee047820dc972d7e1676a1d69fa58a472adbbfda3d3
|
| 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