MemorySync
Getting Started

Migrating from Zep

Zep attaches each conversation thread to a user, and that user’s knowledge graph collects facts across threads. MemorySync has the same two ideas under different names: who a memory belongs to, and which conversation produced it. Mapping them is the one step worth getting right before you start.

How the concepts map

Key differences

ZepMemorySyncWhat it means for your code
user.add() and thread.create() before writingNo setup callScope is a header on the write itself.
thread.add_messages(thread_id, messages=[...])POST /v1/history/append or POST /memory/addBoth extract facts from what you send; history append also keeps the transcript for your app to read back.
graph.search(user_id=..., query=...) or thread.get_user_context(thread_id)POST /memory/query with query and kReads span the end user, and session_id narrows when you want that.
A thread’s messagesPOST /v1/history/list for the transcript; GET /memory/export for the factsThe transcript and the facts are kept apart, so reading one never returns the other.
Messages with rolesRecords with source and metadataRole becomes descriptive context you can filter on.

Install the client

pip install memorysync
import os
from memorysync import MemorySyncClient
client = MemorySyncClient(
api_key=os.environ["MEMORYSYNC_API_KEY"],
base_url="https://api.memorysync.io",
project_id=os.environ.get("MEMORYSYNC_PROJECT_ID") or None,
end_user_id="user-123",
)

Operation by operation

from zep_cloud.types import Message
client.user.add(user_id="user_123")
client.thread.create(thread_id="thread_42", user_id="user_123")
client.thread.add_messages("thread_42", messages=[
Message(name="Alice", role="user", content="I love Python for backend work"),
])
results = client.graph.search(
user_id="user_123",
query="programming",
limit=3,
)

Move your episodes with one command

The CLI reads a Zep export, or walks your account through the API — users first, then each user’s episodes — and sends every episode into that user’s scope through the same fact extraction as POST /memory/bulk-add, carrying the thread id through as session_id. Only the durable facts in each episode are stored, never the episode text as a transcript row. Start with --dry-run: it reports the per-user breakdown, the user and assistant split, and everything it would skip, without writing anything.

# See what would happen. Writes nothing.
npx memorysync-cli migrate zep --file zep_export.json --dry-run
# Do it.
npx memorysync-cli migrate zep --file zep_export.json
What it doesWhy
Migrates episodes, not the derived graphZep keeps episodes verbatim as the original source, alongside the nodes and edges it derives from them. The graph is Zep’s own representation and has no equivalent here, so porting it would mean holding a model nothing reads.
Writes each episode under its own user, and carries thread_id into session_idThis is the split described above, applied. The user is the isolation boundary; the thread only narrows a query.
Sends both sides of the conversation and reports the splitAn assistant episode is what the model said. Like every imported record it goes through fact extraction, so only the durable facts in it are kept, filed under the user — right for some products, wrong for others. Dropping it by default would be silent data loss, so you get the counts and --role user if you want only the human turns.
Skips json and fact_triple episodes, and reports how manyA fact triple is a serialised graph edge and JSON is a business record. Stored as memory text they are strings retrieval can match but never usefully explain. --include-source json,fact_triple migrates them anyway.
Skips deleted usersTheir episodes are data the customer asked you to remove. Migrating it would resurrect it somewhere new.
Keeps role, role_type, source and the original timestamp in metadataWho said it is context about a record, not part of its identity, so it stays filterable without becoming the scope.
Sends every episode uuid as client_refRe-running is safe. Episodes that already landed are reported as already migrated rather than stored twice.

Or move them yourself

The command above is these steps, done for you. Do it by hand when you are building it into your own pipeline rather than running a one-time migration.

  1. 1List the threads you want to bring across (sessions, in older Zep SDKs), and read each thread’s user id. That id becomes the end-user scope.
  2. 2Decide whether to send the assistant’s messages too. Every message goes through fact extraction, so small talk is skipped either way, but facts the assistant stated would be filed under the user.
  3. 3Import per session, carrying the role in metadata and the session id in session_id.
  4. 4Check the per-item results, then query as one migrated user to confirm the scope is right.
import os
from memorysync import MemorySyncClient
# messages and user_id come from your Zep export
def import_session(session_id: str, user_id: str, messages: list[dict]) -> None:
client = MemorySyncClient(
api_key=os.environ["MEMORYSYNC_API_KEY"],
base_url="https://api.memorysync.io",
project_id=os.environ.get("MEMORYSYNC_PROJECT_ID") or None,
end_user_id=user_id,
)
items = [
{
"text": message["content"],
"source": "import",
# the uuid makes re-running safe; without it a retry stores a second copy
"client_ref": message.get("uuid"),
"metadata": {"role": message.get("role"), "zep_uuid": message.get("uuid")},
}
for message in messages
if message.get("content", "").strip()
]
result = client.bulk_add(items)
print(session_id, result.created, "created,", result.skipped, "skipped")

Next steps

Was this page helpful?