feat(memory): expose expiration controls in client docs (#5874)

Co-authored-by: Soumil Rathi <soumilrathi@gmail.com>
This commit is contained in:
soumil-rathi
2026-06-25 17:14:33 -07:00
committed by GitHub
parent 818c2981b7
commit 0fbbb2f525
19 changed files with 519 additions and 57 deletions
@@ -50,6 +50,7 @@ Provide conversation messages for Mem0 to extract memories from. At least one en
| `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. |
| `expiration_date` | string | Optional | Date in `YYYY-MM-DD` format. The memory is visible through this date and hidden by default after it passes. |
> \* At least one entity ID (`user_id`, `agent_id`, `app_id`, or `run_id`) is required.
@@ -83,3 +84,11 @@ The request is queued for background processing. The response contains an `event
<Info>
Poll the event status via `GET /v1/event/{event_id}/`. Status will be `SUCCEEDED` or `FAILED` once processing completes.
</Info>
<Info>
Memories with `expiration_date` remain stored after they expire. Search and get-all hide them by default; pass `show_expired: true` to include them.
</Info>
<Info>
Python uses `expiration_date`; TypeScript uses `expirationDate`.
</Info>
@@ -6,6 +6,10 @@ openapi: post /v3/memories/
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.
Expired memories are hidden by default. Pass `show_expired: true` to include memories whose `expiration_date` has passed.
Python uses `show_expired`; TypeScript uses `showExpired`.
The `filters` object supports complex logical operations (AND, OR, NOT) and comparison operators:
- `in`: Matches any of the values specified
@@ -32,6 +36,7 @@ memories = client.get_all(
}
]
},
show_expired=False,
page=1,
page_size=50
)
@@ -46,12 +51,14 @@ memories = client.get_all(
{
"id": "f4cbdb08-7062-4f3e-8eb2-9f5c80dfe64c",
"memory": "Alex is planning a trip to San Francisco from July 1st to July 10th",
"expiration_date": null,
"created_at": "2024-07-01T12:00:00Z",
"updated_at": "2024-07-01T12:00:00Z"
},
{
"id": "a2b8c3d4-5e6f-7g8h-9i0j-1k2l3m4n5o6p",
"memory": "Alex prefers vegetarian restaurants",
"expiration_date": null,
"created_at": "2024-07-05T15:30:00Z",
"updated_at": "2024-07-05T15:30:00Z"
}
@@ -8,6 +8,10 @@ Relevance-ranked hybrid search across stored memories. V3 uses multi-signal retr
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.
Expired memories are hidden by default. Pass `show_expired: true` to include memories whose `expiration_date` has passed.
Python uses `show_expired`; TypeScript uses `showExpired`.
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
@@ -30,6 +34,7 @@ The `filters` object supports complex logical operations (AND, OR, NOT) and comp
```python Platform API Example
related_memories = client.search(
query="What are Alice's hobbies?",
show_expired=False,
filters={
"OR": [
{
@@ -54,6 +59,7 @@ related_memories = client.search(
"category": "hobbies"
},
"score": 0.82,
"expiration_date": null,
"created_at": "2024-07-26T10:29:36.630547-07:00",
"updated_at": null,
"categories": ["hobbies"]
+11 -2
View File
@@ -1,5 +1,14 @@
---
title: 'Update Memory'
description: "Update the content or metadata of a single memory by its unique ID using the PUT endpoint."
description: "Update the content, metadata, timestamp, or expiration date of a single memory by its unique ID using the PUT endpoint."
openapi: put /v1/memories/{memory_id}/
---
---
Use this endpoint to update mutable memory fields. To make a memory expire, set `expiration_date` to a `YYYY-MM-DD` date. To make it permanent again, send `expiration_date: null`.
```python
client.update("mem_123", expiration_date="2030-01-31")
client.update("mem_123", expiration_date=None)
```
TypeScript uses `expirationDate`.
+4 -4
View File
@@ -62,7 +62,7 @@ These changes produce a **+20 point improvement on LoCoMo** (71.4 → 91.6) and
|---|---|---|---|
| Constructor | `MemoryClient(api_key, org_id, project_id)` | `MemoryClient(api_key)` | Remove `org_id`, `project_id` from constructor |
| Method options | `client.add(messages, **kwargs)` | `client.add(messages, options=AddMemoryOptions(...))` | Use typed option classes (or `**kwargs` still works) |
| Removed params | `api_version`, `output_format`, `async_mode`, `filter_memories`, `expiration_date`, `keyword_search`, `force_add_only`, `batch_size`, `immutable`, `includes`, `excludes`, `enable_graph`, `org_name`, `project_name` | — | Remove from all calls |
| Removed params | `api_version`, `output_format`, `async_mode`, `filter_memories`, `keyword_search`, `force_add_only`, `batch_size`, `immutable`, `includes`, `excludes`, `enable_graph`, `org_name`, `project_name` | — | Remove from all calls |
### TypeScript Client SDK
@@ -70,7 +70,7 @@ These changes produce a **+20 point improvement on LoCoMo** (71.4 → 91.6) and
|---|---|---|---|
| Constructor | `new MemoryClient({ apiKey, organizationId, projectId })` | `new MemoryClient({ apiKey })` | Remove `organizationId`, `projectId`, `organizationName`, `projectName` |
| All params | snake_case: `user_id`, `agent_id`, `top_k` | camelCase: `userId`, `agentId`, `topK` | Rename all params to camelCase |
| Removed params | `api_version`, `output_format`, `async_mode`, `enable_graph`, `org_id`, `project_id`, `org_name`, `project_name`, `filter_memories`, `batch_size`, `force_add_only`, `immutable`, `expiration_date`, `includes`, `excludes`, `keyword_search` | — | Remove from all calls |
| Removed params | `api_version`, `output_format`, `async_mode`, `enable_graph`, `org_id`, `project_id`, `org_name`, `project_name`, `filter_memories`, `batch_size`, `force_add_only`, `immutable`, `includes`, `excludes`, `keyword_search` | — | Remove from all calls |
| Output format enum | `OutputFormat.V1`, `OutputFormat.V1_1` | Removed | v1.1 is now always used |
| API version enum | `API_VERSION.V1`, `API_VERSION.V2` | Removed | Handled internally |
@@ -423,7 +423,7 @@ These parameters have been removed across all SDKs. Remove them from your code:
**All methods:** `api_version`, `output_format`, `async_mode`, `org_name`, `project_name`, `org_id`, `project_id`
**add():** `enable_graph`, `immutable`, `expiration_date`, `filter_memories`, `batch_size`, `force_add_only`, `includes`, `excludes`, `keyword_search`
**add():** `enable_graph`, `immutable`, `filter_memories`, `batch_size`, `force_add_only`, `includes`, `excludes`, `keyword_search`
**search():** `enable_graph`
@@ -437,7 +437,7 @@ These parameters have been removed across all SDKs. Remove them from your code:
**All methods:** `OutputFormat` enum, `API_VERSION` enum
**add():** `enable_graph` / `enableGraph`, `async_mode` / `asyncMode`, `output_format` / `outputFormat`, `immutable`, `expiration_date` / `expirationDate`, `filter_memories` / `filterMemories`, `batch_size` / `batchSize`, `force_add_only` / `forceAddOnly`, `includes`, `excludes`, `keyword_search` / `keywordSearch`
**add():** `enable_graph` / `enableGraph`, `async_mode` / `asyncMode`, `output_format` / `outputFormat`, `immutable`, `filter_memories` / `filterMemories`, `batch_size` / `batchSize`, `force_add_only` / `forceAddOnly`, `includes`, `excludes`, `keyword_search` / `keywordSearch`
**search():** `enable_graph` / `enableGraph`
+2 -2
View File
@@ -215,7 +215,7 @@ client.add(messages, user_id="alice")
# async_mode and output_format removed (async by default, v1.1 always)
```
**Removed parameters:** `org_id`, `project_id`, `api_version`, `output_format`, `async_mode`, `enable_graph`, `immutable`, `expiration_date`, `filter_memories`, `batch_size`, `force_add_only`, `includes`, `excludes`, `keyword_search`, `org_name`, `project_name`
**Removed parameters:** `org_id`, `project_id`, `api_version`, `output_format`, `async_mode`, `enable_graph`, `immutable`, `filter_memories`, `batch_size`, `force_add_only`, `includes`, `excludes`, `keyword_search`, `org_name`, `project_name`
### TypeScript Client SDK
@@ -242,7 +242,7 @@ await client.search("query", {
});
```
**Removed:** `OutputFormat` enum, `API_VERSION` enum, `organizationId`, `projectId`, `organizationName`, `projectName`, `enableGraph`, `asyncMode`, `outputFormat`, `immutable`, `expirationDate`, `filterMemories`, `batchSize`, `forceAddOnly`, `includes`, `excludes`, `keywordSearch`
**Removed:** `OutputFormat` enum, `API_VERSION` enum, `organizationId`, `projectId`, `organizationName`, `projectName`, `enableGraph`, `asyncMode`, `outputFormat`, `immutable`, `filterMemories`, `batchSize`, `forceAddOnly`, `includes`, `excludes`, `keywordSearch`
<Info>
For the full list of parameter changes across all SDKs, see the [OSS migration guide](/migration/oss-v2-to-v3#removed-parameters-reference).
+23 -1
View File
@@ -1779,6 +1779,11 @@
"type": "object",
"description": "Entity and metadata filters. Must include at least one entity ID (`user_id`, `agent_id`, `app_id`, or `run_id`).",
"additionalProperties": true
},
"show_expired": {
"type": "boolean",
"default": false,
"description": "When true, include memories whose `expiration_date` has passed. Expired memories are hidden by default."
}
}
},
@@ -1977,6 +1982,12 @@
"additionalProperties": true,
"description": "User-supplied metadata to attach to each extracted memory."
},
"expiration_date": {
"type": "string",
"format": "date",
"nullable": true,
"description": "Optional expiration date in YYYY-MM-DD format. After this date, memories are hidden from search and get-all unless `show_expired` is true."
},
"custom_instructions": {
"type": "string",
"description": "Project-level instructions that guide extraction for this call."
@@ -2094,6 +2105,11 @@
"description": "Entity and metadata filters. Must include at least one entity ID (`user_id`, `agent_id`, `app_id`, or `run_id`). Supports `AND`, `OR`, `NOT`, and comparison operators (`in`, `gte`, `lte`, `gt`, `lt`, `contains`, `icontains`, `ne`).",
"additionalProperties": true
},
"show_expired": {
"type": "boolean",
"default": false,
"description": "When true, include memories whose `expiration_date` has passed. Expired memories are hidden by default."
},
"top_k": {
"type": "integer",
"minimum": 1,
@@ -2432,6 +2448,12 @@
"metadata": {
"type": "object",
"description": "Additional metadata associated with the memory"
},
"expiration_date": {
"type": "string",
"format": "date",
"nullable": true,
"description": "Expiration date in YYYY-MM-DD format, or null to clear the expiration date."
}
}
}
@@ -6256,4 +6278,4 @@
}
},
"x-original-swagger-version": "2.0"
}
}
+6 -2
View File
@@ -281,19 +281,22 @@ export default class MemoryClient {
text,
metadata,
timestamp,
expirationDate,
}: {
text?: string;
metadata?: Record<string, any>;
timestamp?: number | string;
expirationDate?: string | null;
},
): Promise<Array<Memory>> {
if (
text === undefined &&
metadata === undefined &&
timestamp === undefined
timestamp === undefined &&
expirationDate === undefined
) {
throw new Error(
"At least one of text, metadata, or timestamp must be provided for update.",
"At least one of text, metadata, timestamp, or expirationDate must be provided for update.",
);
}
@@ -302,6 +305,7 @@ export default class MemoryClient {
if (text !== undefined) payload.text = text;
if (metadata !== undefined) payload.metadata = metadata;
if (timestamp !== undefined) payload.timestamp = timestamp;
if (expirationDate !== undefined) payload.expiration_date = expirationDate;
const payloadKeys = Object.keys(payload);
this._captureEvent("update", [payloadKeys]);
+4
View File
@@ -13,6 +13,7 @@ export interface AddMemoryOptions extends EntityOptions {
customCategories?: custom_categories[];
customInstructions?: string;
timestamp?: number;
expirationDate?: string;
structuredDataSchema?: Record<string, any>;
}
@@ -25,6 +26,7 @@ export interface SearchMemoryOptions {
latestOnly?: boolean;
fields?: string[];
categories?: string[];
showExpired?: boolean;
}
export interface GetAllMemoryOptions {
@@ -35,6 +37,7 @@ export interface GetAllMemoryOptions {
endDate?: string;
latestOnly?: boolean;
categories?: string[];
showExpired?: boolean;
}
export interface DeleteAllMemoryOptions extends EntityOptions {}
@@ -119,6 +122,7 @@ export interface Memory {
memoryType?: string;
score?: number;
metadata?: any | null;
expirationDate?: string | null;
owner?: string | null;
agentId?: string | null;
appId?: string | null;
@@ -59,6 +59,21 @@ describe("MemoryClient - add()", () => {
expect(getFetchBody(call!).user_id).toBe("user_1");
});
test("serializes expirationDate as expiration_date", async () => {
const extra = new Map<string, { status: number; body: unknown }>();
extra.set("/v3/memories/add/", { status: 200, body: [createMockMemory()] });
const mock = setupMockFetch(extra);
const client = new MemoryClient({ apiKey: TEST_API_KEY });
await client.add([{ role: "user", content: "test" }], {
userId: "u1",
expirationDate: "2030-01-31",
});
const call = findFetchCall(mock, "/v3/memories/add/", "POST");
expect(getFetchBody(call!).expiration_date).toBe("2030-01-31");
});
test("throws an error when given an empty messages array", async () => {
setupMockFetch();
@@ -176,11 +191,26 @@ describe("MemoryClient - update()", () => {
expect(body.timestamp).toBe(1710600000);
});
test("sends expirationDate as expiration_date, including null", async () => {
const extra = new Map<string, { status: number; body: unknown }>();
extra.set("/v1/memories/mem_123/", {
status: 200,
body: createMockMemory(),
});
const mock = setupMockFetch(extra);
const client = new MemoryClient({ apiKey: TEST_API_KEY });
await client.update("mem_123", { expirationDate: null });
const call = findFetchCall(mock, "/v1/memories/mem_123/", "PUT");
expect(getFetchBody(call!).expiration_date).toBeNull();
});
test("throws when no fields provided", async () => {
setupMockFetch();
const client = new MemoryClient({ apiKey: TEST_API_KEY });
await expect(client.update("mem_123", {})).rejects.toThrow(
"At least one of text, metadata, or timestamp must be provided",
"At least one of text, metadata, timestamp, or expirationDate must be provided",
);
});
});
@@ -81,6 +81,24 @@ describe("MemoryClient - search()", () => {
expect(getFetchBody(call!).latest_only).toBe(true);
});
test("serializes showExpired as show_expired", async () => {
const extra = new Map<string, { status: number; body: unknown }>();
extra.set("/v3/memories/search/", {
status: 200,
body: { results: [] },
});
const mock = setupMockFetch(extra);
const client = new MemoryClient({ apiKey: TEST_API_KEY });
await client.search("test", {
filters: { user_id: "u1" },
showExpired: true,
});
const call = findFetchCall(mock, "/v3/memories/search/", "POST");
expect(getFetchBody(call!).show_expired).toBe(true);
});
test("passes complex OR filters through to the API body", async () => {
const extra = new Map<string, { status: number; body: unknown }>();
extra.set("/v3/memories/search/", {
@@ -300,4 +318,22 @@ describe("MemoryClient - getAll() entity param rejection", () => {
const call = findFetchCall(mock, "/v3/memories/", "POST");
expect(getFetchBody(call!).latest_only).toBe(true);
});
test("serializes showExpired as show_expired", async () => {
const extra = new Map<string, { status: number; body: unknown }>();
extra.set("/v3/memories/", {
status: 200,
body: { results: [] },
});
const mock = setupMockFetch(extra);
const client = new MemoryClient({ apiKey: TEST_API_KEY });
await client.getAll({
filters: { user_id: "u1" },
showExpired: true,
});
const call = findFetchCall(mock, "/v3/memories/", "POST");
expect(getFetchBody(call!).show_expired).toBe(true);
});
});
+14 -10
View File
@@ -344,24 +344,26 @@ class MemoryClient:
Args:
memory_id: The ID of the memory to update.
options: Typed options (UpdateMemoryOptions) with text, metadata,
and/or timestamp fields.
**kwargs: Alternatively pass text, metadata, timestamp as keyword args.
timestamp, and/or expiration_date fields.
**kwargs: Alternatively pass text, metadata, timestamp, or
expiration_date as keyword args.
Returns:
Dict[str, Any]: The response from the server.
Raises:
ValueError: If none of text, metadata, or timestamp are provided.
ValueError: If none of text, metadata, timestamp, or expiration_date are provided.
Example:
>>> client.update("mem_123", UpdateMemoryOptions(text="Updated text"))
>>> client.update("mem_123", text="Updated text")
>>> client.update("mem_123", expiration_date=None)
"""
payload = {**(options.model_dump(exclude_unset=True) if options else {}), **kwargs}
payload = {k: v for k, v in payload.items() if v is not None}
payload = {k: v for k, v in payload.items() if v is not None or k == "expiration_date"}
if not payload:
raise ValueError("At least one of text, metadata, or timestamp must be provided for update.")
raise ValueError("At least one of text, metadata, timestamp, or expiration_date must be provided for update.")
capture_client_event("client.update", self, {"memory_id": memory_id, "sync_type": "sync"})
params = self._prepare_params()
@@ -1260,24 +1262,26 @@ class AsyncMemoryClient:
Args:
memory_id: The ID of the memory to update.
options: Typed options (UpdateMemoryOptions) with text, metadata,
and/or timestamp fields.
**kwargs: Alternatively pass text, metadata, timestamp as keyword args.
timestamp, and/or expiration_date fields.
**kwargs: Alternatively pass text, metadata, timestamp, or
expiration_date as keyword args.
Returns:
Dict[str, Any]: The response from the server.
Raises:
ValueError: If none of text, metadata, or timestamp are provided.
ValueError: If none of text, metadata, timestamp, or expiration_date are provided.
Example:
>>> await client.update("mem_123", UpdateMemoryOptions(text="Updated text"))
>>> await client.update("mem_123", text="Updated text")
>>> await client.update("mem_123", expiration_date=None)
"""
payload = {**(options.model_dump(exclude_unset=True) if options else {}), **kwargs}
payload = {k: v for k, v in payload.items() if v is not None}
payload = {k: v for k, v in payload.items() if v is not None or k == "expiration_date"}
if not payload:
raise ValueError("At least one of text, metadata, or timestamp must be provided for update.")
raise ValueError("At least one of text, metadata, timestamp, or expiration_date must be provided for update.")
capture_client_event("client.update", self, {"memory_id": memory_id, "sync_type": "async"})
params = self._prepare_params()
+4
View File
@@ -29,6 +29,7 @@ class AddMemoryOptions(BaseModel):
)
custom_instructions: Optional[str] = Field(default=None, description="Custom instructions for fact extraction")
timestamp: Optional[int] = Field(default=None, description="Unix timestamp for the memory")
expiration_date: Optional[str] = Field(default=None, description="Expiration date in YYYY-MM-DD format")
structured_data_schema: Optional[Dict[str, Any]] = Field(
default=None, description="Schema for structured data extraction"
)
@@ -50,6 +51,7 @@ class SearchMemoryOptions(BaseModel):
threshold: Optional[float] = Field(default=None, description="Minimum similarity score threshold")
fields: Optional[List[str]] = Field(default=None, description="Fields to include in the response")
categories: Optional[List[str]] = Field(default=None, description="Categories to filter by")
show_expired: Optional[bool] = Field(default=None, description="Whether to include expired memories")
class GetAllMemoryOptions(BaseModel):
@@ -71,6 +73,7 @@ class GetAllMemoryOptions(BaseModel):
default=None, description="Filter memories created on or before this date (ISO 8601)"
)
categories: Optional[List[str]] = Field(default=None, description="Categories to filter by")
show_expired: Optional[bool] = Field(default=None, description="Whether to include expired memories")
class DeleteAllMemoryOptions(BaseModel):
@@ -91,6 +94,7 @@ class UpdateMemoryOptions(BaseModel):
text: Optional[str] = Field(default=None, description="New text content for the memory")
metadata: Optional[Dict[str, Any]] = Field(default=None, description="Updated metadata")
timestamp: Optional[Union[int, float, str]] = Field(default=None, description="Updated timestamp")
expiration_date: Optional[str] = Field(default=None, description="Expiration date in YYYY-MM-DD format, or None to clear")
class ProjectUpdateOptions(BaseModel):
+144 -24
View File
@@ -9,7 +9,7 @@ import time
import uuid
import warnings
from copy import deepcopy
from datetime import datetime, timezone
from datetime import date, datetime, timezone
from typing import Any, Dict, Optional
from pydantic import ValidationError
@@ -378,9 +378,37 @@ def _entity_collection_name(provider: str, collection_name: str) -> str:
return f"{collection_name}{separator}entities"
def _normalize_expiration_date(value: Any) -> Optional[str]:
if value is None:
return None
if isinstance(value, datetime):
return value.date().isoformat()
if isinstance(value, date):
return value.isoformat()
if isinstance(value, str):
try:
return date.fromisoformat(value).isoformat()
except ValueError as exc:
raise ValueError("expiration_date must be a valid date in YYYY-MM-DD format.") from exc
raise ValueError("expiration_date must be a date string in YYYY-MM-DD format.")
def _payload_is_expired(payload: Optional[Dict[str, Any]]) -> bool:
if not payload:
return False
expiration_date = payload.get("expiration_date")
if not expiration_date:
return False
try:
return date.fromisoformat(str(expiration_date)) < datetime.now(timezone.utc).date()
except ValueError:
return False
setup_config()
logger = logging.getLogger(__name__)
_UNSET = object()
_PROJECT_UPDATE_UNSUPPORTED_ERROR = "Project updates are not supported by the OSS Memory SDK."
@@ -694,6 +722,7 @@ class Memory(MemoryBase):
run_id: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
timestamp: Optional[Any] = None,
expiration_date: Optional[Any] = None,
infer: bool = True,
memory_type: Optional[str] = None,
prompt: Optional[str] = None,
@@ -712,6 +741,8 @@ class Memory(MemoryBase):
run_id (str, optional): ID of the run creating the memory. Defaults to None.
metadata (dict, optional): Metadata to store with the memory. Defaults to None.
timestamp (Any, optional): Platform-only temporal parameter. Not supported in OSS.
expiration_date (Any, optional): Date in YYYY-MM-DD format. Expired memories are hidden
from search and get_all unless show_expired is True.
infer (bool, optional): If True (default), an LLM is used to extract key facts from
'messages' and decide whether to add, update, or delete related memories.
If False, 'messages' are added as raw memories directly.
@@ -737,6 +768,7 @@ class Memory(MemoryBase):
if timestamp is not None:
raise ValueError(get_temporal_feature_error_message("sync", "add", "timestamp"))
normalized_expiration_date = _normalize_expiration_date(expiration_date)
temporal_usage_notice = detect_temporal_usage_from_metadata(metadata)
processed_metadata, effective_filters = _build_filters_and_metadata(
user_id=user_id,
@@ -744,6 +776,8 @@ class Memory(MemoryBase):
run_id=run_id,
input_metadata=metadata,
)
if normalized_expiration_date is not None:
processed_metadata["expiration_date"] = normalized_expiration_date
if memory_type is not None and memory_type != MemoryType.PROCEDURAL.value:
raise Mem0ValidationError(
@@ -1141,6 +1175,7 @@ class Memory(MemoryBase):
"actor_id",
"role",
"attributed_to",
"expiration_date",
]
core_and_promoted_keys = {"data", "hash", "created_at", "updated_at", "id", "text_lemmatized", "attributed_to", *promoted_payload_keys}
@@ -1169,6 +1204,7 @@ class Memory(MemoryBase):
*,
filters: Optional[Dict[str, Any]] = None,
top_k: int = 20,
show_expired: bool = False,
**kwargs,
):
"""
@@ -1179,6 +1215,7 @@ class Memory(MemoryBase):
Must contain at least one of: user_id, agent_id, run_id.
Example: filters={"user_id": "u1", "agent_id": "a1"}
top_k (int, optional): The maximum number of memories to return. Defaults to 20.
show_expired (bool, optional): Include expired memories. Defaults to False.
Returns:
dict: A dictionary containing a list of memories under the "results" key.
@@ -1217,6 +1254,7 @@ class Memory(MemoryBase):
)
limit = top_k
fetch_limit = limit if show_expired else max(limit * 4, 60)
scale_threshold_notice = detect_scale_threshold_from_top_k(top_k)
keys, encoded_ids = process_telemetry_filters(effective_filters)
@@ -1224,7 +1262,7 @@ class Memory(MemoryBase):
"mem0.get_all", self, {"limit": limit, "keys": keys, "encoded_ids": encoded_ids, "sync_type": "sync"}
)
all_memories_result = self._get_all_from_vector_store(effective_filters, limit)
all_memories_result = self._get_all_from_vector_store(effective_filters, fetch_limit, show_expired, limit)
if scale_threshold_notice:
display_scale_threshold_notice(self, "sync", "get_all", *scale_threshold_notice)
@@ -1232,7 +1270,7 @@ class Memory(MemoryBase):
display_first_run_notice(self, "sync", "get_all")
return {"results": all_memories_result}
def _get_all_from_vector_store(self, filters, limit):
def _get_all_from_vector_store(self, filters, limit, show_expired=False, output_limit=None):
memories_result = self.vector_store.list(filters=filters, top_k=limit)
# Handle different vector store return formats by inspecting first element
@@ -1255,11 +1293,14 @@ class Memory(MemoryBase):
"actor_id",
"role",
"attributed_to",
"expiration_date",
]
core_and_promoted_keys = {"data", "hash", "created_at", "updated_at", "id", "text_lemmatized", "attributed_to", *promoted_payload_keys}
formatted_memories = []
for mem in actual_memories:
if not show_expired and _payload_is_expired(mem.payload):
continue
memory_item_dict = MemoryItem(
id=mem.id,
memory=mem.payload.get("data", ""),
@@ -1277,6 +1318,8 @@ class Memory(MemoryBase):
memory_item_dict["metadata"] = additional_metadata
formatted_memories.append(memory_item_dict)
if output_limit is not None and len(formatted_memories) >= output_limit:
break
return formatted_memories
@@ -1290,6 +1333,7 @@ class Memory(MemoryBase):
rerank: bool = False,
explain: bool = False,
reference_date: Optional[Any] = None,
show_expired: bool = False,
**kwargs,
):
"""
@@ -1322,6 +1366,7 @@ class Memory(MemoryBase):
rerank (bool, optional): Whether to rerank results. Defaults to False.
explain (bool, optional): Whether to include score_details for each result. Defaults to False.
reference_date (Any, optional): Platform-only temporal parameter. Not supported in OSS.
show_expired (bool, optional): Include expired memories. Defaults to False.
Returns:
dict: A dictionary containing the search results under a "results" key.
@@ -1393,7 +1438,9 @@ class Memory(MemoryBase):
)
search_start = time.perf_counter()
original_memories = self._search_vector_store(query, effective_filters, limit, threshold, explain=explain)
original_memories = self._search_vector_store(
query, effective_filters, limit, threshold, explain=explain, show_expired=show_expired
)
search_elapsed_seconds = time.perf_counter() - search_start
# Apply reranking if enabled and reranker is available
@@ -1525,7 +1572,7 @@ class Memory(MemoryBase):
return True
return False
def _search_vector_store(self, query, filters, limit, threshold=0.1, explain=False):
def _search_vector_store(self, query, filters, limit, threshold=0.1, explain=False, show_expired=False):
# Guard against None threshold (backward compat)
if threshold is None:
threshold = 0.1
@@ -1566,11 +1613,14 @@ class Memory(MemoryBase):
# Step 7: Build candidate set from semantic results
candidates = []
for mem in semantic_results:
payload = mem.payload if hasattr(mem, 'payload') else {}
if not show_expired and _payload_is_expired(payload):
continue
mem_id = str(mem.id)
candidates.append({
"id": mem_id,
"score": mem.score,
"payload": mem.payload if hasattr(mem, 'payload') else {},
"payload": payload,
})
# Step 8: Score and rank
@@ -1591,6 +1641,7 @@ class Memory(MemoryBase):
"actor_id",
"role",
"attributed_to",
"expiration_date",
]
core_and_promoted_keys = {"data", "hash", "created_at", "updated_at", "id", "text_lemmatized", "attributed_to", *promoted_payload_keys}
@@ -1708,14 +1759,21 @@ class Memory(MemoryBase):
return memory_boosts
def update(self, memory_id, data, metadata: Optional[Dict[str, Any]] = None):
def update(
self,
memory_id,
data: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
expiration_date: Any = _UNSET,
):
"""
Update a memory by ID.
Args:
memory_id (str): ID of the memory to update.
data (str): New content to update the memory with.
data (str, optional): New content to update the memory with.
metadata (dict, optional): Metadata to update with the memory. Defaults to None.
expiration_date (Any, optional): Date in YYYY-MM-DD format, or None to clear it.
Returns:
dict: Success message indicating the memory was updated.
@@ -1726,9 +1784,19 @@ class Memory(MemoryBase):
"""
capture_event("mem0.update", self, {"memory_id": memory_id, "sync_type": "sync"})
existing_embeddings = {data: self.embedding_model.embed(data, "update")}
if data is None and metadata is None and expiration_date is _UNSET:
raise ValueError("At least one of data, metadata, or expiration_date must be provided.")
self._update_memory(memory_id, data, existing_embeddings, metadata)
update_metadata = deepcopy(metadata) if metadata is not None else None
if expiration_date is not _UNSET:
update_metadata = update_metadata or {}
update_metadata["expiration_date"] = _normalize_expiration_date(expiration_date)
existing_embeddings = {}
if data is not None:
existing_embeddings[data] = self.embedding_model.embed(data, "update")
self._update_memory(memory_id, data, existing_embeddings, update_metadata)
display_first_run_notice(self, "sync", "update")
return {"message": "Memory updated successfully!"}
@@ -1895,6 +1963,11 @@ class Memory(MemoryBase):
raise ValueError(f"Memory with id {memory_id} not found. Please provide a valid 'memory_id'")
prev_value = existing_memory.payload.get("data")
if data is None:
data = prev_value
if not isinstance(data, str):
raise ValueError(f"Memory with id {memory_id} does not have text content to update")
text_changed = data != prev_value
new_metadata = deepcopy(existing_memory.payload)
if metadata is not None:
@@ -1936,8 +2009,9 @@ class Memory(MemoryBase):
# Entity-store cleanup: strip this memory's id from old-text entities,
# then re-extract entities from the new text and link them back.
session_filters = {k: new_metadata[k] for k in ("user_id", "agent_id", "run_id") if new_metadata.get(k)}
self._remove_memory_from_entity_store(memory_id, session_filters)
self._link_entities_for_memory(memory_id, data, session_filters)
if text_changed:
self._remove_memory_from_entity_store(memory_id, session_filters)
self._link_entities_for_memory(memory_id, data, session_filters)
return memory_id
@@ -2284,6 +2358,7 @@ class AsyncMemory(MemoryBase):
run_id: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
timestamp: Optional[Any] = None,
expiration_date: Optional[Any] = None,
infer: bool = True,
memory_type: Optional[str] = None,
prompt: Optional[str] = None,
@@ -2299,6 +2374,8 @@ class AsyncMemory(MemoryBase):
run_id (str, optional): ID of the run creating the memory. Defaults to None.
metadata (dict, optional): Metadata to store with the memory. Defaults to None.
timestamp (Any, optional): Platform-only temporal parameter. Not supported in OSS.
expiration_date (Any, optional): Date in YYYY-MM-DD format. Expired memories are hidden
from search and get_all unless show_expired is True.
infer (bool, optional): Whether to infer the memories. Defaults to True.
memory_type (str, optional): Type of memory to create. Defaults to None.
Pass "procedural_memory" to create procedural memories.
@@ -2310,10 +2387,13 @@ class AsyncMemory(MemoryBase):
if timestamp is not None:
raise ValueError(await get_temporal_feature_error_message_async("async", "add", "timestamp"))
normalized_expiration_date = _normalize_expiration_date(expiration_date)
temporal_usage_notice = detect_temporal_usage_from_metadata(metadata)
processed_metadata, effective_filters = _build_filters_and_metadata(
user_id=user_id, agent_id=agent_id, run_id=run_id, input_metadata=metadata
)
if normalized_expiration_date is not None:
processed_metadata["expiration_date"] = normalized_expiration_date
if memory_type is not None and memory_type != MemoryType.PROCEDURAL.value:
raise ValueError(
@@ -2716,6 +2796,7 @@ class AsyncMemory(MemoryBase):
"actor_id",
"role",
"attributed_to",
"expiration_date",
]
core_and_promoted_keys = {"data", "hash", "created_at", "updated_at", "id", "text_lemmatized", "attributed_to", *promoted_payload_keys}
@@ -2744,6 +2825,7 @@ class AsyncMemory(MemoryBase):
*,
filters: Optional[Dict[str, Any]] = None,
top_k: int = 20,
show_expired: bool = False,
**kwargs,
):
"""
@@ -2754,6 +2836,7 @@ class AsyncMemory(MemoryBase):
Must contain at least one of: user_id, agent_id, run_id.
Example: filters={"user_id": "u1", "agent_id": "a1"}
top_k (int, optional): The maximum number of memories to return. Defaults to 20.
show_expired (bool, optional): Include expired memories. Defaults to False.
Returns:
dict: A dictionary containing a list of memories under the "results" key.
@@ -2792,6 +2875,7 @@ class AsyncMemory(MemoryBase):
)
limit = top_k
fetch_limit = limit if show_expired else max(limit * 4, 60)
scale_threshold_notice = detect_scale_threshold_from_top_k(top_k)
keys, encoded_ids = process_telemetry_filters(effective_filters)
@@ -2799,7 +2883,7 @@ class AsyncMemory(MemoryBase):
"mem0.get_all", self, {"limit": limit, "keys": keys, "encoded_ids": encoded_ids, "sync_type": "async"}
)
all_memories_result = await self._get_all_from_vector_store(effective_filters, limit)
all_memories_result = await self._get_all_from_vector_store(effective_filters, fetch_limit, show_expired, limit)
if scale_threshold_notice:
await display_scale_threshold_notice_async(self, "async", "get_all", *scale_threshold_notice)
@@ -2807,7 +2891,7 @@ class AsyncMemory(MemoryBase):
await display_first_run_notice_async(self, "async", "get_all")
return {"results": all_memories_result}
async def _get_all_from_vector_store(self, filters, limit):
async def _get_all_from_vector_store(self, filters, limit, show_expired=False, output_limit=None):
memories_result = await asyncio.to_thread(self.vector_store.list, filters=filters, top_k=limit)
# Handle different vector store return formats by inspecting first element
@@ -2830,11 +2914,14 @@ class AsyncMemory(MemoryBase):
"actor_id",
"role",
"attributed_to",
"expiration_date",
]
core_and_promoted_keys = {"data", "hash", "created_at", "updated_at", "id", "text_lemmatized", "attributed_to", *promoted_payload_keys}
formatted_memories = []
for mem in actual_memories:
if not show_expired and _payload_is_expired(mem.payload):
continue
memory_item_dict = MemoryItem(
id=mem.id,
memory=mem.payload.get("data", ""),
@@ -2852,6 +2939,8 @@ class AsyncMemory(MemoryBase):
memory_item_dict["metadata"] = additional_metadata
formatted_memories.append(memory_item_dict)
if output_limit is not None and len(formatted_memories) >= output_limit:
break
return formatted_memories
@@ -2865,6 +2954,7 @@ class AsyncMemory(MemoryBase):
rerank: bool = False,
explain: bool = False,
reference_date: Optional[Any] = None,
show_expired: bool = False,
**kwargs,
):
"""
@@ -2897,6 +2987,7 @@ class AsyncMemory(MemoryBase):
rerank (bool, optional): Whether to rerank results. Defaults to False.
explain (bool, optional): Whether to include score_details for each result. Defaults to False.
reference_date (Any, optional): Platform-only temporal parameter. Not supported in OSS.
show_expired (bool, optional): Include expired memories. Defaults to False.
Returns:
dict: A dictionary containing the search results under a "results" key.
@@ -2972,7 +3063,9 @@ class AsyncMemory(MemoryBase):
)
search_start = time.perf_counter()
original_memories = await self._search_vector_store(query, effective_filters, limit, threshold, explain=explain)
original_memories = await self._search_vector_store(
query, effective_filters, limit, threshold, explain=explain, show_expired=show_expired
)
search_elapsed_seconds = time.perf_counter() - search_start
# Apply reranking if enabled and reranker is available
@@ -3107,7 +3200,7 @@ class AsyncMemory(MemoryBase):
return True
return False
async def _search_vector_store(self, query, filters, limit, threshold=0.1, explain=False):
async def _search_vector_store(self, query, filters, limit, threshold=0.1, explain=False, show_expired=False):
if threshold is None:
threshold = 0.1
@@ -3147,11 +3240,14 @@ class AsyncMemory(MemoryBase):
# Step 7: Build candidate set from semantic results
candidates = []
for mem in semantic_results:
payload = mem.payload if hasattr(mem, 'payload') else {}
if not show_expired and _payload_is_expired(payload):
continue
mem_id = str(mem.id)
candidates.append({
"id": mem_id,
"score": mem.score,
"payload": mem.payload if hasattr(mem, 'payload') else {},
"payload": payload,
})
# Step 8: Score and rank
@@ -3172,6 +3268,7 @@ class AsyncMemory(MemoryBase):
"actor_id",
"role",
"attributed_to",
"expiration_date",
]
core_and_promoted_keys = {"data", "hash", "created_at", "updated_at", "id", "text_lemmatized", "attributed_to", *promoted_payload_keys}
@@ -3280,14 +3377,21 @@ class AsyncMemory(MemoryBase):
return memory_boosts
async def update(self, memory_id, data, metadata: Optional[Dict[str, Any]] = None):
async def update(
self,
memory_id,
data: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
expiration_date: Any = _UNSET,
):
"""
Update a memory by ID asynchronously.
Args:
memory_id (str): ID of the memory to update.
data (str): New content to update the memory with.
data (str, optional): New content to update the memory with.
metadata (dict, optional): Metadata to update with the memory. Defaults to None.
expiration_date (Any, optional): Date in YYYY-MM-DD format, or None to clear it.
Returns:
dict: Success message indicating the memory was updated.
@@ -3298,10 +3402,20 @@ class AsyncMemory(MemoryBase):
"""
capture_event("mem0.update", self, {"memory_id": memory_id, "sync_type": "async"})
embeddings = await asyncio.to_thread(self.embedding_model.embed, data, "update")
existing_embeddings = {data: embeddings}
if data is None and metadata is None and expiration_date is _UNSET:
raise ValueError("At least one of data, metadata, or expiration_date must be provided.")
await self._update_memory(memory_id, data, existing_embeddings, metadata)
update_metadata = deepcopy(metadata) if metadata is not None else None
if expiration_date is not _UNSET:
update_metadata = update_metadata or {}
update_metadata["expiration_date"] = _normalize_expiration_date(expiration_date)
existing_embeddings = {}
if data is not None:
embeddings = await asyncio.to_thread(self.embedding_model.embed, data, "update")
existing_embeddings[data] = embeddings
await self._update_memory(memory_id, data, existing_embeddings, update_metadata)
await display_first_run_notice_async(self, "async", "update")
return {"message": "Memory updated successfully!"}
@@ -3499,6 +3613,11 @@ class AsyncMemory(MemoryBase):
raise ValueError(f"Memory with id {memory_id} not found. Please provide a valid 'memory_id'")
prev_value = existing_memory.payload.get("data")
if data is None:
data = prev_value
if not isinstance(data, str):
raise ValueError(f"Memory with id {memory_id} does not have text content to update")
text_changed = data != prev_value
new_metadata = deepcopy(existing_memory.payload)
if metadata is not None:
@@ -3542,8 +3661,9 @@ class AsyncMemory(MemoryBase):
# Entity-store cleanup: strip this memory's id from old-text entities,
# then re-extract entities from the new text and link them back.
session_filters = {k: new_metadata[k] for k in ("user_id", "agent_id", "run_id") if new_metadata.get(k)}
await self._remove_memory_from_entity_store(memory_id, session_filters)
await self._link_entities_for_memory(memory_id, data, session_filters)
if text_changed:
await self._remove_memory_from_entity_store(memory_id, session_filters)
await self._link_entities_for_memory(memory_id, data, session_filters)
return memory_id
+20 -5
View File
@@ -182,14 +182,16 @@ class MemoryCreate(BaseModel):
agent_id: Optional[str] = None
run_id: Optional[str] = None
metadata: Optional[Dict[str, Any]] = None
expiration_date: Optional[str] = Field(None, description="Expiration date in YYYY-MM-DD format.")
infer: Optional[bool] = Field(None, description="Whether to extract facts from messages. Defaults to True.")
memory_type: Optional[str] = Field(None, description="Type of memory to store (e.g. 'core').")
prompt: Optional[str] = Field(None, description="Custom prompt to use for fact extraction.")
class MemoryUpdate(BaseModel):
text: str = Field(..., description="New content to update the memory with.")
text: Optional[str] = Field(None, description="New content to update the memory with.")
metadata: Optional[Dict[str, Any]] = Field(None, description="Metadata to update.")
expiration_date: Optional[str] = Field(None, description="Expiration date in YYYY-MM-DD format, or null to clear.")
class SearchRequest(BaseModel):
@@ -201,6 +203,7 @@ class SearchRequest(BaseModel):
top_k: Optional[int] = Field(None, description="Maximum number of results to return.")
threshold: Optional[float] = Field(None, description="Minimum similarity score for results.")
explain: Optional[bool] = Field(None, description="Include score details for each search result.")
show_expired: Optional[bool] = Field(None, description="Include expired memories.")
class GenerateInstructionsRequest(BaseModel):
@@ -379,7 +382,7 @@ def add_memory(memory_create: MemoryCreate, _auth=Depends(verify_auth)):
ALL_MEMORIES_LIMIT = 1000
_RESERVED_PAYLOAD_KEYS = {"data", "user_id", "agent_id", "run_id", "hash", "created_at", "updated_at"}
_RESERVED_PAYLOAD_KEYS = {"data", "user_id", "agent_id", "run_id", "hash", "created_at", "updated_at", "expiration_date"}
def _serialize_memory(row: Any) -> Dict[str, Any]:
@@ -391,6 +394,7 @@ def _serialize_memory(row: Any) -> Dict[str, Any]:
"agent_id": payload.get("agent_id"),
"run_id": payload.get("run_id"),
"hash": payload.get("hash"),
"expiration_date": payload.get("expiration_date"),
"metadata": {k: v for k, v in payload.items() if k not in _RESERVED_PAYLOAD_KEYS},
"created_at": payload.get("created_at"),
"updated_at": payload.get("updated_at"),
@@ -410,6 +414,7 @@ def get_all_memories(
run_id: Optional[str] = None,
agent_id: Optional[str] = None,
top_k: Optional[int] = Query(None, ge=0, le=ALL_MEMORIES_LIMIT),
show_expired: bool = Query(False),
_auth=Depends(verify_auth),
):
"""Retrieve stored memories. Lists all memories when no identifier is provided (admin only)."""
@@ -418,6 +423,7 @@ def get_all_memories(
auth_type = getattr(request.state, "auth_type", "none")
if _auth is not None and _auth.role != "admin" and auth_type not in {"admin_api_key", "disabled"}:
raise HTTPException(status_code=403, detail="Admin role required to list all memories.")
# Admin all-memory listing is intentionally raw; scoped get_all below applies expiry visibility.
return _list_all_memories(limit=top_k if top_k is not None else ALL_MEMORIES_LIMIT)
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
@@ -425,6 +431,7 @@ def get_all_memories(
params = {"filters": filters}
if top_k is not None:
params["top_k"] = top_k
params["show_expired"] = show_expired
return get_memory_instance().get_all(**params)
except HTTPException:
raise
@@ -465,6 +472,8 @@ def search_memories(search_req: SearchRequest, _auth=Depends(verify_auth)):
params["threshold"] = search_req.threshold
if search_req.explain is not None:
params["explain"] = search_req.explain
if search_req.show_expired is not None:
params["show_expired"] = search_req.show_expired
return get_memory_instance().search(query=search_req.query, filters=filters, **params)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@@ -478,9 +487,15 @@ def search_memories(search_req: SearchRequest, _auth=Depends(verify_auth)):
def update_memory(memory_id: str, updated_memory: MemoryUpdate, _auth=Depends(verify_auth)):
"""Update an existing memory."""
try:
return get_memory_instance().update(
memory_id=memory_id, data=updated_memory.text, metadata=updated_memory.metadata
)
fields_set = getattr(updated_memory, "model_fields_set", getattr(updated_memory, "__fields_set__", set()))
params = {"memory_id": memory_id}
if "text" in fields_set:
params["data"] = updated_memory.text
if "metadata" in fields_set:
params["metadata"] = updated_memory.metadata
if "expiration_date" in fields_set:
params["expiration_date"] = updated_memory.expiration_date
return get_memory_instance().update(**params)
except (ValueError, Mem0ValidationError) as e:
raise _client_error(e)
except Exception:
+27
View File
@@ -167,6 +167,33 @@ class TestAsyncUpdate:
"test_id", "Updated memory", {"Updated memory": [0.1, 0.2, 0.3]}, {}
)
@pytest.mark.asyncio
async def test_async_update_can_change_expiration_date_without_changing_text(self, mock_async_memory, mocker):
mock_async_memory.embedding_model.embed = Mock(return_value=[0.1, 0.2, 0.3])
mock_async_memory.vector_store.get = Mock(
return_value=Mock(
payload={
"data": "Existing memory",
"user_id": "test_user",
"created_at": "2026-01-01T00:00:00+00:00",
"expiration_date": "2026-12-31",
}
)
)
mock_async_memory.vector_store.update = Mock()
mock_async_memory.db.add_history = Mock()
mock_async_memory._remove_memory_from_entity_store = mocker.AsyncMock()
mock_async_memory._link_entities_for_memory = mocker.AsyncMock()
result = await mock_async_memory.update("test_id", expiration_date="2999-01-01")
assert result["message"] == "Memory updated successfully!"
payload = mock_async_memory.vector_store.update.call_args.kwargs["payload"]
assert payload["data"] == "Existing memory"
assert payload["expiration_date"] == "2999-01-01"
mock_async_memory._remove_memory_from_entity_store.assert_not_called()
mock_async_memory._link_entities_for_memory.assert_not_called()
@pytest.mark.asyncio
class TestAsyncAddToVectorStoreErrors:
+47
View File
@@ -51,6 +51,20 @@ class TestSearchEntityParamRejection:
json={"query": "test query", "filters": {"user_id": "u1"}},
)
def test_search_passes_show_expired(self, mock_memory_client):
"""search() should pass show_expired to the API."""
mock_response = MagicMock()
mock_response.json.return_value = {"results": []}
mock_response.raise_for_status.return_value = None
mock_memory_client.client.post.return_value = mock_response
mock_memory_client.search("test query", filters={"user_id": "u1"}, show_expired=True)
mock_memory_client.client.post.assert_called_once_with(
"/v3/memories/search/",
json={"query": "test query", "filters": {"user_id": "u1"}, "show_expired": True},
)
def test_search_rejects_user_id_kwarg(self, mock_memory_client):
"""search() should reject user_id as top-level kwarg."""
with pytest.raises(ValueError, match=r"user_id"):
@@ -100,6 +114,39 @@ class TestGetAllEntityParamRejection:
with pytest.raises(ValueError, match=r"run_id"):
mock_memory_client.get_all(run_id="r1")
def test_get_all_passes_show_expired(self, mock_memory_client):
"""get_all() should pass show_expired to the API."""
mock_response = MagicMock()
mock_response.json.return_value = {"results": []}
mock_response.raise_for_status.return_value = None
mock_memory_client.client.post.return_value = mock_response
mock_memory_client.get_all(filters={"user_id": "u1"}, show_expired=True)
mock_memory_client.client.post.assert_called_once_with(
"/v3/memories/",
json={"filters": {"user_id": "u1"}, "show_expired": True},
)
class TestUpdateExpirationDate:
"""Tests for update expiration_date payload handling."""
def test_update_preserves_null_expiration_date(self, mock_memory_client):
"""update() should send expiration_date=None so the API can clear it."""
mock_response = MagicMock()
mock_response.json.return_value = {"id": "mem_1", "expiration_date": None}
mock_response.raise_for_status.return_value = None
mock_memory_client.client.put.return_value = mock_response
mock_memory_client.update("mem_1", expiration_date=None)
mock_memory_client.client.put.assert_called_once_with(
"/v1/memories/mem_1/",
json={"expiration_date": None},
params={},
)
class TestFilterOperatorPassthrough:
"""Tests that AND/OR/NOT filter operators are passed through to the API."""
+111 -1
View File
@@ -65,6 +65,24 @@ def test_add(memory_instance):
)
def test_add_stores_expiration_date(memory_instance):
memory_instance._add_to_vector_store = Mock(return_value=[{"memory": "Test memory", "event": "ADD"}])
memory_instance.add(
messages=[{"role": "user", "content": "Test message"}],
user_id="test_user",
expiration_date="2999-01-01",
)
memory_instance._add_to_vector_store.assert_called_once_with(
[{"role": "user", "content": "Test message"}],
{"user_id": "test_user", "expiration_date": "2999-01-01"},
{"user_id": "test_user"},
True,
prompt=None,
)
def test_get(memory_instance):
mock_memory = Mock(
id="test_id",
@@ -117,6 +135,39 @@ def test_search(memory_instance):
)
def test_search_hides_expired_memories_by_default(memory_instance):
mock_memories = [
Mock(id="1", payload={"data": "Expired memory", "user_id": "test_user", "expiration_date": "2000-01-01"}, score=0.9),
Mock(id="2", payload={"data": "Active memory", "user_id": "test_user", "expiration_date": "2999-01-01"}, score=0.8),
]
memory_instance.vector_store.search = Mock(return_value=mock_memories)
memory_instance.vector_store.keyword_search = Mock(return_value=None)
memory_instance.embedding_model.embed = Mock(return_value=[0.1, 0.2, 0.3])
with patch("mem0.memory.main.lemmatize_for_bm25", return_value="test query"), \
patch("mem0.memory.main.extract_entities", return_value=[]):
result = memory_instance.search("test query", filters={"user_id": "test_user"})
assert [memory["memory"] for memory in result["results"]] == ["Active memory"]
assert result["results"][0]["expiration_date"] == "2999-01-01"
def test_search_can_show_expired_memories(memory_instance):
mock_memories = [
Mock(id="1", payload={"data": "Expired memory", "user_id": "test_user", "expiration_date": "2000-01-01"}, score=0.9),
Mock(id="2", payload={"data": "Active memory", "user_id": "test_user"}, score=0.8),
]
memory_instance.vector_store.search = Mock(return_value=mock_memories)
memory_instance.vector_store.keyword_search = Mock(return_value=None)
memory_instance.embedding_model.embed = Mock(return_value=[0.1, 0.2, 0.3])
with patch("mem0.memory.main.lemmatize_for_bm25", return_value="test query"), \
patch("mem0.memory.main.extract_entities", return_value=[]):
result = memory_instance.search("test query", filters={"user_id": "test_user"}, show_expired=True)
assert [memory["memory"] for memory in result["results"]] == ["Expired memory", "Active memory"]
def test_update(memory_instance):
memory_instance.embedding_model = Mock()
memory_instance.embedding_model.embed = Mock(return_value=[0.1, 0.2, 0.3])
@@ -161,6 +212,42 @@ def test_update_with_empty_metadata(memory_instance):
)
@pytest.mark.parametrize(
("expiration_date", "expected_expiration_date"),
[
("2999-01-01", "2999-01-01"),
(None, None),
],
)
def test_update_can_change_expiration_date_without_changing_text(
memory_instance, expiration_date, expected_expiration_date
):
memory_instance.embedding_model.embed = Mock(return_value=[0.1, 0.2, 0.3])
memory_instance.vector_store.get = Mock(
return_value=Mock(
payload={
"data": "Existing memory",
"user_id": "test_user",
"created_at": "2026-01-01T00:00:00+00:00",
"expiration_date": "2026-12-31",
}
)
)
memory_instance.vector_store.update = Mock()
memory_instance.db.add_history = Mock()
memory_instance._remove_memory_from_entity_store = Mock()
memory_instance._link_entities_for_memory = Mock()
result = memory_instance.update("test_id", expiration_date=expiration_date)
assert result["message"] == "Memory updated successfully!"
payload = memory_instance.vector_store.update.call_args.kwargs["payload"]
assert payload["data"] == "Existing memory"
assert payload["expiration_date"] == expected_expiration_date
memory_instance._remove_memory_from_entity_store.assert_not_called()
memory_instance._link_entities_for_memory.assert_not_called()
def test_delete(memory_instance):
memory_instance._delete_memory = Mock()
@@ -200,7 +287,30 @@ def test_get_all(memory_instance):
assert result["results"][0]["memory"] == "Memory 1"
assert result["results"][0]["user_id"] == "test_user"
memory_instance.vector_store.list.assert_called_once_with(filters={"user_id": "test_user"}, top_k=20)
def test_get_all_hides_expired_memories_by_default(memory_instance):
mock_memories = [
Mock(id="1", payload={"data": "Expired memory", "user_id": "test_user", "expiration_date": "2000-01-01"}),
Mock(id="2", payload={"data": "Active memory", "user_id": "test_user", "expiration_date": "2999-01-01"}),
]
memory_instance.vector_store.list = Mock(return_value=(mock_memories, None))
result = memory_instance.get_all(filters={"user_id": "test_user"})
assert [memory["memory"] for memory in result["results"]] == ["Active memory"]
assert result["results"][0]["expiration_date"] == "2999-01-01"
def test_get_all_can_show_expired_memories(memory_instance):
mock_memories = [
Mock(id="1", payload={"data": "Expired memory", "user_id": "test_user", "expiration_date": "2000-01-01"}),
Mock(id="2", payload={"data": "Active memory", "user_id": "test_user"}),
]
memory_instance.vector_store.list = Mock(return_value=(mock_memories, None))
result = memory_instance.get_all(filters={"user_id": "test_user"}, show_expired=True)
assert [memory["memory"] for memory in result["results"]] == ["Expired memory", "Active memory"]
def test_no_telemetry_vector_store_when_disabled():
+13 -5
View File
@@ -564,12 +564,20 @@ class TestUpdateMemory:
resp = client.put("/memories/mem-1", json={"text": "Likes tennis"})
assert resp.status_code == 200
_, kwargs = mock_memory.update.call_args
assert kwargs["metadata"] is None
assert "metadata" not in kwargs
def test_missing_text_returns_422(self, client):
"""text is required — omitting it should fail validation."""
resp = client.put("/memories/mem-1", json={"metadata": {"k": "v"}})
assert resp.status_code == 422
def test_expiration_date_forwarded_without_text(self, client, mock_memory):
resp = client.put("/memories/mem-1", json={"expiration_date": "2999-01-01"})
assert resp.status_code == 200
_, kwargs = mock_memory.update.call_args
assert kwargs["expiration_date"] == "2999-01-01"
assert "data" not in kwargs
def test_null_expiration_date_forwarded_for_clear(self, client, mock_memory):
resp = client.put("/memories/mem-1", json={"expiration_date": None})
assert resp.status_code == 200
_, kwargs = mock_memory.update.call_args
assert kwargs["expiration_date"] is None
def test_dict_not_passed_as_data(self, client, mock_memory):
"""Regression test for #3933: the entire dict must NOT be passed as data."""