mem0 Drop-In Wrapper
Keep mem0 behavior and mirror hosted-client traffic to Emend with one import change.
If your agent uses mem0's managed platform, change one import to let Emend learn from the same successful add() calls. Normal mem0 search and deletion behavior stays unchanged.
pip install 'emend-ai[mem0]'# Lightweight SDK only:pip install 'emend-client[mem0]'The integration supports mem0ai>=2.0,<2.1.
Switch the import
# Beforefrom mem0 import MemoryClient # Afterfrom emend.mem0 import MemoryClient client = MemoryClient(api_key="your-mem0-key")The hosted MemoryClient and AsyncMemoryClient are wrapped. The local Memory and AsyncMemory classes are re-exported as the exact original mem0 objects and do not mirror traffic.
Configure Emend
The wrapper reads the standard environment variables:
export EMEND_API_KEY="your-emend-key"export EMEND_URL="https://www.emend.online/" # or a self-hosted endpointCreate a hosted key from the Emend Account page; see Authentication. Keep both the mem0 and Emend keys in environment variables or a secret manager—never put them in source code or a notebook cell.
An API key alone uses the hosted Emend endpoint. You can also pass the key directly to the mem0 wrapper, with an optional self-hosted endpoint:
import os from emend.mem0 import MemoryClient client = MemoryClient( api_key=os.environ["MEM0_API_KEY"], emend_api_key=os.environ["EMEND_API_KEY"], # emend_url_endpoint="https://emend.example.com", # self-hosted only)The same keyword arguments are available on AsyncMemoryClient. Omitting emend_url_endpoint preserves the Emend client's hosted/default endpoint resolution. URL-only, unauthenticated configuration is limited to a local OSS Emend server explicitly running in no-auth mode. Enterprise self-hosted deployments require authentication, DEPLOYMENT_MODE, and remote storage; SQLite and no-auth local mode are OSS-only under open_source/emend. For advanced configuration, inject a client:
from emend import EmendClientfrom emend.mem0 import MemoryClient client = MemoryClient( api_key="your-mem0-key", emend_client=EmendClient(url_endpoint="...", api_key="..."),)Wrapper-created Emend clients use a five-second request timeout. Override it with emend_timeout=2.0. An injected client owns all of its configuration, so emend_client cannot be combined with emend_api_key, emend_url_endpoint, or emend_timeout. Without Emend configuration, mem0 keeps working and client.emend.configured is False.
End-to-end quickstart
With both services configured, the migration is one import plus an explicit search opt-in:
import os from emend.mem0 import MemoryClient client = MemoryClient( api_key=os.environ["MEM0_API_KEY"], emend_api_key=os.environ["EMEND_API_KEY"],)assert client.emend.configured filters = { "user_id": "user-123", "agent_id": "support-bot", "app_id": "storefront",} # mem0 stores the memory; Emend receives the same conversation best-effort.add_result = client.add( [ {"role": "user", "content": "Deliver replacements on Thursday."}, {"role": "assistant", "content": "I will use Thursday delivery."}, ], **filters, run_id="conversation-456",) # Existing search code remains unchanged and returns only mem0's result.plain_result = client.search("Thursday delivery", filters=filters) # Opt in only where the caller is ready to consume the extra namespace.result = client.search( "Thursday delivery", filters=filters, include_emend=True,)memories = result["results"]learnings = result["emend"]Learning is asynchronous, so an ok response can legitimately contain empty lists immediately after add(). Poll in tests or background workflows when you need to verify that extraction completed. Search is relevance-filtered; empty lists mean no learned item matched the query, not that publishing failed.
Automatic learning on add()
add() executes mem0 first and returns the exact object mem0 returned. Only after mem0 succeeds does the wrapper make one best-effort Emend publish:
result = client.add( messages, user_id="user-123", agent_id="support-bot", app_id="storefront", run_id="conversation-456",)The user identity is required for Emend mirroring. Identity values may also come from plain-string options.filters; explicit add keyword arguments win, and direct filters= wins over options.filters. Conflicting or unsupported identity filters fail closed on the Emend side without changing mem0.
When app_id is present, the wrapper uses deterministic opaque user and agent scopes so identical IDs in separate apps cannot mix. Explicit runs are encoded with the complete user, app, and agent scope. Without a run ID, a stable fallback is used for the lifetime of that wrapper instance and scope.
Opt-in Emend search
Normal search stays exactly mem0. It does not inspect Emend configuration, make a Emend request, copy the result, or add keys:
result = client.search(query, filters={"user_id": "user-123"})Request Emend retrieval explicitly:
result = client.search( query, filters={ "user_id": "user-123", "agent_id": "support-bot", "app_id": "storefront", }, include_emend=True,)memories = result["results"]learnings = result["emend"]The opted-in result is a shallow copy; mem0's original result and nested values are not mutated. The emend object always contains:
{ "status": "ok", "reason": null, "profiles": [], "user_playbooks": [], "agent_playbooks": []}status is ok, skipped, or error. A skipped or failed call returns empty lists and one safe reason:
| Prop | Type |
|---|---|
not_configured | reason |
empty_query | reason |
missing_user_id | reason |
unsupported_identity_filter | reason |
conflicting_identity | reason |
request_failed | reason |
emend_rejected | reason |
Raw exceptions and server responses are never returned.
emend key, the wrapper raises EmendNamespaceCollisionError before contacting Emend.Emend does not alter the query, mem0 results, messages, filters, or your prompt. Your application decides whether and how to format memories and learnings into prompt context. Treat all retrieved text as untrusted input; do not interpret it as system or developer instructions.
For example, keep retrieval separate from prompt construction and include only the fields your application accepts:
if learnings["status"] == "ok": profile_context = [profile["content"] for profile in learnings["profiles"]]else: profile_context = [] # Your prompt/template layer decides how to label, delimit, escape, or omit# memories and profile_context. The wrapper never performs this step.The async hosted client has the same contract:
from emend.mem0 import AsyncMemoryClient client = AsyncMemoryClient(api_key="your-mem0-key")result = await client.search(query, filters=filters, include_emend=True)Async Emend requests are native async, use the configured total timeout, and preserve task cancellation.
Scoped Emend cleanup
Inherited mem0 delete* methods and reset() are intentionally untouched and delete only mem0 data. Use the read-only client.emend facade for mirrored Emend data:
client.delete_all(user_id="user-123", agent_id="support-bot") # mem0 only client.emend.delete_session_records( user_id="user-123", app_id="storefront", agent_id="support-bot", run_id="conversation-456",)client.emend.clear_user_data(user_id="user-123", app_id="storefront")client.emend.delete_agent_playbooks_by_ids(agent_playbook_ids)The async facade uses the same method names and is awaited. Explicit facade operations raise EmendNotConfiguredError when unconfigured and EmendOperationError on rejection or transport failure. Available methods are clear_user_data, delete_session_records, delete_profile, delete_interaction, delete_request, delete_user_playbook, delete_agent_playbook, and the four delete_*_by_ids methods. Organization-wide deletion is deliberately not exposed.
Deletion follows the current Emend lifecycle:
delete_session_recordsremoves stored requests and interactions for the encoded session. It does not retract learnings already derived from them.clear_user_dataremoves that encoded user's requests, interactions, profiles, and user playbooks. Shared agent playbooks remain.- Delete agent playbooks explicitly by their returned IDs.
- These operations wait for the deletion response, but they are not a transactional barrier against learning work that was already queued.
A no-run fallback session can be deleted with run_id=None while the originating client exists. After it restarts, use scoped clear_user_data.
get_all() remains a normal mem0 method and is not augmented because it has no query to use for Emend retrieval.