Compare commits

..
Author SHA1 Message Date
karthikandClaude Opus 4.8 1c448b3eec docs(llms): add Dream API reference pages to llms.txt
The check-llms-txt CI (and the CI Gate that aggregates it) failed because the 8
new Dream API-reference pages weren't listed in docs/llms.txt. Add a Dream
subsection [Platform] between Projects and Webhooks. `check-llms-txt-coverage.py`
now reports in sync.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-27 18:48:29 +05:30
karthikandClaude Opus 4.8 806e15e927 fix(api-reference): use nullable array for dream activity sources
`type: null` is not valid OpenAPI 3.0.1, so the spec failed validation and
Mintlify rendered the Dream pages blank. Represent the always-null `sources`
field on the activity feed as a nullable array (parity with the runs feed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-25 20:47:15 +05:30
karthikandClaude Opus 4.8 6f804a8a20 docs(api-reference): add Dream (memory synthesis) endpoints
Dream shipped to prod but its API endpoints weren't in the API reference. Add
the org/project-scoped Dream endpoints (OpenAPI spec + per-endpoint pages + nav):

- GET/PATCH dream/config/  — get / update Synthesis config + plan entitlements
- GET  dream/stats/        — lifecycle + synthesis counts
- GET  dream/activity/     — supersede/merge feed (keyset-paginated)
- GET  dream/runs/         — synthesis runs feed
- GET  dream/runs/{run_id}/memories/       — memories within a run
- GET  dream/memory/{memory_id}/sources/   — a synthesized memory's sources
- POST dream/preview/      — no-write scope preview

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-25 19:58:25 +05:30
15 changed files with 213 additions and 1144 deletions
+171 -152
View File
@@ -1,8 +1,6 @@
# Mem0 Python SDK
<p align="center">
<a href="https://github.com/mem0ai/mem0">
<img src="docs/images/banner-sm.png" width="800px" alt="Mem0, the memory layer for personalized AI">
<img src="docs/images/banner-sm.png" width="800px" alt="Mem0 - The Memory Layer for Personalized AI">
</a>
</p>
<p align="center" style="display: flex; justify-content: center; gap: 20px; align-items: center;">
@@ -24,187 +22,166 @@
<img src="https://img.shields.io/badge/Discord-%235865F2.svg?&logo=discord&logoColor=white" alt="Mem0 Discord">
</a>
<a href="https://pepy.tech/project/mem0ai">
<img src="https://img.shields.io/pypi/dm/mem0ai" alt="Mem0 PyPI downloads">
<img src="https://img.shields.io/pypi/dm/mem0ai" alt="Mem0 PyPI - Downloads">
</a>
<a href="https://github.com/mem0ai/mem0">
<img src="https://img.shields.io/github/commit-activity/m/mem0ai/mem0?style=flat-square" alt="GitHub commit activity">
</a>
<a href="https://pypi.org/project/mem0ai" target="blank">
<img src="https://img.shields.io/pypi/v/mem0ai?color=%2334D058&label=pypi%20package" alt="PyPI package version">
<img src="https://img.shields.io/pypi/v/mem0ai?color=%2334D058&label=pypi%20package" alt="Package version">
</a>
<a href="https://www.npmjs.com/package/mem0ai" target="blank">
<img src="https://img.shields.io/npm/v/mem0ai" alt="Npm package">
</a>
<a href="https://www.ycombinator.com/companies/mem0">
<img src="https://img.shields.io/badge/Y%20Combinator-S24-orange?style=flat-square" alt="Y Combinator S24">
</a>
</p>
Mem0 gives AI assistants and agents persistent memory. It extracts useful facts from conversations, scopes them to a user, agent, or run, and retrieves the relevant facts for later interactions. The Python package includes `MemoryClient` for the hosted Mem0 Platform and `Memory` for open-source, in-process memory.
<p align="center">
<a href="https://mem0.ai/research"><strong>📄 Benchmarking Mem0's token-efficient memory algorithm →</strong></a>
</p>
## Requirements
## New Memory Algorithm (April 2026)
- Python 3.10 or later
- Hosted Platform: `MEM0_API_KEY` from the [Mem0 dashboard](https://app.mem0.ai/dashboard/api-keys)
- Open source with the default providers: `OPENAI_API_KEY`
| Benchmark | Old | New | Tokens | Latency p50 |
| --- | --- | --- | --- | --- |
| **LoCoMo** | 71.4 | **92.5** | 7.0K | 0.88s |
| **LongMemEval** | 67.8 | **94.4** | 6.8K | 1.09s |
| **BEAM (1M)** | — | **64.1** | 6.7K | 1.00s |
| **BEAM (10M)** | — | **48.6** | 6.9K | 1.05s |
## Install
All benchmarks run on the same production-representative model stack. Single-pass retrieval (one call, no agentic loops) at a top_200 retrieval budget. Scores reflect Mem0's managed platform, which includes proprietary optimizations not available in the open-source SDK; open-source users should expect directionally similar gains but not identical numbers.
**What changed:**
- **Single-pass ADD-only extraction** -- one LLM call, no UPDATE/DELETE. Memories accumulate; nothing is overwritten.
- **Agent-generated facts are first-class** -- when an agent confirms an action, that information is now stored with equal weight.
- **Entity linking** -- entities are extracted, embedded, and linked across memories for retrieval boosting.
- **Multi-signal retrieval** -- semantic, BM25 keyword, and entity matching scored in parallel and fused.
- **Temporal Reasoning** -- time-aware retrieval that ranks the right dated instance for queries about current state, past events, and upcoming plans.
See the [migration guide](https://docs.mem0.ai/migration/oss-v2-to-v3) for upgrade instructions. The [evaluation framework](https://github.com/mem0ai/memory-benchmarks) is open-sourced so anyone can reproduce the numbers.
## Research Highlights
- **92.5 on LoCoMo** -- +21 points over the previous algorithm
- **94.4 on LongMemEval** -- +27 points, with 98.2 on assistant memory recall
- **64.1 on BEAM (1M)** -- production-scale memory evaluation at 1M tokens
- [Read the full paper](https://mem0.ai/research)
# Introduction
[Mem0](https://mem0.ai) ("mem-zero") enhances AI assistants and agents with an intelligent memory layer, enabling personalized AI interactions. It remembers user preferences, adapts to individual needs, and continuously learns over time—ideal for customer support chatbots, AI assistants, and autonomous systems.
### Key Features & Use Cases
**Core Capabilities:**
- **Multi-Level Memory**: Seamlessly retains User, Session, and Agent state with adaptive personalization
- **Developer-Friendly**: Intuitive API, cross-platform SDKs, and a fully managed service option
**Applications:**
- **AI Assistants**: Consistent, context-rich conversations
- **Customer Support**: Recall past tickets and user history for tailored help
- **Healthcare**: Track patient preferences and history for personalized care
- **Productivity & Gaming**: Adaptive workflows and environments based on user behavior
## 🚀 Quickstart Guide <a name="quickstart"></a>
### Sign up as an agent
AI agents can mint a working Mem0 API key in under five seconds — no email, no dashboard, no OTP. Four commands end-to-end:
```bash
# 1. Install
npm install -g @mem0/cli # or: pip install mem0-cli
# 2. Sign up as an agent (replace `claude-code` with your name)
mem0 init --agent --agent-caller claude-code
# 3. Add a memory
mem0 add "I am using mem0"
# 4. Search
mem0 search "am I using mem0"
```
The human owner can claim the account later with `mem0 init --email <their-email>` — same key, memories preserved. Full guide: [Sign up as an agent](https://docs.mem0.ai/platform/agent-signup).
| | Library | Self-Hosted Server | Cloud Platform |
|---|---------|-------------------|----------------|
| **Best for** | Testing, prototyping | Teams running on their own infrastructure | Zero-ops production use |
| **Setup** | `pip install mem0ai` | `docker compose up` | Sign up at [app.mem0.ai](https://app.mem0.ai?utm_source=oss&utm_medium=readme) |
| **Dashboard** | -- | [Yes](https://docs.mem0.ai/open-source/setup) | Yes |
| **Auth & API Keys** | -- | Yes | Yes |
| **Advanced Features** | -- | Teasers | All included |
Just testing? Use the library. Building for a team? Self-hosted. Want zero ops? Cloud.
### Library (pip / npm)
```bash
pip install mem0ai
```
For enhanced hybrid search with BM25 keyword matching and entity extraction:
For enhanced hybrid search with BM25 keyword matching and entity extraction, install with NLP support:
```bash
pip install "mem0ai[nlp]"
pip install mem0ai[nlp]
python -m spacy download en_core_web_sm
```
## Platform or open source
| | Platform (`MemoryClient`) | Open source (`Memory`) |
|---|---|---|
| Import | `from mem0 import MemoryClient` | `from mem0 import Memory` |
| Where memories live | Mem0's hosted API | Your configured vector store |
| Required key | `MEM0_API_KEY` | `OPENAI_API_KEY` with the defaults, or keys for your chosen providers |
| Extraction | Managed and asynchronous | Runs synchronously against your configured LLM |
| Best for | Zero-ops production use | Local development and custom infrastructure |
## Platform quickstart
Set `MEM0_API_KEY`, then add a conversation:
```python
import os
from mem0 import MemoryClient
client = MemoryClient(api_key=os.environ["MEM0_API_KEY"])
messages = [
{"role": "user", "content": "I am vegetarian and allergic to nuts."},
{"role": "assistant", "content": "I will remember that."},
]
result = client.add(messages, user_id="alex")
print(result)
```
Hosted `add()` queues extraction and usually returns an `event_id` with `status: "PENDING"`. Do not search immediately after `add()`. Wait for processing to finish in the dashboard, or use a [`memory_add` webhook](https://docs.mem0.ai/platform/features/webhooks), then search:
```python
import os
from mem0 import MemoryClient
client = MemoryClient(api_key=os.environ["MEM0_API_KEY"])
results = client.search(
"What does Alex eat?",
filters={"user_id": "alex"},
top_k=5,
)
print(results["results"])
```
`search()` and `get_all()` take entity IDs inside `filters`. `add()` and `delete_all()` take `user_id`, `agent_id`, or `run_id` as top-level keyword arguments.
## Open-source quickstart
Set `OPENAI_API_KEY` before using the default OpenAI LLM and embedder:
```python
from mem0 import Memory
memory = Memory()
messages = [
{"role": "user", "content": "I am vegetarian and allergic to nuts."},
{"role": "assistant", "content": "I will remember that."},
]
memory.add(messages, user_id="alex")
results = memory.search(
"What does Alex eat?",
filters={"user_id": "alex"},
top_k=5,
)
print(results["results"])
```
The default `Memory` configuration uses OpenAI `gpt-5-mini`, OpenAI `text-embedding-3-small`, local Qdrant storage, and a SQLite history database. Pass a `MemoryConfig` or use `Memory.from_config()` to change the LLM, embedder, vector store, history path, or reranker.
## Configuration and features
| Feature | Documentation |
|---|---|
| Memory operations: `add`, `search`, `get`, `get_all`, `update`, `delete`, `delete_all`, `history` | [Python quickstart](https://docs.mem0.ai/open-source/python-quickstart) |
| Entity scoping with `user_id`, `agent_id`, and `run_id` | [Entity-scoped memory](https://docs.mem0.ai/platform/features/entity-scoped-memory) |
| Metadata and filters | [Metadata filtering](https://docs.mem0.ai/open-source/features/metadata-filtering) |
| Async clients: `AsyncMemory` and `AsyncMemoryClient` | [Async memory](https://docs.mem0.ai/open-source/features/async-memory) |
| LLMs, embedders, vector stores, and rerankers | [Components](https://docs.mem0.ai/components/llms/overview) |
| Graph memory | [Graph memory](https://docs.mem0.ai/platform/features/graph-memory) |
| Custom instructions | [Custom instructions](https://docs.mem0.ai/open-source/features/custom-instructions) |
| Multimodal input | [Multimodal support](https://docs.mem0.ai/open-source/features/multimodal-support) |
| Platform webhooks, export, feedback, expiration, and custom categories | [Platform features](https://docs.mem0.ai/platform/features) |
## Benchmarks
<p align="center">
<a href="https://mem0.ai/research"><strong>Benchmarking Mem0's token-efficient memory algorithm</strong></a>
</p>
| Benchmark | Old | New | Tokens | Latency p50 |
|---|---:|---:|---:|---:|
| **LoCoMo** | 71.4 | **92.5** | 7.0K | 0.88s |
| **LongMemEval** | 67.8 | **94.4** | 6.8K | 1.09s |
| **BEAM (1M)** | n/a | **64.1** | 6.7K | 1.00s |
| **BEAM (10M)** | n/a | **48.6** | 6.9K | 1.05s |
All benchmarks use the same production-representative model stack, single-pass retrieval, and a top-200 retrieval budget. Scores reflect the managed Platform, which includes proprietary optimizations not available in the open-source SDK. Open-source results should show similar directional gains, but may not match these scores.
The current algorithm uses single-pass ADD-only extraction, first-class agent facts, entity linking, multi-signal retrieval, and temporal reasoning. Read the [research paper](https://mem0.ai/research), the [migration guide](https://docs.mem0.ai/migration/oss-v2-to-v3), or the open-source [evaluation framework](https://github.com/mem0ai/memory-benchmarks).
## Self-hosted server
Run Mem0 as a FastAPI service with PostgreSQL, pgvector, and Neo4j:
Install sdk via npm:
```bash
# Recommended: start the stack, create an admin, and issue the first API key.
npm install mem0ai
```
### Self-Hosted Server
> **Note:** Self-hosted auth is on by default. Upgrading from a pre-auth build? Set `ADMIN_API_KEY`, register an admin through the wizard, or `AUTH_DISABLED=true` for local dev only. See [upgrade notes](https://docs.mem0.ai/open-source/setup#upgrade-notes).
```bash
# Recommended: one command — start the stack, create an admin, issue the first API key.
cd server && make bootstrap
# Manual: start the stack, then finish setup in the browser wizard.
cd server && docker compose up -d
# Manual: start the stack and finish setup via the browser wizard.
cd server && docker compose up -d # http://localhost:3000
```
Self-hosted authentication is enabled by default. See the [self-hosted documentation](https://docs.mem0.ai/open-source/overview) and [upgrade notes](https://docs.mem0.ai/open-source/setup#upgrade-notes).
See the [self-hosted docs](https://docs.mem0.ai/open-source/overview) for configuration.
## CLI
### Cloud Platform
Manage hosted memories from your terminal:
1. Sign up on [Mem0 Platform](https://app.mem0.ai?utm_source=oss&utm_medium=readme)
2. Embed the memory layer via SDK or API keys
3. Using hosted Qdrant vectors? See the [Platform migration guide](https://docs.mem0.ai/migration/oss-to-platform) to import them into Mem0 Platform.
### CLI
Manage memories from your terminal:
```bash
pip install mem0-cli
npm install -g @mem0/cli # or: pip install mem0-cli
mem0 init
mem0 add "Prefers dark mode and vim keybindings" --user-id alice
mem0 search "What does Alice prefer?" --user-id alice
```
AI agents can create an account without email or a dashboard:
See the [CLI documentation](https://docs.mem0.ai/platform/cli) for the full command reference.
```bash
mem0 init --agent --agent-caller claude-code
```
### Agent Skills
The human owner can claim the account later with `mem0 init --email <their-email>`. The API key and memories remain unchanged. See the [CLI documentation](https://docs.mem0.ai/platform/cli) and [agent signup guide](https://docs.mem0.ai/platform/agent-signup).
Teach your AI coding assistant (Claude Code, Codex, Cursor, Windsurf, OpenCode, OpenClaw, and any tool that supports the skills standard) how to build with Mem0. Two categories:
## Agent skills
Install reference skills to give compatible coding assistants Mem0 context:
**Reference skills — always on** (SDK knowledge loaded into the assistant's context):
```bash
npx skills add https://github.com/mem0ai/mem0 --skill mem0
npx skills add https://github.com/mem0ai/mem0 --skill mem0-cli
npx skills add https://github.com/mem0ai/mem0 --skill mem0-vercel-ai-sdk
```
Install pipeline skills for end-to-end workflows:
**Pipeline skills — run on demand** (execute an end-to-end workflow in an existing repo):
```bash
npx skills add https://github.com/mem0ai/mem0 --skill mem0-integrate
@@ -212,30 +189,72 @@ npx skills add https://github.com/mem0ai/mem0 --skill mem0-test-integration
npx skills add https://github.com/mem0ai/mem0 --skill mem0-oss-to-platform
```
See the [skills catalog](./skills/) or [Vibecoding with Mem0](https://docs.mem0.ai/vibecoding).
Use `/mem0-integrate` to wire Mem0 into an existing repo via a test-first pipeline, then `/mem0-test-integration` to verify. Use `/mem0-oss-to-platform` to migrate an existing project from Mem0 OSS to the hosted Platform SDK. See the [skills catalog](./skills/) or [Vibecoding with Mem0](https://docs.mem0.ai/vibecoding) for the full picture.
## Integrations and demos
### Basic Usage
- [ChatGPT with Memory demo](https://mem0.dev/demo)
- [Browser extension](https://chromewebstore.google.com/detail/onihkkbipkfeijkadecaafbgagkhglop?utm_source=item-share-cb)
- [LangGraph integration](https://docs.mem0.ai/integrations/langgraph)
- [CrewAI integration](https://docs.mem0.ai/integrations/crewai)
Mem0 requires an LLM to function, with `gpt-5-mini` from OpenAI as the default. However, it supports a variety of LLMs; for details, refer to our [Supported LLMs documentation](https://docs.mem0.ai/components/llms/overview).
## Documentation and help
Mem0 uses `text-embedding-3-small` from OpenAI as the default embedding model. For best results with hybrid search (semantic + keyword + entity boosting), we recommend using at least [Qwen 600M](https://huggingface.co/Alibaba-NLP/gte-Qwen2-1.5B-instruct) or a comparable embedding model. See [Supported Embeddings](https://docs.mem0.ai/components/embedders/overview) for configuration details.
- [Python quickstart](https://docs.mem0.ai/open-source/python-quickstart)
- [Platform quickstart](https://docs.mem0.ai/platform/quickstart)
- [API reference](https://docs.mem0.ai/api-reference)
- [Discord](https://mem0.dev/DiG)
- [GitHub issues](https://github.com/mem0ai/mem0/issues)
- Email: founders@mem0.ai
First step is to instantiate the memory:
## Contributing
```python
from openai import OpenAI
from mem0 import Memory
Read [CONTRIBUTING.md](https://github.com/mem0ai/mem0/blob/main/CONTRIBUTING.md) before opening an issue or pull request.
openai_client = OpenAI()
memory = Memory()
def chat_with_memories(message: str, user_id: str = "default_user") -> str:
# Retrieve relevant memories
relevant_memories = memory.search(query=message, filters={"user_id": user_id}, top_k=3)
memories_str = "\n".join(f"- {entry['memory']}" for entry in relevant_memories["results"])
# Generate Assistant response
system_prompt = f"You are a helpful AI. Answer the question based on query and memories.\nUser Memories:\n{memories_str}"
messages = [{"role": "system", "content": system_prompt}, {"role": "user", "content": message}]
response = openai_client.chat.completions.create(model="gpt-5-mini", messages=messages)
assistant_response = response.choices[0].message.content
# Create new memories from the conversation
messages.append({"role": "assistant", "content": assistant_response})
memory.add(messages, user_id=user_id)
return assistant_response
def main():
print("Chat with AI (type 'exit' to quit)")
while True:
user_input = input("You: ").strip()
if user_input.lower() == 'exit':
print("Goodbye!")
break
print(f"AI: {chat_with_memories(user_input)}")
if __name__ == "__main__":
main()
```
For detailed integration steps, see the [Quickstart](https://docs.mem0.ai/quickstart) and [API Reference](https://docs.mem0.ai/api-reference).
## 🔗 Integrations & Demos
- **ChatGPT with Memory**: Personalized chat powered by Mem0 ([Live Demo](https://mem0.dev/demo))
- **Browser Extension**: Store memories across ChatGPT, Perplexity, and Claude ([Chrome Extension](https://chromewebstore.google.com/detail/onihkkbipkfeijkadecaafbgagkhglop?utm_source=item-share-cb))
- **Langgraph Support**: Build a customer bot with Langgraph + Mem0 ([Guide](https://docs.mem0.ai/integrations/langgraph))
- **CrewAI Integration**: Tailor CrewAI outputs with Mem0 ([Example](https://docs.mem0.ai/integrations/crewai))
## 📚 Documentation & Support
- Full docs: https://docs.mem0.ai
- Community: [Discord](https://mem0.dev/DiG) · [X (formerly Twitter)](https://x.com/mem0ai)
- Contact: founders@mem0.ai
## Citation
We now have a paper you can cite:
```bibtex
@article{mem0,
title={Mem0: Building Production-Ready AI Agents with Scalable Long-Term Memory},
@@ -245,6 +264,6 @@ Read [CONTRIBUTING.md](https://github.com/mem0ai/mem0/blob/main/CONTRIBUTING.md)
}
```
## License
## ⚖️ License
Apache 2.0. See [LICENSE](https://github.com/mem0ai/mem0/blob/main/LICENSE).
Apache 2.0 — see the [LICENSE](https://github.com/mem0ai/mem0/blob/main/LICENSE) file for details.
-14
View File
@@ -2841,13 +2841,6 @@ Existing memories written by the previous versions are not rewritten. If your me
<Tab title="Strands">
<Update label="2026-08-25" description="mem0-strands v0.1.1">
**New Features:**
- **Usage telemetry:** Anonymous usage events (`strands.store.init`, `strands.store.search`, `strands.store.add`, `strands.store.add_messages`) ride the Mem0 SDK's existing PostHog client, unsampled, with no new dependency. Events carry only counts, durations, booleans, and coarse failure kinds: never queries, memory text, message content, entity ids, metadata, or API keys. Opt out with `MEM0_TELEMETRY=false` ([#7110](https://github.com/mem0ai/mem0/pull/7110))
</Update>
<Update label="2026-08-24" description="mem0-strands v0.1.0">
**Initial release** of [`mem0-strands`](https://pypi.org/project/mem0-strands/), a native `MemoryStore` that plugs Mem0 into the [Strands Agents](https://strandsagents.com/) `MemoryManager` ([#7021](https://github.com/mem0ai/mem0/pull/7021))
@@ -2870,13 +2863,6 @@ Existing memories written by the previous versions are not rewritten. If your me
<Tab title="DeepSeek Harness">
<Update label="2026-08-25" description="deepseek-plugin v0.1.1">
**New Features:**
- **Usage telemetry:** Anonymous usage events (`deepseek.plugin.mounted`, `deepseek.tool.search_memory`, `deepseek.tool.add_memory`) are batched to PostHog over native fetch and flushed in the background. Events carry only tool names, durations, counts, and coarse failure kinds: never queries, memory text, filters, or API keys. Opt out with `MEM0_TELEMETRY=false` ([#7110](https://github.com/mem0ai/mem0/pull/7110))
</Update>
<Update label="2026-08-24" description="deepseek-plugin v0.1.0">
**Initial release** of [`deepseek-plugin`](https://www.npmjs.com/package/@mem0/deepseek-plugin), a native DeepSeek Harness (Cordis) plugin that registers Mem0 as two agent-callable tools ([#7027](https://github.com/mem0ai/mem0/pull/7027))
-2
View File
@@ -55,8 +55,6 @@ For a Mem0 Platform on-prem or dedicated deployment, point `config.host` at that
Writes are tagged `source="DEEPSEEK_HARNESS"` so Mem0's backend can attribute usage to this integration. For it to surface by name (rather than bucketing into `OTHERS`), `DEEPSEEK_HARNESS` must be present in the backend's `KNOWN_EVENT_SOURCES` allowlist, a one-line platform change matching the existing `ZAPIER` / `STRANDS` sources.
The plugin also sends anonymous usage events (which tool ran, duration, result counts, coarse failure kind) so Mem0 can tell how the plugin is used and where it breaks. Queries, memory text, and entity ids are never sent. Turn it off with `MEM0_TELEMETRY=false`.
## Status
Developer preview. Tracks the DeepSeek Harness v0.1 plugin API (`@deepseek-ai/cordis`, `@deepseek-ai/dsh-tools`), which is young and moving; pin versions once it stabilizes. Auto-capture (store turns without an explicit tool call) and auto-recall (inject memory into the prompt at assembly) are planned once the harness session/assembly event API is confirmed.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@mem0/deepseek-plugin",
"version": "0.1.1",
"version": "0.1.0",
"description": "Mem0 long-term memory as a native DeepSeek Harness (Cordis) plugin.",
"type": "module",
"license": "Apache-2.0",
+1 -53
View File
@@ -16,7 +16,6 @@ import { MemoryClient } from "mem0ai";
import { formatMemoryList, formatAddResult } from "./formatting.ts";
import { truncateOutput } from "./output.ts";
import { resolveSearchFilters, resolveAddParams } from "./scoping.ts";
import { captureEvent, errorKind } from "./telemetry.ts";
export const name = "mem0";
export const inject = ["tools"];
@@ -82,8 +81,6 @@ export function apply(ctx: Context, config: Config): void {
...(config.host ? { host: config.host } : {}),
});
captureEvent("deepseek.plugin.mounted", { has_host: Boolean(config.host) }, client);
// Recall. The platform rejects top-level entity params on search, so scope
// goes inside `filters` (unlike add below, which takes them top-level).
ctx.tools.register(
@@ -102,36 +99,11 @@ export function apply(ctx: Context, config: Config): void {
output: textOutput,
async execute({ query, limit, userId: u, agentId, runId }) {
const filters = resolveSearchFilters({ userId: u, agentId, runId }, userId);
const topK = limit && limit > 0 ? limit : DEFAULT_SEARCH_LIMIT;
const started = Date.now();
try {
const topK = limit && limit > 0 ? limit : DEFAULT_SEARCH_LIMIT;
const { results } = await client.search(query, { filters, topK });
captureEvent(
"deepseek.tool.search_memory",
{
success: true,
duration_ms: Date.now() - started,
top_k: topK,
result_count: results?.length ?? 0,
query_chars: query.length,
scope_overridden: Boolean(u && u !== userId),
has_agent_id: Boolean(agentId),
has_run_id: Boolean(runId),
},
client,
);
return truncateOutput(formatMemoryList(results ?? []));
} catch (err) {
captureEvent(
"deepseek.tool.search_memory",
{
success: false,
duration_ms: Date.now() - started,
top_k: topK,
error_kind: errorKind(err),
},
client,
);
return `search_memory failed: ${err instanceof Error ? err.message : String(err)}`;
}
},
@@ -151,37 +123,13 @@ export function apply(ctx: Context, config: Config): void {
output: textOutput,
async execute({ text, userId: u, agentId, runId }) {
const addParams = resolveAddParams({ userId: u, agentId, runId }, userId);
const started = Date.now();
try {
const result = await client.add([{ role: "user", content: text }], {
...addParams,
source: SOURCE,
});
captureEvent(
"deepseek.tool.add_memory",
{
success: true,
duration_ms: Date.now() - started,
text_chars: text.length,
memory_count: Array.isArray(result) ? result.length : 0,
scope_overridden: Boolean(u && u !== userId),
has_agent_id: Boolean(agentId),
has_run_id: Boolean(runId),
},
client,
);
return truncateOutput(formatAddResult(result));
} catch (err) {
captureEvent(
"deepseek.tool.add_memory",
{
success: false,
duration_ms: Date.now() - started,
text_chars: text.length,
error_kind: errorKind(err),
},
client,
);
return `add_memory failed: ${err instanceof Error ? err.message : String(err)}`;
}
},
@@ -1,173 +0,0 @@
/**
* Anonymous usage telemetry for the DeepSeek Harness plugin.
*
* Fire-and-forget PostHog events over native fetch, batched and flushed every
* 5 seconds, at 10 queued events, or on process exit. Never throws, never logs.
*
* Events carry only tool names, durations, counts, and coarse failure kinds:
* never queries, memory text, filters, or API keys.
*
* Disable with MEM0_TELEMETRY=false.
*/
import { randomUUID } from "node:crypto";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
const POSTHOG_API_KEY = "phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX";
const POSTHOG_BATCH_URL = "https://us.i.posthog.com/batch/";
const FLUSH_INTERVAL_MS = 5_000;
const FLUSH_THRESHOLD = 10;
const SEND_TIMEOUT_MS = 3_000;
const PLUGIN_VERSION = ((): string => {
try {
return JSON.parse(
fs.readFileSync(new URL("../package.json", import.meta.url), "utf-8"),
).version;
} catch {
return "unknown";
}
})();
/** The Mem0 SDK resolves this to the account email once ping() lands, so events join other Mem0 surfaces. */
export interface TelemetryIdentity {
telemetryId?: string;
}
let queue: Record<string, unknown>[] = [];
let flushTimer: ReturnType<typeof setInterval> | undefined;
let exitHandlerInstalled = false;
let cachedAnonymousId: string | undefined;
let identified = false;
function identityPath(): string {
return path.join(os.homedir(), ".mem0", "deepseek-plugin-telemetry.json");
}
export function isTelemetryEnabled(): boolean {
const value = process.env.MEM0_TELEMETRY?.toLowerCase();
return value !== "false" && value !== "0" && value !== "no" && value !== "off";
}
function anonymousId(): string {
if (cachedAnonymousId) return cachedAnonymousId;
try {
const stored = JSON.parse(fs.readFileSync(identityPath(), "utf-8"));
if (typeof stored.anonymousId === "string" && stored.anonymousId) {
cachedAnonymousId = stored.anonymousId;
return cachedAnonymousId!;
}
} catch {
/* first run, or an unreadable identity file */
}
const created = `deepseek-anon-${randomUUID().replace(/-/g, "")}`;
try {
const target = identityPath();
fs.mkdirSync(path.dirname(target), { recursive: true });
fs.writeFileSync(target, JSON.stringify({ anonymousId: created }), "utf-8");
} catch {
/* an unwritable home directory must not break the plugin */
}
cachedAnonymousId = created;
return created;
}
/** Merge the pre-ping anonymous history into the account once the email resolves. */
function identifyEvent(distinctId: string): Record<string, unknown> | undefined {
if (identified || distinctId.startsWith("deepseek-anon-")) return undefined;
identified = true;
let storedAnonymousId: string | undefined;
try {
storedAnonymousId = JSON.parse(fs.readFileSync(identityPath(), "utf-8")).anonymousId;
fs.unlinkSync(identityPath());
} catch {
return undefined;
}
cachedAnonymousId = undefined;
if (!storedAnonymousId) return undefined;
return {
event: "$identify",
distinct_id: distinctId,
properties: { $anon_distinct_id: storedAnonymousId, $lib: "posthog-node" },
};
}
export function flushEvents(): void {
if (queue.length === 0) return;
const batch = queue;
queue = [];
fetch(POSTHOG_BATCH_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ api_key: POSTHOG_API_KEY, batch }),
signal: AbortSignal.timeout(SEND_TIMEOUT_MS),
}).catch(() => {
/* telemetry never surfaces its own failures */
});
}
function scheduleFlush(): void {
if (!flushTimer) {
flushTimer = setInterval(flushEvents, FLUSH_INTERVAL_MS);
flushTimer.unref?.();
}
if (!exitHandlerInstalled) {
exitHandlerInstalled = true;
process.on("beforeExit", flushEvents);
}
}
export function errorKind(error: unknown): string {
const text = (error instanceof Error ? error.message : String(error)).toLowerCase();
if (text.includes("timeout") || text.includes("aborted")) return "timeout";
if (text.includes("401") || text.includes("403") || text.includes("unauthor")) return "auth";
if (text.includes("429") || text.includes("rate limit")) return "rate-limited";
if (/50[0234]/.test(text)) return "server-error";
if (text.includes("400") || text.includes("422")) return "bad-request";
if (text.includes("fetch failed") || text.includes("enotfound")) return "network";
return error instanceof Error ? error.constructor.name : "other";
}
export function captureEvent(
event: string,
properties: Record<string, unknown>,
client: TelemetryIdentity,
): void {
if (!isTelemetryEnabled()) return;
try {
const distinctId = client.telemetryId || anonymousId();
const identify = identifyEvent(distinctId);
if (identify) queue.push(identify);
queue.push({
event,
distinct_id: distinctId,
properties: {
source: "DEEPSEEK_HARNESS",
language: "node",
plugin_version: PLUGIN_VERSION,
node_version: process.version,
os: process.platform,
$process_person_profile: false,
$lib: "posthog-node",
...properties,
},
});
scheduleFlush();
if (queue.length >= FLUSH_THRESHOLD) flushEvents();
} catch {
/* telemetry never breaks a tool call */
}
}
export function _queueForTesting(): Record<string, unknown>[] {
return queue;
}
export function _resetForTesting(): void {
queue = [];
clearInterval(flushTimer);
flushTimer = undefined;
cachedAnonymousId = undefined;
identified = false;
}
@@ -35,12 +35,9 @@ function applyAndCollect(config: Config): Map<string, RegisteredTool> {
}
let savedKey: string | undefined;
let savedTelemetry: string | undefined;
beforeEach(() => {
savedKey = process.env.MEM0_API_KEY;
savedTelemetry = process.env.MEM0_TELEMETRY;
process.env.MEM0_TELEMETRY = "false";
mockSearch.mockReset();
mockAdd.mockReset();
});
@@ -48,8 +45,6 @@ beforeEach(() => {
afterEach(() => {
if (savedKey === undefined) delete process.env.MEM0_API_KEY;
else process.env.MEM0_API_KEY = savedKey;
if (savedTelemetry === undefined) delete process.env.MEM0_TELEMETRY;
else process.env.MEM0_TELEMETRY = savedTelemetry;
});
describe("apply() config validation", () => {
@@ -1,218 +0,0 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
const mockSearch = vi.fn();
const mockAdd = vi.fn();
vi.mock("mem0ai", () => ({
MemoryClient: class {
telemetryId = "dev@example.com";
search = mockSearch;
add = mockAdd;
},
}));
vi.mock("@deepseek-ai/dsh-tools", () => ({ defineTool: (options: unknown) => options }));
import { apply, type Config } from "../src/index.ts";
import {
captureEvent,
errorKind,
flushEvents,
isTelemetryEnabled,
_queueForTesting,
_resetForTesting,
} from "../src/telemetry.ts";
interface RegisteredTool {
name: string;
execute(args: unknown, exec: unknown): Promise<unknown>;
}
function applyAndCollect(config: Config): Map<string, RegisteredTool> {
const tools = new Map<string, RegisteredTool>();
apply({ tools: { register: (t: RegisteredTool) => tools.set(t.name, t) } } as never, config);
return tools;
}
function queued(): Record<string, any>[] {
return _queueForTesting() as Record<string, any>[];
}
let home: string;
let savedHome: string | undefined;
let savedTelemetry: string | undefined;
beforeEach(() => {
savedHome = process.env.HOME;
savedTelemetry = process.env.MEM0_TELEMETRY;
delete process.env.MEM0_TELEMETRY;
home = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-deepseek-"));
process.env.HOME = home;
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: true }));
mockSearch.mockReset();
mockAdd.mockReset();
_resetForTesting();
});
afterEach(() => {
_resetForTesting();
vi.unstubAllGlobals();
fs.rmSync(home, { recursive: true, force: true });
if (savedHome === undefined) delete process.env.HOME;
else process.env.HOME = savedHome;
if (savedTelemetry === undefined) delete process.env.MEM0_TELEMETRY;
else process.env.MEM0_TELEMETRY = savedTelemetry;
});
describe("opt-out", () => {
it("queues nothing for every documented off value", () => {
for (const value of ["false", "0", "no", "OFF"]) {
process.env.MEM0_TELEMETRY = value;
expect(isTelemetryEnabled()).toBe(false);
captureEvent("deepseek.tool.search_memory", {}, { telemetryId: "dev@example.com" });
}
expect(queued()).toHaveLength(0);
});
it("is on by default", () => {
expect(isTelemetryEnabled()).toBe(true);
});
});
describe("identity", () => {
it("keys events on the account email the SDK resolved", () => {
captureEvent("deepseek.tool.add_memory", {}, { telemetryId: "dev@example.com" });
expect(queued()[0].distinct_id).toBe("dev@example.com");
});
it("falls back to a persisted anonymous id before the SDK has pinged", () => {
captureEvent("deepseek.tool.add_memory", {}, {});
const first = queued()[0].distinct_id as string;
expect(first).toMatch(/^deepseek-anon-/);
_resetForTesting();
captureEvent("deepseek.tool.add_memory", {}, {});
expect(queued()[0].distinct_id).toBe(first);
});
it("aliases the anonymous history onto the email exactly once", () => {
captureEvent("deepseek.tool.add_memory", {}, {});
const anonymous = queued()[0].distinct_id;
_resetForTesting();
captureEvent("deepseek.tool.add_memory", {}, { telemetryId: "dev@example.com" });
const [identify, event] = queued();
expect(identify.event).toBe("$identify");
expect(identify.distinct_id).toBe("dev@example.com");
expect(identify.properties.$anon_distinct_id).toBe(anonymous);
expect(event.distinct_id).toBe("dev@example.com");
captureEvent("deepseek.tool.add_memory", {}, { telemetryId: "dev@example.com" });
expect(queued().filter((e) => e.event === "$identify")).toHaveLength(1);
});
});
describe("event shape", () => {
it("stamps every event with the integration source and runtime baseline", () => {
captureEvent("deepseek.tool.search_memory", { success: true }, { telemetryId: "d@e.com" });
const { properties } = queued()[0];
expect(properties.source).toBe("DEEPSEEK_HARNESS");
expect(properties.language).toBe("node");
expect(properties.$process_person_profile).toBe(false);
expect(properties.os).toBe(process.platform);
expect(properties.plugin_version).not.toBe("unknown");
expect(properties.success).toBe(true);
});
it("flushes one batch to PostHog and empties the queue", async () => {
captureEvent("deepseek.tool.search_memory", {}, { telemetryId: "d@e.com" });
flushEvents();
const [url, init] = (fetch as unknown as ReturnType<typeof vi.fn>).mock.calls[0];
expect(url).toContain("posthog.com");
expect(JSON.parse(init.body).batch).toHaveLength(1);
expect(queued()).toHaveLength(0);
});
it("never throws when the network is gone", () => {
vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("fetch failed")));
captureEvent("deepseek.tool.search_memory", {}, { telemetryId: "d@e.com" });
expect(() => flushEvents()).not.toThrow();
});
});
describe("errorKind", () => {
it("buckets failures without leaking the message", () => {
expect(errorKind(new Error("Request failed with status 429"))).toBe("rate-limited");
expect(errorKind(new Error("401 Unauthorized"))).toBe("auth");
expect(errorKind(new Error("503 Service Unavailable"))).toBe("server-error");
expect(errorKind(new Error("The operation was aborted due to timeout"))).toBe("timeout");
expect(errorKind(new Error("fetch failed"))).toBe("network");
expect(errorKind(new Error("token sk-abcdef is invalid"))).toBe("Error");
});
});
describe("tool instrumentation", () => {
it("records a mount, then a successful search with its shape but not its query", async () => {
mockSearch.mockResolvedValue({ results: [{ id: "m1", memory: "Likes tea" }] });
const tools = applyAndCollect({ apiKey: "k", userId: "u" });
await tools.get("search_memory")!.execute({ query: "what drink", limit: 3 }, {});
const [mounted, search] = queued();
expect(mounted.event).toBe("deepseek.plugin.mounted");
expect(mounted.properties.has_host).toBe(false);
expect(search.event).toBe("deepseek.tool.search_memory");
expect(search.properties).toMatchObject({
success: true,
top_k: 3,
result_count: 1,
query_chars: 10,
scope_overridden: false,
has_agent_id: false,
has_run_id: false,
});
expect(typeof search.properties.duration_ms).toBe("number");
expect(JSON.stringify(search)).not.toContain("what drink");
expect(JSON.stringify(search)).not.toContain("Likes tea");
});
it("records a failed search with a coarse error kind", async () => {
mockSearch.mockRejectedValue(new Error("429 rate limit exceeded"));
const tools = applyAndCollect({ apiKey: "k", userId: "u" });
await tools.get("search_memory")!.execute({ query: "x" }, {});
const search = queued().at(-1)!;
expect(search.properties).toMatchObject({ success: false, error_kind: "rate-limited" });
});
it("records a write with its size but not its text", async () => {
mockAdd.mockResolvedValue([{ id: "m1", memory: "Fact" }]);
const tools = applyAndCollect({ apiKey: "k", userId: "u" });
await tools.get("add_memory")!.execute({ text: "secret fact", userId: "alice" }, {});
const add = queued().at(-1)!;
expect(add.event).toBe("deepseek.tool.add_memory");
expect(add.properties).toMatchObject({
success: true,
text_chars: 11,
memory_count: 1,
scope_overridden: true,
});
expect(JSON.stringify(add)).not.toContain("secret fact");
expect(JSON.stringify(add)).not.toContain("alice");
});
it("records a failed write and still returns the graceful failure line", async () => {
mockAdd.mockRejectedValue(new Error("boom"));
const tools = applyAndCollect({ apiKey: "k", userId: "u" });
const out = await tools.get("add_memory")!.execute({ text: "x" }, {});
expect(out).toContain("add_memory failed");
expect(queued().at(-1)!.properties).toMatchObject({ success: false, error_kind: "Error" });
});
});
-7
View File
@@ -77,13 +77,6 @@ For the model-called tool (`store` / `retrieve` / `get` / `delete`), use the
[`mem0_memory`](https://github.com/strands-agents/tools) tool from `strands-agents-tools`. The store and
the tool share one Mem0 backend and namespace.
## Telemetry
The store sends anonymous usage events (store configuration, operation, duration,
result counts, coarse failure kind) over the Mem0 SDK's existing telemetry client,
tagged `source="STRANDS"`. Queries, memory text, message content, entity ids, and
metadata are never sent. Turn it off with `MEM0_TELEMETRY=false`.
## Development
The package lives under [`python/`](python/) (monorepo-style layout matching the
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "mem0-strands"
version = "0.1.1"
version = "0.1.0"
description = "Persistent long-term memory for Strands agents, backed by Mem0."
readme = "README.md"
requires-python = ">=3.10"
@@ -36,13 +36,11 @@ environment variable, or pass a Mem0 OSS ``config`` dict for a self-hosted backe
from __future__ import annotations
import asyncio
import time
from typing import Any
from strands.memory import AddMessagesContext, MemoryEntry, MemoryStore, SearchOptions
from strands.types.content import Message
from mem0_strands import telemetry
from mem0_strands.client import Mem0ServiceClient
DEFAULT_MAX_SEARCH_RESULTS = 5
@@ -136,7 +134,6 @@ class Mem0MemoryStore(MemoryStore):
self._host = host
self._config = config
self._client = client
self._recorded_init = False
@property
def client(self) -> Mem0ServiceClient:
@@ -147,22 +144,8 @@ class Mem0MemoryStore(MemoryStore):
stores), so first use is deferred and always happens inside a worker thread
via :func:`asyncio.to_thread`, never on the event loop.
"""
client_injected = self._client is not None
if self._client is None:
self._client = Mem0ServiceClient(api_key=self._api_key, host=self._host, config=self._config)
if not self._recorded_init:
self._recorded_init = True
telemetry.record(
"store.init",
self._client,
scopes=sorted(self.scope),
writable=self.writable,
extraction_enabled=bool(self.extraction),
has_default_metadata=bool(self.metadata),
max_search_results=self.max_search_results,
host_overridden=bool(self._host),
client_injected=client_injected,
)
return self._client
async def search(self, query: str, options: SearchOptions | None = None) -> list[MemoryEntry]:
@@ -175,13 +158,7 @@ class Mem0MemoryStore(MemoryStore):
# ``self.client`` is resolved inside the thread so lazy construction (a
# blocking call) does not run on the event loop.
started = time.perf_counter()
try:
memories = await asyncio.to_thread(lambda: self.client.search_memories(query, self.scope, top_k))
except Exception as exc:
self._record("store.search", started, False, top_k=top_k, error_kind=telemetry.error_kind(exc))
raise
self._record("store.search", started, True, top_k=top_k, result_count=len(memories))
memories = await asyncio.to_thread(lambda: self.client.search_memories(query, self.scope, top_k))
return [self._to_entry(memory) for memory in memories]
async def add(self, content: str, metadata: dict[str, Any] | None = None) -> Any:
@@ -191,14 +168,7 @@ class Mem0MemoryStore(MemoryStore):
Mem0 de-duplicates on the server.
"""
merged = self._merge_metadata(metadata)
started = time.perf_counter()
try:
result = await asyncio.to_thread(lambda: self.client.store_memory(content, self.scope, merged))
except Exception as exc:
self._record("store.add", started, False, error_kind=telemetry.error_kind(exc))
raise
self._record("store.add", started, True, content_chars=len(content), has_metadata=bool(merged))
return result
return await asyncio.to_thread(lambda: self.client.store_memory(content, self.scope, merged))
async def add_messages(self, messages: list[Message], context: AddMessagesContext | None = None) -> Any:
"""Ingest raw conversation turns for Mem0 server-side extraction (``infer=True``).
@@ -216,33 +186,7 @@ class Mem0MemoryStore(MemoryStore):
payload.append({"role": message["role"], "content": text})
if not payload:
return None
started = time.perf_counter()
try:
result = await asyncio.to_thread(lambda: self.client.store_messages(payload, self.scope))
except Exception as exc:
self._record("store.add_messages", started, False, error_kind=telemetry.error_kind(exc))
raise
self._record(
"store.add_messages",
started,
True,
message_count=len(messages),
rendered_count=len(payload),
total_chars=sum(len(turn["content"]) for turn in payload),
)
return result
def _record(self, event: str, started: float, success: bool, **properties: Any) -> None:
"""Send one store telemetry event, timed from ``started``."""
if self._client is None:
return
telemetry.record(
event,
self._client,
success=success,
duration_ms=round((time.perf_counter() - started) * 1000, 2),
**properties,
)
return await asyncio.to_thread(lambda: self.client.store_messages(payload, self.scope))
@staticmethod
def _render_content(content: Any) -> str:
@@ -1,70 +0,0 @@
"""Anonymous usage telemetry for the Strands memory store.
Events ride the Mem0 SDK's own PostHog client, so they need no extra dependency
and join the account's other Mem0 usage: on the hosted platform the distinct id
is the account email, and on a self-hosted OSS backend it is the machine-local
anonymous id the SDK already keeps. Unlike the SDK's OSS hot-path events these
are never sampled, because a store call is an agent-level action, not a loop.
Only names, counts, durations, and coarse failure kinds are sent: never queries,
memory text, message content, entity ids, metadata, or API keys.
Opt out with MEM0_TELEMETRY=false.
"""
from __future__ import annotations
from importlib.metadata import PackageNotFoundError, version
from typing import TYPE_CHECKING, Any
from mem0.memory.telemetry import client_telemetry
if TYPE_CHECKING:
from mem0_strands.client import Mem0ServiceClient
SOURCE = "STRANDS"
try:
_VERSION = version("mem0-strands")
except PackageNotFoundError: # pragma: no cover - only when running from a source tree
_VERSION = "0.0.0+unknown"
def error_kind(exc: BaseException) -> str:
"""Coarse, content-free label for a failure, safe to send."""
text = f"{type(exc).__name__}: {exc}".lower()
if "timeout" in text or "timed out" in text:
return "timeout"
if "401" in text or "403" in text or "unauthor" in text or "forbidden" in text:
return "auth"
if "429" in text or "rate limit" in text:
return "rate-limited"
if any(code in text for code in ("500", "502", "503", "504")):
return "server-error"
if "400" in text or "422" in text:
return "bad-request"
return type(exc).__name__
def _distinct_id(client: Mem0ServiceClient) -> str | None:
"""The account email to attribute events to, or None to fall back to the SDK's anonymous id."""
email = getattr(client.mem0, "user_email", None)
return email if isinstance(email, str) and email else None
def record(event: str, client: Mem0ServiceClient, **properties: Any) -> None:
"""Send one strands.* usage event. Never raises."""
try:
client_telemetry.capture_event(
f"strands.{event}",
{
"source": SOURCE,
"language": "python",
"strands_store_version": _VERSION,
"backend": "platform" if client.is_platform else "oss",
**properties,
},
_distinct_id(client),
)
except Exception:
pass
@@ -1,10 +0,0 @@
"""Keep the test suite from sending usage telemetry to the live PostHog project.
Set before ``mem0`` is imported: the SDK reads ``MEM0_TELEMETRY`` once, at import.
"""
from __future__ import annotations
import os
os.environ["MEM0_TELEMETRY"] = "false"
@@ -1,172 +0,0 @@
"""Tests for the store's anonymous usage telemetry."""
from unittest.mock import MagicMock, patch
import pytest
from mem0_strands import Mem0MemoryStore, telemetry
@pytest.fixture
def mock_client():
"""A mocked Mem0ServiceClient that looks like the hosted platform."""
client = MagicMock()
client.is_platform = True
client.mem0.user_email = "dev@example.com"
return client
@pytest.fixture
def captured():
"""Intercept the SDK PostHog client and collect (event, properties, distinct_id)."""
with patch.object(telemetry, "client_telemetry") as posthog:
events = []
posthog.capture_event.side_effect = lambda event, properties, distinct_id=None: events.append(
(event, properties, distinct_id)
)
yield events
def make_store(mock_client, **kwargs):
kwargs.setdefault("user_id", "alex")
return Mem0MemoryStore(client=mock_client, **kwargs)
# ---------------------------------------------------------------------------
# Event shape
# ---------------------------------------------------------------------------
def test_every_event_carries_source_and_backend(mock_client, captured):
assert make_store(mock_client).client is mock_client
event, properties, distinct_id = captured[0]
assert event == "strands.store.init"
assert properties["source"] == "STRANDS"
assert properties["language"] == "python"
assert properties["backend"] == "platform"
assert properties["strands_store_version"]
assert distinct_id == "dev@example.com"
def test_oss_backend_is_labelled_and_falls_back_to_the_sdk_anonymous_id(captured):
client = MagicMock()
client.is_platform = False
del client.mem0.user_email
assert make_store(client).client is client
_, properties, distinct_id = captured[0]
assert properties["backend"] == "oss"
assert distinct_id is None
def test_init_is_recorded_once_per_store(mock_client, captured):
store = make_store(mock_client)
assert store.client is store.client
assert [event for event, _, _ in captured] == ["strands.store.init"]
def test_init_describes_configuration_without_scope_values(mock_client, captured):
store = make_store(mock_client, agent_id="assistant", writable=False, extraction=True, metadata={"team": "core"})
assert store.client is mock_client
_, properties, _ = captured[0]
assert properties["scopes"] == ["agent_id", "user_id"]
assert properties["writable"] is False
assert properties["extraction_enabled"] is True
assert properties["has_default_metadata"] is True
assert "alex" not in str(properties)
assert "assistant" not in str(properties)
assert "core" not in str(properties)
# ---------------------------------------------------------------------------
# Per-operation events
# ---------------------------------------------------------------------------
async def test_search_records_counts_but_never_the_query(mock_client, captured):
mock_client.search_memories.return_value = [{"id": "m1", "memory": "Likes tea"}]
await make_store(mock_client).search("what does alex drink")
event, properties, _ = captured[-1]
assert event == "strands.store.search"
assert properties["success"] is True
assert properties["result_count"] == 1
assert properties["top_k"] == 5
assert properties["duration_ms"] >= 0
assert "drink" not in str(properties)
assert "tea" not in str(properties)
async def test_add_records_size_but_never_the_content(mock_client, captured):
await make_store(mock_client).add("Alex prefers dark roast", metadata={"team": "core"})
event, properties, _ = captured[-1]
assert event == "strands.store.add"
assert properties["success"] is True
assert properties["content_chars"] == len("Alex prefers dark roast")
assert properties["has_metadata"] is True
assert "roast" not in str(properties)
async def test_add_messages_records_turn_counts_but_never_the_turns(mock_client, captured):
messages = [
{"role": "user", "content": [{"text": "hello"}]},
{"role": "assistant", "content": [{"text": "hi there"}]},
]
await make_store(mock_client).add_messages(messages)
event, properties, _ = captured[-1]
assert event == "strands.store.add_messages"
assert properties["message_count"] == 2
assert properties["rendered_count"] == 2
assert properties["total_chars"] == len("hello") + len("hi there")
assert "hello" not in str(properties)
async def test_empty_add_messages_records_nothing(mock_client, captured):
assert await make_store(mock_client).add_messages([]) is None
assert [event for event, _, _ in captured] == []
# ---------------------------------------------------------------------------
# Failures
# ---------------------------------------------------------------------------
async def test_a_failed_search_records_a_coarse_error_kind_and_still_raises(mock_client, captured):
mock_client.search_memories.side_effect = RuntimeError("HTTP 429 rate limit exceeded for key sk-secret")
with pytest.raises(RuntimeError):
await make_store(mock_client).search("anything")
event, properties, _ = captured[-1]
assert event == "strands.store.search"
assert properties["success"] is False
assert properties["error_kind"] == "rate-limited"
assert "sk-secret" not in str(properties)
@pytest.mark.parametrize(
("message", "expected"),
[
("request timed out", "timeout"),
("401 Unauthorized", "auth"),
("429 Too Many Requests", "rate-limited"),
("503 Service Unavailable", "server-error"),
("422 Unprocessable Entity", "bad-request"),
("something else entirely", "ValueError"),
],
)
def test_error_kind_buckets_failures(message, expected):
assert telemetry.error_kind(ValueError(message)) == expected
def test_a_broken_telemetry_backend_never_breaks_the_store(mock_client):
with patch.object(telemetry, "client_telemetry") as posthog:
posthog.capture_event.side_effect = RuntimeError("posthog is down")
assert make_store(mock_client).client is mock_client
+36 -207
View File
@@ -1,235 +1,64 @@
# Mem0 TypeScript SDK
# Mem0 - The Memory Layer for Your AI Apps
<p align="center">
<a href="https://github.com/mem0ai/mem0">
<img src="../docs/images/banner-sm.png" width="800px" alt="Mem0, the memory layer for personalized AI">
</a>
</p>
<p align="center" style="display: flex; justify-content: center; gap: 20px; align-items: center;">
<a href="https://trendshift.io/repositories/11194" target="blank">
<img src="https://trendshift.io/api/badge/repositories/11194" alt="mem0ai%2Fmem0 | Trendshift" width="250" height="55"/>
</a>
</p>
Mem0 is a self-improving memory layer for LLM applications, enabling personalized AI experiences that save costs and delight users. We offer both cloud and open-source solutions to cater to different needs.
<p align="center">
<a href="https://mem0.ai">Learn more</a>
·
<a href="https://mem0.dev/DiG">Join Discord</a>
·
<a href="https://mem0.dev/demo">Demo</a>
</p>
See the complete [OSS Docs](https://docs.mem0.ai/open-source/node-quickstart).
See the complete [Platform API Reference](https://docs.mem0.ai/api-reference).
<p align="center">
<a href="https://mem0.dev/DiG">
<img src="https://img.shields.io/badge/Discord-%235865F2.svg?&logo=discord&logoColor=white" alt="Mem0 Discord">
</a>
<a href="https://www.npmjs.com/package/mem0ai">
<img src="https://img.shields.io/npm/dm/mem0ai" alt="Mem0 npm downloads">
</a>
<a href="https://github.com/mem0ai/mem0">
<img src="https://img.shields.io/github/commit-activity/m/mem0ai/mem0?style=flat-square" alt="GitHub commit activity">
</a>
<a href="https://www.npmjs.com/package/mem0ai" target="blank">
<img src="https://img.shields.io/npm/v/mem0ai?color=%2334D058&label=npm%20package" alt="npm package version">
</a>
<a href="https://www.ycombinator.com/companies/mem0">
<img src="https://img.shields.io/badge/Y%20Combinator-S24-orange?style=flat-square" alt="Y Combinator S24">
</a>
</p>
## 1. Installation
Mem0 gives AI assistants and agents persistent memory. It extracts useful facts from conversations, scopes them to a user, agent, or run, and retrieves the relevant facts for later interactions. The TypeScript package, `mem0ai` on npm, includes `MemoryClient` for the hosted Mem0 Platform, imported from `mem0ai`, and `Memory` for open-source, in-process memory, imported from `mem0ai/oss`.
## Requirements
- Node.js 20 or later
- Hosted Platform: `MEM0_API_KEY` from the [Mem0 dashboard](https://app.mem0.ai/dashboard/api-keys)
- Open source with the default providers: `OPENAI_API_KEY`
## Install
For the open-source version, you can install the Mem0 package using npm:
```bash
npm install mem0ai
npm i mem0ai
```
## Platform or open source
## 2. API Key Setup
| | Platform (`MemoryClient`) | Open source (`Memory`) |
| ------------------- | --------------------------------------- | --------------------------------------------------------------------- |
| Import | `import { MemoryClient } from "mem0ai"` | `import { Memory } from "mem0ai/oss"` |
| Where memories live | Mem0's hosted API | Your configured vector store |
| Required key | `MEM0_API_KEY` | `OPENAI_API_KEY` with the defaults, or keys for your chosen providers |
| Extraction | Managed and asynchronous | Runs against your configured LLM |
| Best for | Zero-ops production use | Local development and custom infrastructure |
For the cloud offering, sign in to [Mem0 Platform](https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=mem0-ts-readme) to obtain your API Key.
## Platform quickstart
## 3. Client Features
Set `MEM0_API_KEY`, then add a conversation:
### Cloud Offering
```ts
import { MemoryClient, type Message } from "mem0ai";
The cloud version provides a comprehensive set of features, including:
const client = new MemoryClient({ apiKey: process.env.MEM0_API_KEY! });
- **Memory Operations**: Perform CRUD operations on memories.
- **Search Capabilities**: Search for relevant memories using advanced filters.
- **Memory History**: Track changes to memories over time.
- **Error Handling**: Robust error handling for API-related issues.
- **Async/Await Support**: All methods return promises for easy integration.
const messages: Message[] = [
{ role: "user", content: "I am vegetarian and allergic to nuts." },
{ role: "assistant", content: "I will remember that." },
];
await client.add(messages, { userId: "alex" });
```
### Open-Source Offering
Hosted `add()` queues extraction and usually returns an `eventId` with `status: "PENDING"`. Do not search immediately after `add()`. Wait for processing to finish in the dashboard, or use a [`memory_add` webhook](https://docs.mem0.ai/platform/features/webhooks), then search:
The open-source version includes the following top features:
```ts
import { MemoryClient } from "mem0ai";
- **Memory Management**: Add, update, delete, and retrieve memories.
- **Vector Store Integration**: Supports various vector store providers for efficient memory retrieval.
- **LLM Support**: Integrates with multiple LLM providers for generating responses.
- **Customizable Configuration**: Easily configure memory settings and providers.
- **SQLite Storage**: Use SQLite for memory history management.
const client = new MemoryClient({ apiKey: process.env.MEM0_API_KEY! });
const results = await client.search("What does Alex eat?", {
filters: { user_id: "alex" },
topK: 5,
});
console.log(results.results);
```
## 4. Memory Operations
`search()` and `getAll()` take entity IDs inside `filters` with snake_case keys. `add()` and `deleteAll()` take `userId`, `agentId`, or `runId` as top-level camelCase options.
Mem0 provides a simple and customizable interface for performing memory operations. You can create long-term and short-term memories, search for relevant memories, and manage memory history.
## Open-source quickstart
## 5. Error Handling
Set `OPENAI_API_KEY` before using the default OpenAI LLM and embedder:
The MemoryClient throws errors for any API-related issues. You can catch and handle these errors effectively.
```ts
import { Memory } from "mem0ai/oss";
## 6. Using with async/await
const memory = new Memory();
All methods of the MemoryClient return promises, allowing for seamless integration with async/await syntax.
const messages = [
{ role: "user", content: "I am vegetarian and allergic to nuts." },
{ role: "assistant", content: "I will remember that." },
];
await memory.add(messages, { userId: "alex" });
## 7. Testing the Client
const results = await memory.search("What does Alex eat?", {
filters: { user_id: "alex" },
topK: 5,
});
console.log(results.results);
```
To test the MemoryClient in a Node.js environment, you can create a simple script to verify the functionality of memory operations.
The default `Memory` configuration uses OpenAI `gpt-5-mini`, OpenAI `text-embedding-3-small`, a SQLite-backed vector store at `~/.mem0/vector_store.db`, and a SQLite history database at `memory.db`. Pass a config object to `Memory` to change the LLM, embedder, vector store, history path, or reranker.
## Getting Help
## Configuration and features
If you have any questions or need assistance, please reach out to us:
| Feature | Documentation |
| ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| Memory operations: `add`, `search`, `get`, `getAll`, `update`, `delete`, `deleteAll`, `history`, all async and Promise-based | [Node quickstart](https://docs.mem0.ai/open-source/node-quickstart) |
| Entity scoping with `userId`, `agentId`, and `runId` | [Entity-scoped memory](https://docs.mem0.ai/platform/features/entity-scoped-memory) |
| Metadata and filters | [Metadata filtering](https://docs.mem0.ai/open-source/features/metadata-filtering) |
| LLMs, embedders, vector stores, and rerankers | [Components](https://docs.mem0.ai/components/llms/overview) |
| Graph memory | [Graph memory](https://docs.mem0.ai/platform/features/graph-memory) |
| Custom instructions | [Custom instructions](https://docs.mem0.ai/open-source/features/custom-instructions) |
| Multimodal input | [Multimodal support](https://docs.mem0.ai/open-source/features/multimodal-support) |
| Platform webhooks, export, feedback, expiration, and custom categories | [Platform features](https://docs.mem0.ai/platform/features) |
## Benchmarks
<p align="center">
<a href="https://mem0.ai/research"><strong>Benchmarking Mem0's token-efficient memory algorithm</strong></a>
</p>
| Benchmark | Old | New | Tokens | Latency p50 |
| --------------- | ---: | -------: | -----: | ----------: |
| **LoCoMo** | 71.4 | **92.5** | 7.0K | 0.88s |
| **LongMemEval** | 67.8 | **94.4** | 6.8K | 1.09s |
| **BEAM (1M)** | n/a | **64.1** | 6.7K | 1.00s |
| **BEAM (10M)** | n/a | **48.6** | 6.9K | 1.05s |
All benchmarks use the same production-representative model stack, single-pass retrieval, and a top-200 retrieval budget. Scores reflect the managed Platform, which includes proprietary optimizations not available in the open-source SDK. Open-source results should show similar directional gains, but may not match these scores.
The current algorithm uses single-pass ADD-only extraction, first-class agent facts, entity linking, multi-signal retrieval, and temporal reasoning. Read the [research paper](https://mem0.ai/research), the [migration guide](https://docs.mem0.ai/migration/oss-v2-to-v3), or the open-source [evaluation framework](https://github.com/mem0ai/memory-benchmarks).
## Self-hosted server
Run Mem0 as a FastAPI service with PostgreSQL, pgvector, and Neo4j:
```bash
# Recommended: start the stack, create an admin, and issue the first API key.
cd server && make bootstrap
# Manual: start the stack, then finish setup in the browser wizard.
cd server && docker compose up -d
```
Self-hosted authentication is enabled by default. See the [self-hosted documentation](https://docs.mem0.ai/open-source/overview) and [upgrade notes](https://docs.mem0.ai/open-source/setup#upgrade-notes).
## CLI
Manage hosted memories from your terminal:
```bash
npm install -g @mem0/cli
mem0 init
mem0 add "Prefers dark mode and vim keybindings" --user-id alice
mem0 search "What does Alice prefer?" --user-id alice
```
AI agents can create an account without email or a dashboard:
```bash
mem0 init --agent --agent-caller claude-code
```
The human owner can claim the account later with `mem0 init --email <their-email>`. The API key and memories remain unchanged. See the [CLI documentation](https://docs.mem0.ai/platform/cli) and [agent signup guide](https://docs.mem0.ai/platform/agent-signup).
## Agent skills
Install reference skills to give compatible coding assistants Mem0 context:
```bash
npx skills add https://github.com/mem0ai/mem0 --skill mem0
npx skills add https://github.com/mem0ai/mem0 --skill mem0-cli
```
Install pipeline skills for end-to-end workflows:
```bash
npx skills add https://github.com/mem0ai/mem0 --skill mem0-integrate
npx skills add https://github.com/mem0ai/mem0 --skill mem0-test-integration
npx skills add https://github.com/mem0ai/mem0 --skill mem0-oss-to-platform
```
See the [skills catalog](../skills/) or [Vibecoding with Mem0](https://docs.mem0.ai/vibecoding).
## Integrations and demos
- [ChatGPT with Memory demo](https://mem0.dev/demo)
- [Browser extension](https://chromewebstore.google.com/detail/onihkkbipkfeijkadecaafbgagkhglop?utm_source=item-share-cb)
- [LangGraph integration](https://docs.mem0.ai/integrations/langgraph)
- [CrewAI integration](https://docs.mem0.ai/integrations/crewai)
## Documentation and help
- [Node quickstart](https://docs.mem0.ai/open-source/node-quickstart)
- [Platform quickstart](https://docs.mem0.ai/platform/quickstart)
- [API reference](https://docs.mem0.ai/api-reference)
- [Discord](https://mem0.dev/DiG)
- [GitHub issues](https://github.com/mem0ai/mem0/issues)
- Email: founders@mem0.ai
## Contributing
Read [CONTRIBUTING.md](https://github.com/mem0ai/mem0/blob/main/CONTRIBUTING.md) before opening an issue or pull request.
## Citation
```bibtex
@article{mem0,
title={Mem0: Building Production-Ready AI Agents with Scalable Long-Term Memory},
author={Chhikara, Prateek and Khant, Dev and Aryan, Saket and Singh, Taranjeet and Yadav, Deshraj},
journal={arXiv preprint arXiv:2504.19413},
year={2025}
}
```
## License
Apache 2.0. See [LICENSE](https://github.com/mem0ai/mem0/blob/main/LICENSE).
- [Join our discord community](https://mem0.ai/discord)
- GitHub Issues: [Report bugs or request features](https://github.com/mem0ai/mem0/issues)