Compare commits

..
17 changed files with 251 additions and 45 deletions
+1 -1
View File
@@ -39,7 +39,7 @@
</p>
<p align="center">
<a href="https://mem0.ai/research"><strong>📄 Building Production-Ready AI Agents with Scalable Long-Term Memory →</strong></a>
<a href="https://mem0.ai/research"><strong>📄 Benchmarking Mem0's token-efficient memory algorithm →</strong></a>
</p>
## New Memory Algorithm (April 2026)
+23 -20
View File
@@ -1,18 +1,18 @@
---
title: 'Add Memories'
description: "Add facts, messages, or metadata to a user memory store with support for async processing and event tracking."
openapi: post /v1/memories/
title: Add Memories
description: "Add facts, messages, or metadata to a user memory store with async processing and event tracking via the V3 additive pipeline."
openapi: post /v3/memories/add/
---
Add new facts, messages, or metadata to a user’s memory store. The Add Memories endpoint accepts either raw text or conversational turns and commits them asynchronously so the memory is ready for later search, retrieval, and graph queries.
Extract and store memories from a conversation using the V3 additive pipeline. The endpoint uses single-pass ADD-only extraction — one LLM call, no UPDATE/DELETE. Memories accumulate over time; nothing is overwritten.
## Endpoint
- **Method**: `POST`
- **URL**: `/v1/memories/`
- **URL**: `/v3/memories/add/`
- **Content-Type**: `application/json`
Memories are processed asynchronously by default. The response contains queued events you can track while the platform finalizes enrichment.
Processing is asynchronous. The response returns an `event_id` you can poll via `GET /v1/event/{event_id}/`.
## Required headers
@@ -23,7 +23,7 @@ Memories are processed asynchronously by default. The response contains queued e
## Request body
Provide at least one message or direct memory string. Most callers supply `messages` so Mem0 can infer structured memories as part of ingestion.
Provide conversation messages for Mem0 to extract memories from. At least one entity ID (`user_id`, `agent_id`, `app_id`, or `run_id`) is required so the memory is scoped to a session. Entity IDs are accepted at the top level.
<CodeGroup>
```json Basic request
@@ -43,12 +43,15 @@ Provide at least one message or direct memory string. Most callers supply `messa
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `user_id` | string | No* | Associates the memory with a user. Provide when you want the memory scoped to a specific identity. |
| `messages` | array | No* | Conversation turns for Mem0 to infer memories from. Each object should include `role` and `content`. |
| `messages` | array | Yes | Conversation turns for Mem0 to extract memories from. Each object should include `role` and `content`. |
| `user_id` | string | No* | Associates the memory with a user. |
| `agent_id` | string | No* | Associates the memory with an agent. |
| `run_id` | string | No* | Associates the memory with a run. |
| `app_id` | string | No* | Associates the memory with an app. |
| `metadata` | object | Optional | Custom key/value metadata (e.g., `{"topic": "preferences"}`). |
| `infer` | boolean (default `true`) | Optional | Set to `false` to skip inference and store the provided text as-is. |
> \* Provide at least one `messages` entry to describe what you are storing. For scoped memories, include `user_id`. You can also attach `agent_id`, `app_id`, `run_id`, `project_id`, or `org_id` to refine ownership.
> \* At least one entity ID (`user_id`, `agent_id`, `app_id`, or `run_id`) is required.
<Tip>
Need more details? See [all request parameters](#body-messages) below for complete field descriptions, types, and constraints.
@@ -56,19 +59,15 @@ Provide at least one message or direct memory string. Most callers supply `messa
## Response
Successful requests return an array of events queued for processing. Each event includes the generated memory text and an identifier you can persist for auditing.
The request is queued for background processing. The response contains an `event_id` for tracking status.
<CodeGroup>
```json 200 response
[
{
"id": "mem_01JF8ZS4Y0R0SPM13R5R6H32CJ",
"event": "ADD",
"data": {
"memory": "The user moved to Austin in 2025."
}
}
]
{
"message": "Memory processing has been queued for background execution",
"status": "PENDING",
"event_id": "evt-uuid"
}
```
```json 400 response
@@ -81,3 +80,7 @@ Successful requests return an array of events queued for processing. Each event
```
</CodeGroup>
<Info>
Poll the event status via `GET /v1/event/{event_id}/`. Status will be `SUCCEEDED` or `FAILED` once processing completes.
</Info>
+18 -6
View File
@@ -1,10 +1,12 @@
---
title: "Get Memories"
description: "Retrieve memories with advanced filtering using logical operators like AND, OR, NOT, and comparison queries."
openapi: post /v2/memories/
description: "Retrieve memories with paginated results and advanced filtering using logical operators like AND, OR, NOT, and comparison queries."
openapi: post /v3/memories/
---
The v2 get memories API is powerful and flexible, allowing for more precise memory listing without the need for a search query. It supports complex logical operations (AND, OR, NOT) and comparison operators for advanced filtering capabilities. The comparison operators include:
List memories scoped by filters with paginated results. Entity IDs (`user_id`, `agent_id`, `app_id`, `run_id`) **must** be passed inside the `filters` object — top-level entity IDs are rejected with 400.
The `filters` object supports complex logical operations (AND, OR, NOT) and comparison operators:
- `in`: Matches any of the values specified
- `gte`: Greater than or equal to
@@ -15,6 +17,8 @@ The v2 get memories API is powerful and flexible, allowing for more precise memo
- `icontains`: Case-insensitive containment check
- `*`: Wildcard character that matches everything
Pass `page` and `page_size` as query parameters to paginate through results.
<CodeGroup>
```python Code
memories = client.get_all(
@@ -27,12 +31,17 @@ memories = client.get_all(
"created_at": {"gte": "2024-07-01", "lte": "2024-07-31"}
}
]
}
},
page=1,
page_size=50
)
```
```python Output
{
"count": 2,
"next": null,
"previous": null,
"results": [
{
"id": "f4cbdb08-7062-4f3e-8eb2-9f5c80dfe64c",
@@ -46,10 +55,13 @@ memories = client.get_all(
"created_at": "2024-07-05T15:30:00Z",
"updated_at": "2024-07-05T15:30:00Z"
}
],
"total": 2
]
}
```
</CodeGroup>
<Info>
The response is a paginated envelope with `count`, `next`, `previous`, and `results`. Use `page` and `page_size` query params to step through results.
</Info>
+19 -8
View File
@@ -1,10 +1,14 @@
---
title: 'Search Memories'
description: "Search memories with semantic queries and advanced filtering using logical and comparison operators."
openapi: post /v2/memories/search/
description: "Search memories with hybrid retrieval (semantic + BM25 + entity matching) and advanced filtering using logical and comparison operators."
openapi: post /v3/memories/search/
---
The v2 search API is powerful and flexible, allowing for more precise memory retrieval. It supports complex logical operations (AND, OR, NOT) and comparison operators for advanced filtering capabilities. The comparison operators include:
Relevance-ranked hybrid search across stored memories. V3 uses multi-signal retrieval — semantic, BM25 keyword, and entity matching scored in parallel and fused. The returned `score` is a combined `[0, 1]` value.
Entity IDs (`user_id`, `agent_id`, `app_id`, `run_id`) **must** be passed inside the `filters` object — top-level entity IDs are rejected with 400. At least one entity ID is required.
The `filters` object supports complex logical operations (AND, OR, NOT) and comparison operators:
- `in`: Matches any of the values specified
- `gte`: Greater than or equal to
- `lte`: Less than or equal to
@@ -14,6 +18,14 @@ The v2 search API is powerful and flexible, allowing for more precise memory ret
- `icontains`: Case-insensitive containment check
- `*`: Wildcard character that matches everything
### Search parameter defaults
| Parameter | V1/V2 | V3 |
| --- | --- | --- |
| `top_k` | Supported (default 10) | Supported (1-1000, default 10) |
| `threshold` | No default | Default `0.1` (pass `0.0` to disable) |
| `rerank` | Default `true` | Default `false` (pass `true` to enable) |
<CodeGroup>
```python Platform API Example
related_memories = client.search(
@@ -33,20 +45,19 @@ related_memories = client.search(
```json Output
{
"memories": [
"results": [
{
"id": "ea925981-272f-40dd-b576-be64e4871429",
"memory": "Likes to play cricket and plays cricket on weekends.",
"metadata": {
"category": "hobbies"
},
"score": 0.32116443111457704,
"score": 0.82,
"created_at": "2024-07-26T10:29:36.630547-07:00",
"updated_at": null,
"user_id": "alice",
"agent_id": "sports-agent"
"categories": ["hobbies"]
}
],
]
}
```
</CodeGroup>
+28
View File
@@ -7,6 +7,23 @@ mode: "wide"
<Tabs>
<Tab title="Python">
<Update label="2026-04-25" description="v2.0.1">
**Bug Fixes:**
- **Client:** Map `user_id`, `agent_id`, `run_id` entity params to filters in `GET /memories` ([#4960](https://github.com/mem0ai/mem0/pull/4960))
- **Memory:** Honor `prompt` param in vector store extraction pipeline ([#4914](https://github.com/mem0ai/mem0/pull/4914))
- **Memory:** Add missing `text_lemmatized` field in `AsyncMemory._create_memory` ([#4886](https://github.com/mem0ai/mem0/pull/4886))
- **Memory:** Merge same-key operator dicts in AND metadata filters ([#4853](https://github.com/mem0ai/mem0/pull/4853))
- **LLMs:** Narrow `_is_reasoning_model` check to not match `gpt-5.x` variants ([#4746](https://github.com/mem0ai/mem0/pull/4746))
- **Vector Stores:** Add `ca_certs` config option for Elasticsearch vector store ([#3993](https://github.com/mem0ai/mem0/pull/3993))
- **Vector Stores:** Add `agent_id` and `run_id` to Elasticsearch/OpenSearch default mappings ([#4906](https://github.com/mem0ai/mem0/pull/4906))
- **Embeddings:** Set FastEmbed `embedding_dims` from model metadata at init ([#4711](https://github.com/mem0ai/mem0/pull/4711))
**Security:**
- Bump vulnerable dependencies to patched versions ([#4835](https://github.com/mem0ai/mem0/pull/4835))
</Update>
<Update label="2026-04-14" description="v2.0.0">
**Major Release** — Python SDK with V3 memory pipeline, ADD-only extraction, and cleaned-up API surface.
@@ -893,6 +910,17 @@ See the [OSS v1 to v2 migration guide](https://docs.mem0.ai/migration/oss-v1-to-
</Tab>
<Tab title="TypeScript">
<Update label="2026-04-25" description="v3.0.2">
**Bug Fixes:**
- **LLMs:** Forward `timeout` config to OpenAI client in JS OSS LLM providers ([#4770](https://github.com/mem0ai/mem0/pull/4770))
**Improvements:**
- **Telemetry:** Harden TS telemetry version injection and require changelog entry on version bump ([#4900](https://github.com/mem0ai/mem0/pull/4900))
- **Docs:** Update memory tool list, CLI usage, and config file reading logic ([#4861](https://github.com/mem0ai/mem0/pull/4861))
</Update>
<Update label="2026-04-20" description="v3.0.1">
**Bug Fixes:**
Binary file not shown.

Before

Width:  |  Height:  |  Size: 293 KiB

After

Width:  |  Height:  |  Size: 139 KiB

+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mem0ai",
"version": "3.0.1",
"version": "3.0.2",
"description": "The Memory Layer For Your AI Apps",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
@@ -14,6 +14,7 @@ class ElasticsearchConfig(BaseModel):
api_key: Optional[str] = Field(None, description="API key for authentication")
embedding_model_dims: int = Field(1536, description="Dimension of the embedding vector")
verify_certs: bool = Field(True, description="Verify SSL certificates")
ca_certs: Optional[str] = Field(None, description="Path to CA bundle for SSL certificate verification")
use_ssl: bool = Field(True, description="Use SSL for connection")
auto_create_index: bool = Field(True, description="Automatically create index during initialization")
custom_search_query: Optional[Callable[[List[float], int, Optional[Dict]], Dict]] = Field(
+7 -5
View File
@@ -656,10 +656,10 @@ class Memory(MemoryBase):
else:
messages = parse_vision_messages(messages)
vector_store_result = self._add_to_vector_store(messages, processed_metadata, effective_filters, infer)
vector_store_result = self._add_to_vector_store(messages, processed_metadata, effective_filters, infer, prompt=prompt)
return {"results": vector_store_result}
def _add_to_vector_store(self, messages, metadata, filters, infer):
def _add_to_vector_store(self, messages, metadata, filters, infer, prompt=None):
if not infer:
returned_memories = []
for message_dict in messages:
@@ -726,7 +726,7 @@ class Memory(MemoryBase):
if is_agent_scoped:
system_prompt += AGENT_CONTEXT_SUFFIX
custom_instr = self.custom_instructions
custom_instr = prompt or self.custom_instructions
user_prompt = generate_additive_extraction_prompt(
existing_memories=existing_memories,
@@ -2064,7 +2064,7 @@ class AsyncMemory(MemoryBase):
else:
messages = parse_vision_messages(messages)
vector_store_result = await self._add_to_vector_store(messages, processed_metadata, effective_filters, infer)
vector_store_result = await self._add_to_vector_store(messages, processed_metadata, effective_filters, infer, prompt=prompt)
return {"results": vector_store_result}
async def _add_to_vector_store(
@@ -2073,6 +2073,7 @@ class AsyncMemory(MemoryBase):
metadata: dict,
effective_filters: dict,
infer: bool,
prompt: Optional[str] = None,
):
if not infer:
returned_memories = []
@@ -2141,7 +2142,7 @@ class AsyncMemory(MemoryBase):
if is_agent_scoped:
system_prompt += AGENT_CONTEXT_SUFFIX
custom_instr = self.custom_instructions
custom_instr = prompt or self.custom_instructions
user_prompt = generate_additive_extraction_prompt(
existing_memories=existing_memories,
@@ -3008,6 +3009,7 @@ class AsyncMemory(MemoryBase):
if "created_at" not in new_metadata:
new_metadata["created_at"] = datetime.now(timezone.utc).isoformat()
new_metadata["updated_at"] = new_metadata["created_at"]
new_metadata["text_lemmatized"] = lemmatize_for_bm25(data)
await asyncio.to_thread(
self.vector_store.insert,
+2
View File
@@ -31,6 +31,7 @@ class ElasticsearchDB(VectorStoreBase):
cloud_id=config.cloud_id,
api_key=config.api_key,
verify_certs=config.verify_certs,
ca_certs=config.ca_certs,
headers= config.headers or {},
)
else:
@@ -38,6 +39,7 @@ class ElasticsearchDB(VectorStoreBase):
hosts=[f"{config.host}" if config.port is None else f"{config.host}:{config.port}"],
basic_auth=(config.user, config.password) if (config.user and config.password) else None,
verify_certs=config.verify_certs,
ca_certs=config.ca_certs,
headers= config.headers or {},
)
+2
View File
@@ -1,5 +1,7 @@
# OpenMemory
> **⚠️ Sunsetting Notice:** OpenMemory is being sunset. For local self-hosted memory with a dashboard, please use the [Mem0 self-hosted server](https://docs.mem0.ai/open-source/overview) instead. Get started with `cd server && make bootstrap`. See the [self-hosted docs](https://docs.mem0.ai/open-source/setup) for configuration details.
OpenMemory is your personal memory layer for LLMs - private, portable, and open-source. Your memories live locally, giving you complete control over your data. Build AI applications with personalized memories while keeping your data secure.
![OpenMemory](https://github.com/user-attachments/assets/3c701757-ad82-4afa-bfbe-e049c2b4320b)
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "mem0ai"
version = "2.0.0"
version = "2.0.1"
description = "Long-term memory for AI Agents"
authors = [
{ name = "Mem0", email = "support@mem0.ai" }
+2 -2
View File
@@ -395,10 +395,10 @@ def get_all_memories(
try:
if not any([user_id, run_id, agent_id]):
return _list_all_memories()
params = {
filters = {
k: v for k, v in {"user_id": user_id, "run_id": run_id, "agent_id": agent_id}.items() if v is not None
}
return get_memory_instance().get_all(**params)
return get_memory_instance().get_all(filters=filters)
except Exception:
raise upstream_error()
+37
View File
@@ -78,6 +78,43 @@ class TestAddToVectorStoreErrors:
assert result == [] # Should return empty list when no memories processed
class TestPromptOverridesCustomInstructions:
@pytest.fixture
def mock_memory(self, mocker):
mock_llm, _ = _setup_mocks(mocker)
mock_llm.return_value.generate_response.return_value = '{"memory": []}'
memory = Memory()
memory.custom_instructions = "config-level instructions"
memory.db.get_last_messages = MagicMock(return_value=[])
memory.db.save_messages = MagicMock()
return memory
def test_prompt_overrides_custom_instructions(self, mock_memory):
mock_memory._add_to_vector_store(
messages=[{"role": "user", "content": "hello"}],
metadata={},
filters={},
infer=True,
prompt="per-call override",
)
user_prompt = mock_memory.llm.generate_response.call_args[1]["messages"][1]["content"]
assert "per-call override" in user_prompt
assert "config-level instructions" not in user_prompt
def test_falls_back_to_custom_instructions_when_no_prompt(self, mock_memory):
mock_memory._add_to_vector_store(
messages=[{"role": "user", "content": "hello"}],
metadata={},
filters={},
infer=True,
)
user_prompt = mock_memory.llm.generate_response.call_args[1]["messages"][1]["content"]
assert "config-level instructions" in user_prompt
class TestAsyncUpdate:
@pytest.fixture
def mock_async_memory(self, mocker):
+1 -1
View File
@@ -61,7 +61,7 @@ def test_add(memory_instance):
assert result["results"] == [{"memory": "Test memory", "event": "ADD"}]
memory_instance._add_to_vector_store.assert_called_once_with(
[{"role": "user", "content": "Test message"}], {"user_id": "test_user"}, {"user_id": "test_user"}, True
[{"role": "user", "content": "Test message"}], {"user_id": "test_user"}, {"user_id": "test_user"}, True, prompt=None
)
+81
View File
@@ -854,3 +854,84 @@ def test_get_all_rejects_user_id_kwarg(mock_sqlite, mock_llm_factory, mock_vecto
with pytest.raises(ValueError, match=r"user_id.*filters"):
memory.get_all(user_id="u1")
# ─── Regression: AsyncMemory._create_memory must store text_lemmatized ─────────
@patch('mem0.utils.factory.EmbedderFactory.create')
@patch('mem0.utils.factory.VectorStoreFactory.create')
@patch('mem0.utils.factory.LlmFactory.create')
@patch('mem0.memory.storage.SQLiteManager')
def test_sync_create_memory_stores_text_lemmatized(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
"""Sync Memory._create_memory must include text_lemmatized in payload for BM25 keyword search."""
embedder = MagicMock()
embedder.embed.return_value = [0.1, 0.2, 0.3]
mock_embedder_factory.return_value = embedder
mock_vector_store = MagicMock()
mock_vector_store.insert.return_value = None
telemetry_vector_store = MagicMock()
mock_vector_factory.side_effect = [mock_vector_store, telemetry_vector_store]
mock_llm_factory.return_value = MagicMock()
mock_sqlite.return_value = MagicMock()
from mem0.memory.main import Memory as MemoryClass
memory = MemoryClass(MemoryConfig())
data = "I love hiking in the mountains"
embeddings = {data: [0.1, 0.2, 0.3]}
metadata = {"user_id": "test_user"}
memory._create_memory(data, embeddings, metadata)
# Check that text_lemmatized was stored in the payload
insert_call = mock_vector_store.insert.call_args
payload = insert_call.kwargs.get("payloads") or insert_call[1].get("payloads")
assert payload is not None and len(payload) == 1
assert "text_lemmatized" in payload[0], "Sync _create_memory must store text_lemmatized for BM25"
assert payload[0]["text_lemmatized"] != "", "text_lemmatized must not be empty"
@pytest.mark.asyncio
@patch('mem0.utils.factory.EmbedderFactory.create')
@patch('mem0.utils.factory.VectorStoreFactory.create')
@patch('mem0.utils.factory.LlmFactory.create')
@patch('mem0.memory.storage.SQLiteManager')
async def test_async_create_memory_stores_text_lemmatized(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
"""
Regression test: AsyncMemory._create_memory must include text_lemmatized
in the vector store payload.
Without text_lemmatized, memories created via AsyncMemory with infer=False
are invisible to BM25 keyword search, silently degrading search recall for
all async users.
"""
embedder = MagicMock()
embedder.embed.return_value = [0.1, 0.2, 0.3]
mock_embedder_factory.return_value = embedder
mock_vector_store = MagicMock()
mock_vector_store.insert.return_value = None
mock_vector_factory.return_value = mock_vector_store
mock_llm_factory.return_value = MagicMock()
mock_sqlite.return_value = MagicMock()
from mem0.memory.main import AsyncMemory
memory = AsyncMemory(MemoryConfig())
data = "I love hiking in the mountains"
embeddings = {data: [0.1, 0.2, 0.3]}
metadata = {"user_id": "test_user"}
await memory._create_memory(data, embeddings, metadata)
# Check that text_lemmatized was stored in the payload
insert_call = mock_vector_store.insert.call_args
payload = insert_call.kwargs.get("payloads") or insert_call[1].get("payloads")
assert payload is not None and len(payload) == 1
assert "text_lemmatized" in payload[0], (
"AsyncMemory._create_memory must store text_lemmatized for BM25 keyword search"
)
assert payload[0]["text_lemmatized"] != "", "text_lemmatized must not be empty"
+27
View File
@@ -561,3 +561,30 @@ class TestUpdateOpenAPISchema:
schema = client.get("/openapi.json").json()
update_props = schema["components"]["schemas"]["MemoryUpdate"]["properties"]
assert "metadata" in update_props
# ===========================================================================
# GetMemories: Entity parameters to filters mapping (fix for #4955)
# ===========================================================================
class TestGetMemories:
"""Verify that GET /memories correctly maps entity parameters to the filters dict."""
def test_get_memories_entity_filters_routing(self, client, mock_memory):
"""
Issue #4955: Test that the GET /memories route correctly handles
top-level entity parameters by mapping them to the filters dictionary
instead of passing them as direct kwargs to get_all()
"""
# Send a request with a valid top-level entity parameter
response = client.get("/memories?user_id=test_routing_user")
# 1. Verify the endpoint doesn't crash with a 500 error
assert response.status_code == 200
# 2. Verify the response is structured correctly
data = response.json()
assert isinstance(data, list)
# 3. Verify the core logic: the param was mapped to the filters dict!
_, kwargs = mock_memory.get_all.call_args
assert kwargs["filters"] == {"user_id": "test_routing_user"}