feat(memory): expose expiration controls in client docs (#5874)
Co-authored-by: Soumil Rathi <soumilrathi@gmail.com>
This commit is contained in:
@@ -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"]
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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`
|
||||
|
||||
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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():
|
||||
|
||||
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user