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
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
zep = AsyncZep(api_key="your-zep-api-key")
agent = Agent(
OpenAIChatClient(model="gpt-5-mini"),
instructions="You are a helpful assistant with long-term memory.",
context_providers=[
ZepContextProvider(
zep_client=zep,
user_id="user-123",
thread_id="thread-abc",
first_name="Jane",
last_name="Smith",
email="jane@example.com", # optional
)
],
)
async def main() -> None:
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. - Creates the Zep user and thread lazily on first use (cached thereafter) via
ensure_user/ensure_thread. - 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_user_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:
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)
provider = ZepContextProvider(
zep_client=zep,
user_id="user-123",
thread_id="thread-abc",
context_builder=my_builder,
)
ContextInput bundles zep (the AsyncZep client), user_id, thread_id, 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_id="user-123",
thread_id="thread-abc",
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
ensure_user and ensure_thread (in zep_ms_agent_framework.provisioning) explicitly provision the Zep user and thread out-of-band, before the first run — useful for onboarding flows that want genuine failures (auth, network, 5xx) to raise loudly rather than degrade silently:
from zep_ms_agent_framework import ensure_thread, ensure_user
async def setup_user(zep_client, user_id: str) -> None:
... # e.g. configure per-user ontology
created = await ensure_user(
zep,
user_id="user-123",
first_name="Jane",
last_name="Smith",
email="jane@example.com",
on_created=setup_user, # fires exactly once, only on real creation
)
await ensure_thread(zep, thread_id="thread-abc", user_id="user-123")
Both are create-then-catch-conflict: they call the Zep SDK's create method directly and treat an "already exists" conflict as success (returning False), while genuine failures propagate. If on_created raises, that exception also propagates even though the user was created — make the hook idempotent so it can be safely re-run.
You do not have to call these explicitly: ZepContextProvider.before_run calls the same logic lazily on the turn path, but wrapped so that a genuine failure there (including an on_created hook failure) is logged and degrades to skipping that turn rather than breaking the run. Contrast this with calling ensure_user/ensure_thread directly, out-of-band, where the same failures propagate to the caller.
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 graph.search. The model decides when to search the knowledge graph for specific facts, entities, or prior episodes. By default it searches the given user's graph; pass graph_id=... to target a shared standalone graph (e.g. a documentation knowledge base) instead.
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_id="user-123",
thread_id="thread-abc",
expose_search_tool=True,
search_pinned_params={"scope": "nodes", "limit": 5},
)
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, user_id="user-123")
# Pin scope to "nodes" and limit to 5 -- hidden from the model, always sent.
tool = create_zep_search_tool(
zep_client=zep, user_id="user-123",
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, user_id="user-123", 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).
Identity and Threads
Memory is scoped per ZepContextProvider instance to one user_id + thread_id. For a multi-user application, construct one provider (and one agent, or one agent per request) per user/conversation, passing real names so 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 nouser_id-shaped 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_id |
Yes | — | Zep user ID this provider's memory is scoped to |
thread_id |
Yes | — | Zep thread ID the conversation is recorded in |
first_name |
Recommended | None |
User first name — helps Zep anchor identity |
last_name |
Optional | None |
User last name |
email |
Optional | None |
User email |
user_message_name |
Optional | full name | Display name on persisted user messages |
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) |
on_user_created |
Optional | None |
Async hook run once after a new user is created (ontology / instructions setup); see Provisioning |
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 (node_labels, edge_types, etc.) |
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). - Lazy resource creation — the Zep user and thread are created on first run and cached, via the same
ensure_user/ensure_threadhelpers available for out-of-band provisioning. - Whole-user-graph recall — context is fused across all of the user's threads and data.
- Per-user setup hook —
on_user_createdfor 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>=3.23.0agent-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.
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_ms_agent_framework-0.2.1.tar.gz.
File metadata
- Download URL: zep_ms_agent_framework-0.2.1.tar.gz
- Upload date:
- Size: 40.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ef7c08540c4656482455c661cd59977898be2ddc0495a1651c0d28150a4896c9
|
|
| MD5 |
becf2ef15880201b159171f753ac7d62
|
|
| BLAKE2b-256 |
7ae263effbf3275f8fb1fa85e113144f35ea2ae426cb362ae2208391842fe9b3
|
Provenance
The following attestation bundles were made for zep_ms_agent_framework-0.2.1.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_ms_agent_framework-0.2.1.tar.gz -
Subject digest:
ef7c08540c4656482455c661cd59977898be2ddc0495a1651c0d28150a4896c9 - Sigstore transparency entry: 2286337338
- Sigstore integration time:
-
Permalink:
getzep/zep@74ccf8cbf3bdbefe87d68cde71b228f58901a72a -
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@74ccf8cbf3bdbefe87d68cde71b228f58901a72a -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file zep_ms_agent_framework-0.2.1-py3-none-any.whl.
File metadata
- Download URL: zep_ms_agent_framework-0.2.1-py3-none-any.whl
- Upload date:
- Size: 25.7 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 |
61057b357715309895b981529f5a8787be1d70b56928bdc8398e0da83f886315
|
|
| MD5 |
fb8146fcba9ceccbd031fc0c75068e9c
|
|
| BLAKE2b-256 |
8d2a1e785f8ea4b56e40e3e925d2c6cc55e743d778d776ce9fb6a865a932da57
|
Provenance
The following attestation bundles were made for zep_ms_agent_framework-0.2.1-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_ms_agent_framework-0.2.1-py3-none-any.whl -
Subject digest:
61057b357715309895b981529f5a8787be1d70b56928bdc8398e0da83f886315 - Sigstore transparency entry: 2286337437
- Sigstore integration time:
-
Permalink:
getzep/zep@74ccf8cbf3bdbefe87d68cde71b228f58901a72a -
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@74ccf8cbf3bdbefe87d68cde71b228f58901a72a -
Trigger Event:
workflow_dispatch
-
Statement type: