Skip to main content

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:

  1. Registers the graph-search tool via context.extend_tools(...), if expose_search_tool=True (see create_zep_search_tool below).
  2. Extracts the latest user message from context.input_messages.
  3. Persists the message — via thread.add_messages(return_context=True) by default (a single round-trip), or concurrently with a custom context_builder if one is set (see Custom context building).
  4. Injects the resulting context block, wrapped in context_template, into the model's instructions via context.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_run skips 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/SessionContext carry no identity field, and there is no framework convention for stashing identity in session.state (unlike e.g. Google ADK's tool_context.state["zep_user_id"] pattern) -- this was investigated and is documented in the ZepContextProvider class 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_run pipeline, 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_tool for 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,<5
  • agent-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)

Source distribution for zep-ms-agent-framework 1.0.0
File Size Uploaded
zep_ms_agent_framework-1.0.0.tar.gz 41.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for zep-ms-agent-framework 1.0.0
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page