feat(memory): expose expiration controls in client docs (#5874)
Co-authored-by: Soumil Rathi <soumilrathi@gmail.com>
This commit is contained in:
co-authored by
Soumil Rathi
parent
818c2981b7
commit
0fbbb2f525
@@ -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. |
|
| `app_id` | string | No* | Associates the memory with an app. |
|
||||||
| `metadata` | object | Optional | Custom key/value metadata (e.g., `{"topic": "preferences"}`). |
|
| `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. |
|
| `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.
|
> \* 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>
|
<Info>
|
||||||
Poll the event status via `GET /v1/event/{event_id}/`. Status will be `SUCCEEDED` or `FAILED` once processing completes.
|
Poll the event status via `GET /v1/event/{event_id}/`. Status will be `SUCCEEDED` or `FAILED` once processing completes.
|
||||||
</Info>
|
</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.
|
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:
|
The `filters` object supports complex logical operations (AND, OR, NOT) and comparison operators:
|
||||||
|
|
||||||
- `in`: Matches any of the values specified
|
- `in`: Matches any of the values specified
|
||||||
@@ -32,6 +36,7 @@ memories = client.get_all(
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
show_expired=False,
|
||||||
page=1,
|
page=1,
|
||||||
page_size=50
|
page_size=50
|
||||||
)
|
)
|
||||||
@@ -46,12 +51,14 @@ memories = client.get_all(
|
|||||||
{
|
{
|
||||||
"id": "f4cbdb08-7062-4f3e-8eb2-9f5c80dfe64c",
|
"id": "f4cbdb08-7062-4f3e-8eb2-9f5c80dfe64c",
|
||||||
"memory": "Alex is planning a trip to San Francisco from July 1st to July 10th",
|
"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",
|
"created_at": "2024-07-01T12:00:00Z",
|
||||||
"updated_at": "2024-07-01T12:00:00Z"
|
"updated_at": "2024-07-01T12:00:00Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "a2b8c3d4-5e6f-7g8h-9i0j-1k2l3m4n5o6p",
|
"id": "a2b8c3d4-5e6f-7g8h-9i0j-1k2l3m4n5o6p",
|
||||||
"memory": "Alex prefers vegetarian restaurants",
|
"memory": "Alex prefers vegetarian restaurants",
|
||||||
|
"expiration_date": null,
|
||||||
"created_at": "2024-07-05T15:30:00Z",
|
"created_at": "2024-07-05T15:30:00Z",
|
||||||
"updated_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.
|
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:
|
The `filters` object supports complex logical operations (AND, OR, NOT) and comparison operators:
|
||||||
- `in`: Matches any of the values specified
|
- `in`: Matches any of the values specified
|
||||||
- `gte`: Greater than or equal to
|
- `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
|
```python Platform API Example
|
||||||
related_memories = client.search(
|
related_memories = client.search(
|
||||||
query="What are Alice's hobbies?",
|
query="What are Alice's hobbies?",
|
||||||
|
show_expired=False,
|
||||||
filters={
|
filters={
|
||||||
"OR": [
|
"OR": [
|
||||||
{
|
{
|
||||||
@@ -54,6 +59,7 @@ related_memories = client.search(
|
|||||||
"category": "hobbies"
|
"category": "hobbies"
|
||||||
},
|
},
|
||||||
"score": 0.82,
|
"score": 0.82,
|
||||||
|
"expiration_date": null,
|
||||||
"created_at": "2024-07-26T10:29:36.630547-07:00",
|
"created_at": "2024-07-26T10:29:36.630547-07:00",
|
||||||
"updated_at": null,
|
"updated_at": null,
|
||||||
"categories": ["hobbies"]
|
"categories": ["hobbies"]
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
---
|
---
|
||||||
title: 'Update Memory'
|
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}/
|
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 |
|
| 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) |
|
| 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
|
### 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` |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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`
|
**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`
|
**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
|
**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`
|
**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)
|
# 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
|
### 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>
|
<Info>
|
||||||
For the full list of parameter changes across all SDKs, see the [OSS migration guide](/migration/oss-v2-to-v3#removed-parameters-reference).
|
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",
|
"type": "object",
|
||||||
"description": "Entity and metadata filters. Must include at least one entity ID (`user_id`, `agent_id`, `app_id`, or `run_id`).",
|
"description": "Entity and metadata filters. Must include at least one entity ID (`user_id`, `agent_id`, `app_id`, or `run_id`).",
|
||||||
"additionalProperties": true
|
"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,
|
"additionalProperties": true,
|
||||||
"description": "User-supplied metadata to attach to each extracted memory."
|
"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": {
|
"custom_instructions": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Project-level instructions that guide extraction for this call."
|
"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`).",
|
"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
|
"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": {
|
"top_k": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"minimum": 1,
|
"minimum": 1,
|
||||||
@@ -2432,6 +2448,12 @@
|
|||||||
"metadata": {
|
"metadata": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "Additional metadata associated with the memory"
|
"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"
|
"x-original-swagger-version": "2.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -281,19 +281,22 @@ export default class MemoryClient {
|
|||||||
text,
|
text,
|
||||||
metadata,
|
metadata,
|
||||||
timestamp,
|
timestamp,
|
||||||
|
expirationDate,
|
||||||
}: {
|
}: {
|
||||||
text?: string;
|
text?: string;
|
||||||
metadata?: Record<string, any>;
|
metadata?: Record<string, any>;
|
||||||
timestamp?: number | string;
|
timestamp?: number | string;
|
||||||
|
expirationDate?: string | null;
|
||||||
},
|
},
|
||||||
): Promise<Array<Memory>> {
|
): Promise<Array<Memory>> {
|
||||||
if (
|
if (
|
||||||
text === undefined &&
|
text === undefined &&
|
||||||
metadata === undefined &&
|
metadata === undefined &&
|
||||||
timestamp === undefined
|
timestamp === undefined &&
|
||||||
|
expirationDate === undefined
|
||||||
) {
|
) {
|
||||||
throw new Error(
|
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 (text !== undefined) payload.text = text;
|
||||||
if (metadata !== undefined) payload.metadata = metadata;
|
if (metadata !== undefined) payload.metadata = metadata;
|
||||||
if (timestamp !== undefined) payload.timestamp = timestamp;
|
if (timestamp !== undefined) payload.timestamp = timestamp;
|
||||||
|
if (expirationDate !== undefined) payload.expiration_date = expirationDate;
|
||||||
|
|
||||||
const payloadKeys = Object.keys(payload);
|
const payloadKeys = Object.keys(payload);
|
||||||
this._captureEvent("update", [payloadKeys]);
|
this._captureEvent("update", [payloadKeys]);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export interface AddMemoryOptions extends EntityOptions {
|
|||||||
customCategories?: custom_categories[];
|
customCategories?: custom_categories[];
|
||||||
customInstructions?: string;
|
customInstructions?: string;
|
||||||
timestamp?: number;
|
timestamp?: number;
|
||||||
|
expirationDate?: string;
|
||||||
structuredDataSchema?: Record<string, any>;
|
structuredDataSchema?: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,6 +26,7 @@ export interface SearchMemoryOptions {
|
|||||||
latestOnly?: boolean;
|
latestOnly?: boolean;
|
||||||
fields?: string[];
|
fields?: string[];
|
||||||
categories?: string[];
|
categories?: string[];
|
||||||
|
showExpired?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GetAllMemoryOptions {
|
export interface GetAllMemoryOptions {
|
||||||
@@ -35,6 +37,7 @@ export interface GetAllMemoryOptions {
|
|||||||
endDate?: string;
|
endDate?: string;
|
||||||
latestOnly?: boolean;
|
latestOnly?: boolean;
|
||||||
categories?: string[];
|
categories?: string[];
|
||||||
|
showExpired?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeleteAllMemoryOptions extends EntityOptions {}
|
export interface DeleteAllMemoryOptions extends EntityOptions {}
|
||||||
@@ -119,6 +122,7 @@ export interface Memory {
|
|||||||
memoryType?: string;
|
memoryType?: string;
|
||||||
score?: number;
|
score?: number;
|
||||||
metadata?: any | null;
|
metadata?: any | null;
|
||||||
|
expirationDate?: string | null;
|
||||||
owner?: string | null;
|
owner?: string | null;
|
||||||
agentId?: string | null;
|
agentId?: string | null;
|
||||||
appId?: string | null;
|
appId?: string | null;
|
||||||
|
|||||||
@@ -59,6 +59,21 @@ describe("MemoryClient - add()", () => {
|
|||||||
expect(getFetchBody(call!).user_id).toBe("user_1");
|
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 () => {
|
test("throws an error when given an empty messages array", async () => {
|
||||||
setupMockFetch();
|
setupMockFetch();
|
||||||
|
|
||||||
@@ -176,11 +191,26 @@ describe("MemoryClient - update()", () => {
|
|||||||
expect(body.timestamp).toBe(1710600000);
|
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 () => {
|
test("throws when no fields provided", async () => {
|
||||||
setupMockFetch();
|
setupMockFetch();
|
||||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||||
await expect(client.update("mem_123", {})).rejects.toThrow(
|
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);
|
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 () => {
|
test("passes complex OR filters through to the API body", async () => {
|
||||||
const extra = new Map<string, { status: number; body: unknown }>();
|
const extra = new Map<string, { status: number; body: unknown }>();
|
||||||
extra.set("/v3/memories/search/", {
|
extra.set("/v3/memories/search/", {
|
||||||
@@ -300,4 +318,22 @@ describe("MemoryClient - getAll() entity param rejection", () => {
|
|||||||
const call = findFetchCall(mock, "/v3/memories/", "POST");
|
const call = findFetchCall(mock, "/v3/memories/", "POST");
|
||||||
expect(getFetchBody(call!).latest_only).toBe(true);
|
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:
|
Args:
|
||||||
memory_id: The ID of the memory to update.
|
memory_id: The ID of the memory to update.
|
||||||
options: Typed options (UpdateMemoryOptions) with text, metadata,
|
options: Typed options (UpdateMemoryOptions) with text, metadata,
|
||||||
and/or timestamp fields.
|
timestamp, and/or expiration_date fields.
|
||||||
**kwargs: Alternatively pass text, metadata, timestamp as keyword args.
|
**kwargs: Alternatively pass text, metadata, timestamp, or
|
||||||
|
expiration_date as keyword args.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict[str, Any]: The response from the server.
|
Dict[str, Any]: The response from the server.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: If none of text, metadata, or timestamp are provided.
|
ValueError: If none of text, metadata, timestamp, or expiration_date are provided.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
>>> client.update("mem_123", UpdateMemoryOptions(text="Updated text"))
|
>>> client.update("mem_123", UpdateMemoryOptions(text="Updated text"))
|
||||||
>>> client.update("mem_123", 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 = {**(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:
|
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"})
|
capture_client_event("client.update", self, {"memory_id": memory_id, "sync_type": "sync"})
|
||||||
params = self._prepare_params()
|
params = self._prepare_params()
|
||||||
@@ -1260,24 +1262,26 @@ class AsyncMemoryClient:
|
|||||||
Args:
|
Args:
|
||||||
memory_id: The ID of the memory to update.
|
memory_id: The ID of the memory to update.
|
||||||
options: Typed options (UpdateMemoryOptions) with text, metadata,
|
options: Typed options (UpdateMemoryOptions) with text, metadata,
|
||||||
and/or timestamp fields.
|
timestamp, and/or expiration_date fields.
|
||||||
**kwargs: Alternatively pass text, metadata, timestamp as keyword args.
|
**kwargs: Alternatively pass text, metadata, timestamp, or
|
||||||
|
expiration_date as keyword args.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict[str, Any]: The response from the server.
|
Dict[str, Any]: The response from the server.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: If none of text, metadata, or timestamp are provided.
|
ValueError: If none of text, metadata, timestamp, or expiration_date are provided.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
>>> await client.update("mem_123", UpdateMemoryOptions(text="Updated text"))
|
>>> await client.update("mem_123", UpdateMemoryOptions(text="Updated text"))
|
||||||
>>> await client.update("mem_123", 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 = {**(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:
|
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"})
|
capture_client_event("client.update", self, {"memory_id": memory_id, "sync_type": "async"})
|
||||||
params = self._prepare_params()
|
params = self._prepare_params()
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ class AddMemoryOptions(BaseModel):
|
|||||||
)
|
)
|
||||||
custom_instructions: Optional[str] = Field(default=None, description="Custom instructions for fact extraction")
|
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")
|
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(
|
structured_data_schema: Optional[Dict[str, Any]] = Field(
|
||||||
default=None, description="Schema for structured data extraction"
|
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")
|
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")
|
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")
|
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):
|
class GetAllMemoryOptions(BaseModel):
|
||||||
@@ -71,6 +73,7 @@ class GetAllMemoryOptions(BaseModel):
|
|||||||
default=None, description="Filter memories created on or before this date (ISO 8601)"
|
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")
|
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):
|
class DeleteAllMemoryOptions(BaseModel):
|
||||||
@@ -91,6 +94,7 @@ class UpdateMemoryOptions(BaseModel):
|
|||||||
text: Optional[str] = Field(default=None, description="New text content for the memory")
|
text: Optional[str] = Field(default=None, description="New text content for the memory")
|
||||||
metadata: Optional[Dict[str, Any]] = Field(default=None, description="Updated metadata")
|
metadata: Optional[Dict[str, Any]] = Field(default=None, description="Updated metadata")
|
||||||
timestamp: Optional[Union[int, float, str]] = Field(default=None, description="Updated timestamp")
|
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):
|
class ProjectUpdateOptions(BaseModel):
|
||||||
|
|||||||
+144
-24
@@ -9,7 +9,7 @@ import time
|
|||||||
import uuid
|
import uuid
|
||||||
import warnings
|
import warnings
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from datetime import datetime, timezone
|
from datetime import date, datetime, timezone
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
@@ -378,9 +378,37 @@ def _entity_collection_name(provider: str, collection_name: str) -> str:
|
|||||||
return f"{collection_name}{separator}entities"
|
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()
|
setup_config()
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
_UNSET = object()
|
||||||
_PROJECT_UPDATE_UNSUPPORTED_ERROR = "Project updates are not supported by the OSS Memory SDK."
|
_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,
|
run_id: Optional[str] = None,
|
||||||
metadata: Optional[Dict[str, Any]] = None,
|
metadata: Optional[Dict[str, Any]] = None,
|
||||||
timestamp: Optional[Any] = None,
|
timestamp: Optional[Any] = None,
|
||||||
|
expiration_date: Optional[Any] = None,
|
||||||
infer: bool = True,
|
infer: bool = True,
|
||||||
memory_type: Optional[str] = None,
|
memory_type: Optional[str] = None,
|
||||||
prompt: 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.
|
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.
|
metadata (dict, optional): Metadata to store with the memory. Defaults to None.
|
||||||
timestamp (Any, optional): Platform-only temporal parameter. Not supported in OSS.
|
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
|
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.
|
'messages' and decide whether to add, update, or delete related memories.
|
||||||
If False, 'messages' are added as raw memories directly.
|
If False, 'messages' are added as raw memories directly.
|
||||||
@@ -737,6 +768,7 @@ class Memory(MemoryBase):
|
|||||||
if timestamp is not None:
|
if timestamp is not None:
|
||||||
raise ValueError(get_temporal_feature_error_message("sync", "add", "timestamp"))
|
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)
|
temporal_usage_notice = detect_temporal_usage_from_metadata(metadata)
|
||||||
processed_metadata, effective_filters = _build_filters_and_metadata(
|
processed_metadata, effective_filters = _build_filters_and_metadata(
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
@@ -744,6 +776,8 @@ class Memory(MemoryBase):
|
|||||||
run_id=run_id,
|
run_id=run_id,
|
||||||
input_metadata=metadata,
|
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:
|
if memory_type is not None and memory_type != MemoryType.PROCEDURAL.value:
|
||||||
raise Mem0ValidationError(
|
raise Mem0ValidationError(
|
||||||
@@ -1141,6 +1175,7 @@ class Memory(MemoryBase):
|
|||||||
"actor_id",
|
"actor_id",
|
||||||
"role",
|
"role",
|
||||||
"attributed_to",
|
"attributed_to",
|
||||||
|
"expiration_date",
|
||||||
]
|
]
|
||||||
|
|
||||||
core_and_promoted_keys = {"data", "hash", "created_at", "updated_at", "id", "text_lemmatized", "attributed_to", *promoted_payload_keys}
|
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,
|
filters: Optional[Dict[str, Any]] = None,
|
||||||
top_k: int = 20,
|
top_k: int = 20,
|
||||||
|
show_expired: bool = False,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
@@ -1179,6 +1215,7 @@ class Memory(MemoryBase):
|
|||||||
Must contain at least one of: user_id, agent_id, run_id.
|
Must contain at least one of: user_id, agent_id, run_id.
|
||||||
Example: filters={"user_id": "u1", "agent_id": "a1"}
|
Example: filters={"user_id": "u1", "agent_id": "a1"}
|
||||||
top_k (int, optional): The maximum number of memories to return. Defaults to 20.
|
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:
|
Returns:
|
||||||
dict: A dictionary containing a list of memories under the "results" key.
|
dict: A dictionary containing a list of memories under the "results" key.
|
||||||
@@ -1217,6 +1254,7 @@ class Memory(MemoryBase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
limit = top_k
|
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)
|
scale_threshold_notice = detect_scale_threshold_from_top_k(top_k)
|
||||||
|
|
||||||
keys, encoded_ids = process_telemetry_filters(effective_filters)
|
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"}
|
"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:
|
if scale_threshold_notice:
|
||||||
display_scale_threshold_notice(self, "sync", "get_all", *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")
|
display_first_run_notice(self, "sync", "get_all")
|
||||||
return {"results": all_memories_result}
|
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)
|
memories_result = self.vector_store.list(filters=filters, top_k=limit)
|
||||||
|
|
||||||
# Handle different vector store return formats by inspecting first element
|
# Handle different vector store return formats by inspecting first element
|
||||||
@@ -1255,11 +1293,14 @@ class Memory(MemoryBase):
|
|||||||
"actor_id",
|
"actor_id",
|
||||||
"role",
|
"role",
|
||||||
"attributed_to",
|
"attributed_to",
|
||||||
|
"expiration_date",
|
||||||
]
|
]
|
||||||
core_and_promoted_keys = {"data", "hash", "created_at", "updated_at", "id", "text_lemmatized", "attributed_to", *promoted_payload_keys}
|
core_and_promoted_keys = {"data", "hash", "created_at", "updated_at", "id", "text_lemmatized", "attributed_to", *promoted_payload_keys}
|
||||||
|
|
||||||
formatted_memories = []
|
formatted_memories = []
|
||||||
for mem in actual_memories:
|
for mem in actual_memories:
|
||||||
|
if not show_expired and _payload_is_expired(mem.payload):
|
||||||
|
continue
|
||||||
memory_item_dict = MemoryItem(
|
memory_item_dict = MemoryItem(
|
||||||
id=mem.id,
|
id=mem.id,
|
||||||
memory=mem.payload.get("data", ""),
|
memory=mem.payload.get("data", ""),
|
||||||
@@ -1277,6 +1318,8 @@ class Memory(MemoryBase):
|
|||||||
memory_item_dict["metadata"] = additional_metadata
|
memory_item_dict["metadata"] = additional_metadata
|
||||||
|
|
||||||
formatted_memories.append(memory_item_dict)
|
formatted_memories.append(memory_item_dict)
|
||||||
|
if output_limit is not None and len(formatted_memories) >= output_limit:
|
||||||
|
break
|
||||||
|
|
||||||
return formatted_memories
|
return formatted_memories
|
||||||
|
|
||||||
@@ -1290,6 +1333,7 @@ class Memory(MemoryBase):
|
|||||||
rerank: bool = False,
|
rerank: bool = False,
|
||||||
explain: bool = False,
|
explain: bool = False,
|
||||||
reference_date: Optional[Any] = None,
|
reference_date: Optional[Any] = None,
|
||||||
|
show_expired: bool = False,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
@@ -1322,6 +1366,7 @@ class Memory(MemoryBase):
|
|||||||
rerank (bool, optional): Whether to rerank results. Defaults to False.
|
rerank (bool, optional): Whether to rerank results. Defaults to False.
|
||||||
explain (bool, optional): Whether to include score_details for each result. 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.
|
reference_date (Any, optional): Platform-only temporal parameter. Not supported in OSS.
|
||||||
|
show_expired (bool, optional): Include expired memories. Defaults to False.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: A dictionary containing the search results under a "results" key.
|
dict: A dictionary containing the search results under a "results" key.
|
||||||
@@ -1393,7 +1438,9 @@ class Memory(MemoryBase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
search_start = time.perf_counter()
|
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
|
search_elapsed_seconds = time.perf_counter() - search_start
|
||||||
|
|
||||||
# Apply reranking if enabled and reranker is available
|
# Apply reranking if enabled and reranker is available
|
||||||
@@ -1525,7 +1572,7 @@ class Memory(MemoryBase):
|
|||||||
return True
|
return True
|
||||||
return False
|
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)
|
# Guard against None threshold (backward compat)
|
||||||
if threshold is None:
|
if threshold is None:
|
||||||
threshold = 0.1
|
threshold = 0.1
|
||||||
@@ -1566,11 +1613,14 @@ class Memory(MemoryBase):
|
|||||||
# Step 7: Build candidate set from semantic results
|
# Step 7: Build candidate set from semantic results
|
||||||
candidates = []
|
candidates = []
|
||||||
for mem in semantic_results:
|
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)
|
mem_id = str(mem.id)
|
||||||
candidates.append({
|
candidates.append({
|
||||||
"id": mem_id,
|
"id": mem_id,
|
||||||
"score": mem.score,
|
"score": mem.score,
|
||||||
"payload": mem.payload if hasattr(mem, 'payload') else {},
|
"payload": payload,
|
||||||
})
|
})
|
||||||
|
|
||||||
# Step 8: Score and rank
|
# Step 8: Score and rank
|
||||||
@@ -1591,6 +1641,7 @@ class Memory(MemoryBase):
|
|||||||
"actor_id",
|
"actor_id",
|
||||||
"role",
|
"role",
|
||||||
"attributed_to",
|
"attributed_to",
|
||||||
|
"expiration_date",
|
||||||
]
|
]
|
||||||
core_and_promoted_keys = {"data", "hash", "created_at", "updated_at", "id", "text_lemmatized", "attributed_to", *promoted_payload_keys}
|
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
|
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.
|
Update a memory by ID.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
memory_id (str): ID of the memory to update.
|
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.
|
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:
|
Returns:
|
||||||
dict: Success message indicating the memory was updated.
|
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"})
|
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")
|
display_first_run_notice(self, "sync", "update")
|
||||||
return {"message": "Memory updated successfully!"}
|
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'")
|
raise ValueError(f"Memory with id {memory_id} not found. Please provide a valid 'memory_id'")
|
||||||
|
|
||||||
prev_value = existing_memory.payload.get("data")
|
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)
|
new_metadata = deepcopy(existing_memory.payload)
|
||||||
if metadata is not None:
|
if metadata is not None:
|
||||||
@@ -1936,8 +2009,9 @@ class Memory(MemoryBase):
|
|||||||
# Entity-store cleanup: strip this memory's id from old-text entities,
|
# Entity-store cleanup: strip this memory's id from old-text entities,
|
||||||
# then re-extract entities from the new text and link them back.
|
# 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)}
|
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)
|
if text_changed:
|
||||||
self._link_entities_for_memory(memory_id, data, session_filters)
|
self._remove_memory_from_entity_store(memory_id, session_filters)
|
||||||
|
self._link_entities_for_memory(memory_id, data, session_filters)
|
||||||
|
|
||||||
return memory_id
|
return memory_id
|
||||||
|
|
||||||
@@ -2284,6 +2358,7 @@ class AsyncMemory(MemoryBase):
|
|||||||
run_id: Optional[str] = None,
|
run_id: Optional[str] = None,
|
||||||
metadata: Optional[Dict[str, Any]] = None,
|
metadata: Optional[Dict[str, Any]] = None,
|
||||||
timestamp: Optional[Any] = None,
|
timestamp: Optional[Any] = None,
|
||||||
|
expiration_date: Optional[Any] = None,
|
||||||
infer: bool = True,
|
infer: bool = True,
|
||||||
memory_type: Optional[str] = None,
|
memory_type: Optional[str] = None,
|
||||||
prompt: 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.
|
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.
|
metadata (dict, optional): Metadata to store with the memory. Defaults to None.
|
||||||
timestamp (Any, optional): Platform-only temporal parameter. Not supported in OSS.
|
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.
|
infer (bool, optional): Whether to infer the memories. Defaults to True.
|
||||||
memory_type (str, optional): Type of memory to create. Defaults to None.
|
memory_type (str, optional): Type of memory to create. Defaults to None.
|
||||||
Pass "procedural_memory" to create procedural memories.
|
Pass "procedural_memory" to create procedural memories.
|
||||||
@@ -2310,10 +2387,13 @@ class AsyncMemory(MemoryBase):
|
|||||||
if timestamp is not None:
|
if timestamp is not None:
|
||||||
raise ValueError(await get_temporal_feature_error_message_async("async", "add", "timestamp"))
|
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)
|
temporal_usage_notice = detect_temporal_usage_from_metadata(metadata)
|
||||||
processed_metadata, effective_filters = _build_filters_and_metadata(
|
processed_metadata, effective_filters = _build_filters_and_metadata(
|
||||||
user_id=user_id, agent_id=agent_id, run_id=run_id, input_metadata=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:
|
if memory_type is not None and memory_type != MemoryType.PROCEDURAL.value:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
@@ -2716,6 +2796,7 @@ class AsyncMemory(MemoryBase):
|
|||||||
"actor_id",
|
"actor_id",
|
||||||
"role",
|
"role",
|
||||||
"attributed_to",
|
"attributed_to",
|
||||||
|
"expiration_date",
|
||||||
]
|
]
|
||||||
|
|
||||||
core_and_promoted_keys = {"data", "hash", "created_at", "updated_at", "id", "text_lemmatized", "attributed_to", *promoted_payload_keys}
|
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,
|
filters: Optional[Dict[str, Any]] = None,
|
||||||
top_k: int = 20,
|
top_k: int = 20,
|
||||||
|
show_expired: bool = False,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
@@ -2754,6 +2836,7 @@ class AsyncMemory(MemoryBase):
|
|||||||
Must contain at least one of: user_id, agent_id, run_id.
|
Must contain at least one of: user_id, agent_id, run_id.
|
||||||
Example: filters={"user_id": "u1", "agent_id": "a1"}
|
Example: filters={"user_id": "u1", "agent_id": "a1"}
|
||||||
top_k (int, optional): The maximum number of memories to return. Defaults to 20.
|
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:
|
Returns:
|
||||||
dict: A dictionary containing a list of memories under the "results" key.
|
dict: A dictionary containing a list of memories under the "results" key.
|
||||||
@@ -2792,6 +2875,7 @@ class AsyncMemory(MemoryBase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
limit = top_k
|
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)
|
scale_threshold_notice = detect_scale_threshold_from_top_k(top_k)
|
||||||
|
|
||||||
keys, encoded_ids = process_telemetry_filters(effective_filters)
|
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"}
|
"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:
|
if scale_threshold_notice:
|
||||||
await display_scale_threshold_notice_async(self, "async", "get_all", *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")
|
await display_first_run_notice_async(self, "async", "get_all")
|
||||||
return {"results": all_memories_result}
|
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)
|
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
|
# Handle different vector store return formats by inspecting first element
|
||||||
@@ -2830,11 +2914,14 @@ class AsyncMemory(MemoryBase):
|
|||||||
"actor_id",
|
"actor_id",
|
||||||
"role",
|
"role",
|
||||||
"attributed_to",
|
"attributed_to",
|
||||||
|
"expiration_date",
|
||||||
]
|
]
|
||||||
core_and_promoted_keys = {"data", "hash", "created_at", "updated_at", "id", "text_lemmatized", "attributed_to", *promoted_payload_keys}
|
core_and_promoted_keys = {"data", "hash", "created_at", "updated_at", "id", "text_lemmatized", "attributed_to", *promoted_payload_keys}
|
||||||
|
|
||||||
formatted_memories = []
|
formatted_memories = []
|
||||||
for mem in actual_memories:
|
for mem in actual_memories:
|
||||||
|
if not show_expired and _payload_is_expired(mem.payload):
|
||||||
|
continue
|
||||||
memory_item_dict = MemoryItem(
|
memory_item_dict = MemoryItem(
|
||||||
id=mem.id,
|
id=mem.id,
|
||||||
memory=mem.payload.get("data", ""),
|
memory=mem.payload.get("data", ""),
|
||||||
@@ -2852,6 +2939,8 @@ class AsyncMemory(MemoryBase):
|
|||||||
memory_item_dict["metadata"] = additional_metadata
|
memory_item_dict["metadata"] = additional_metadata
|
||||||
|
|
||||||
formatted_memories.append(memory_item_dict)
|
formatted_memories.append(memory_item_dict)
|
||||||
|
if output_limit is not None and len(formatted_memories) >= output_limit:
|
||||||
|
break
|
||||||
|
|
||||||
return formatted_memories
|
return formatted_memories
|
||||||
|
|
||||||
@@ -2865,6 +2954,7 @@ class AsyncMemory(MemoryBase):
|
|||||||
rerank: bool = False,
|
rerank: bool = False,
|
||||||
explain: bool = False,
|
explain: bool = False,
|
||||||
reference_date: Optional[Any] = None,
|
reference_date: Optional[Any] = None,
|
||||||
|
show_expired: bool = False,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
@@ -2897,6 +2987,7 @@ class AsyncMemory(MemoryBase):
|
|||||||
rerank (bool, optional): Whether to rerank results. Defaults to False.
|
rerank (bool, optional): Whether to rerank results. Defaults to False.
|
||||||
explain (bool, optional): Whether to include score_details for each result. 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.
|
reference_date (Any, optional): Platform-only temporal parameter. Not supported in OSS.
|
||||||
|
show_expired (bool, optional): Include expired memories. Defaults to False.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: A dictionary containing the search results under a "results" key.
|
dict: A dictionary containing the search results under a "results" key.
|
||||||
@@ -2972,7 +3063,9 @@ class AsyncMemory(MemoryBase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
search_start = time.perf_counter()
|
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
|
search_elapsed_seconds = time.perf_counter() - search_start
|
||||||
|
|
||||||
# Apply reranking if enabled and reranker is available
|
# Apply reranking if enabled and reranker is available
|
||||||
@@ -3107,7 +3200,7 @@ class AsyncMemory(MemoryBase):
|
|||||||
return True
|
return True
|
||||||
return False
|
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:
|
if threshold is None:
|
||||||
threshold = 0.1
|
threshold = 0.1
|
||||||
|
|
||||||
@@ -3147,11 +3240,14 @@ class AsyncMemory(MemoryBase):
|
|||||||
# Step 7: Build candidate set from semantic results
|
# Step 7: Build candidate set from semantic results
|
||||||
candidates = []
|
candidates = []
|
||||||
for mem in semantic_results:
|
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)
|
mem_id = str(mem.id)
|
||||||
candidates.append({
|
candidates.append({
|
||||||
"id": mem_id,
|
"id": mem_id,
|
||||||
"score": mem.score,
|
"score": mem.score,
|
||||||
"payload": mem.payload if hasattr(mem, 'payload') else {},
|
"payload": payload,
|
||||||
})
|
})
|
||||||
|
|
||||||
# Step 8: Score and rank
|
# Step 8: Score and rank
|
||||||
@@ -3172,6 +3268,7 @@ class AsyncMemory(MemoryBase):
|
|||||||
"actor_id",
|
"actor_id",
|
||||||
"role",
|
"role",
|
||||||
"attributed_to",
|
"attributed_to",
|
||||||
|
"expiration_date",
|
||||||
]
|
]
|
||||||
core_and_promoted_keys = {"data", "hash", "created_at", "updated_at", "id", "text_lemmatized", "attributed_to", *promoted_payload_keys}
|
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
|
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.
|
Update a memory by ID asynchronously.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
memory_id (str): ID of the memory to update.
|
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.
|
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:
|
Returns:
|
||||||
dict: Success message indicating the memory was updated.
|
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"})
|
capture_event("mem0.update", self, {"memory_id": memory_id, "sync_type": "async"})
|
||||||
|
|
||||||
embeddings = await asyncio.to_thread(self.embedding_model.embed, data, "update")
|
if data is None and metadata is None and expiration_date is _UNSET:
|
||||||
existing_embeddings = {data: embeddings}
|
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")
|
await display_first_run_notice_async(self, "async", "update")
|
||||||
return {"message": "Memory updated successfully!"}
|
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'")
|
raise ValueError(f"Memory with id {memory_id} not found. Please provide a valid 'memory_id'")
|
||||||
|
|
||||||
prev_value = existing_memory.payload.get("data")
|
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)
|
new_metadata = deepcopy(existing_memory.payload)
|
||||||
if metadata is not None:
|
if metadata is not None:
|
||||||
@@ -3542,8 +3661,9 @@ class AsyncMemory(MemoryBase):
|
|||||||
# Entity-store cleanup: strip this memory's id from old-text entities,
|
# Entity-store cleanup: strip this memory's id from old-text entities,
|
||||||
# then re-extract entities from the new text and link them back.
|
# 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)}
|
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)
|
if text_changed:
|
||||||
await self._link_entities_for_memory(memory_id, data, session_filters)
|
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
|
return memory_id
|
||||||
|
|
||||||
|
|||||||
+20
-5
@@ -182,14 +182,16 @@ class MemoryCreate(BaseModel):
|
|||||||
agent_id: Optional[str] = None
|
agent_id: Optional[str] = None
|
||||||
run_id: Optional[str] = None
|
run_id: Optional[str] = None
|
||||||
metadata: Optional[Dict[str, Any]] = 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.")
|
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').")
|
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.")
|
prompt: Optional[str] = Field(None, description="Custom prompt to use for fact extraction.")
|
||||||
|
|
||||||
|
|
||||||
class MemoryUpdate(BaseModel):
|
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.")
|
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):
|
class SearchRequest(BaseModel):
|
||||||
@@ -201,6 +203,7 @@ class SearchRequest(BaseModel):
|
|||||||
top_k: Optional[int] = Field(None, description="Maximum number of results to return.")
|
top_k: Optional[int] = Field(None, description="Maximum number of results to return.")
|
||||||
threshold: Optional[float] = Field(None, description="Minimum similarity score for results.")
|
threshold: Optional[float] = Field(None, description="Minimum similarity score for results.")
|
||||||
explain: Optional[bool] = Field(None, description="Include score details for each search result.")
|
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):
|
class GenerateInstructionsRequest(BaseModel):
|
||||||
@@ -379,7 +382,7 @@ def add_memory(memory_create: MemoryCreate, _auth=Depends(verify_auth)):
|
|||||||
|
|
||||||
|
|
||||||
ALL_MEMORIES_LIMIT = 1000
|
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]:
|
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"),
|
"agent_id": payload.get("agent_id"),
|
||||||
"run_id": payload.get("run_id"),
|
"run_id": payload.get("run_id"),
|
||||||
"hash": payload.get("hash"),
|
"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},
|
"metadata": {k: v for k, v in payload.items() if k not in _RESERVED_PAYLOAD_KEYS},
|
||||||
"created_at": payload.get("created_at"),
|
"created_at": payload.get("created_at"),
|
||||||
"updated_at": payload.get("updated_at"),
|
"updated_at": payload.get("updated_at"),
|
||||||
@@ -410,6 +414,7 @@ def get_all_memories(
|
|||||||
run_id: Optional[str] = None,
|
run_id: Optional[str] = None,
|
||||||
agent_id: Optional[str] = None,
|
agent_id: Optional[str] = None,
|
||||||
top_k: Optional[int] = Query(None, ge=0, le=ALL_MEMORIES_LIMIT),
|
top_k: Optional[int] = Query(None, ge=0, le=ALL_MEMORIES_LIMIT),
|
||||||
|
show_expired: bool = Query(False),
|
||||||
_auth=Depends(verify_auth),
|
_auth=Depends(verify_auth),
|
||||||
):
|
):
|
||||||
"""Retrieve stored memories. Lists all memories when no identifier is provided (admin only)."""
|
"""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")
|
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"}:
|
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.")
|
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)
|
return _list_all_memories(limit=top_k if top_k is not None else ALL_MEMORIES_LIMIT)
|
||||||
filters = {
|
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
|
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}
|
params = {"filters": filters}
|
||||||
if top_k is not None:
|
if top_k is not None:
|
||||||
params["top_k"] = top_k
|
params["top_k"] = top_k
|
||||||
|
params["show_expired"] = show_expired
|
||||||
return get_memory_instance().get_all(**params)
|
return get_memory_instance().get_all(**params)
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
@@ -465,6 +472,8 @@ def search_memories(search_req: SearchRequest, _auth=Depends(verify_auth)):
|
|||||||
params["threshold"] = search_req.threshold
|
params["threshold"] = search_req.threshold
|
||||||
if search_req.explain is not None:
|
if search_req.explain is not None:
|
||||||
params["explain"] = search_req.explain
|
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)
|
return get_memory_instance().search(query=search_req.query, filters=filters, **params)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise HTTPException(status_code=400, detail=str(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)):
|
def update_memory(memory_id: str, updated_memory: MemoryUpdate, _auth=Depends(verify_auth)):
|
||||||
"""Update an existing memory."""
|
"""Update an existing memory."""
|
||||||
try:
|
try:
|
||||||
return get_memory_instance().update(
|
fields_set = getattr(updated_memory, "model_fields_set", getattr(updated_memory, "__fields_set__", set()))
|
||||||
memory_id=memory_id, data=updated_memory.text, metadata=updated_memory.metadata
|
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:
|
except (ValueError, Mem0ValidationError) as e:
|
||||||
raise _client_error(e)
|
raise _client_error(e)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@@ -167,6 +167,33 @@ class TestAsyncUpdate:
|
|||||||
"test_id", "Updated memory", {"Updated memory": [0.1, 0.2, 0.3]}, {}
|
"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
|
@pytest.mark.asyncio
|
||||||
class TestAsyncAddToVectorStoreErrors:
|
class TestAsyncAddToVectorStoreErrors:
|
||||||
|
|||||||
@@ -51,6 +51,20 @@ class TestSearchEntityParamRejection:
|
|||||||
json={"query": "test query", "filters": {"user_id": "u1"}},
|
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):
|
def test_search_rejects_user_id_kwarg(self, mock_memory_client):
|
||||||
"""search() should reject user_id as top-level kwarg."""
|
"""search() should reject user_id as top-level kwarg."""
|
||||||
with pytest.raises(ValueError, match=r"user_id"):
|
with pytest.raises(ValueError, match=r"user_id"):
|
||||||
@@ -100,6 +114,39 @@ class TestGetAllEntityParamRejection:
|
|||||||
with pytest.raises(ValueError, match=r"run_id"):
|
with pytest.raises(ValueError, match=r"run_id"):
|
||||||
mock_memory_client.get_all(run_id="r1")
|
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:
|
class TestFilterOperatorPassthrough:
|
||||||
"""Tests that AND/OR/NOT filter operators are passed through to the API."""
|
"""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):
|
def test_get(memory_instance):
|
||||||
mock_memory = Mock(
|
mock_memory = Mock(
|
||||||
id="test_id",
|
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):
|
def test_update(memory_instance):
|
||||||
memory_instance.embedding_model = Mock()
|
memory_instance.embedding_model = Mock()
|
||||||
memory_instance.embedding_model.embed = Mock(return_value=[0.1, 0.2, 0.3])
|
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):
|
def test_delete(memory_instance):
|
||||||
memory_instance._delete_memory = Mock()
|
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]["memory"] == "Memory 1"
|
||||||
assert result["results"][0]["user_id"] == "test_user"
|
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():
|
def test_no_telemetry_vector_store_when_disabled():
|
||||||
|
|||||||
@@ -564,12 +564,20 @@ class TestUpdateMemory:
|
|||||||
resp = client.put("/memories/mem-1", json={"text": "Likes tennis"})
|
resp = client.put("/memories/mem-1", json={"text": "Likes tennis"})
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
_, kwargs = mock_memory.update.call_args
|
_, kwargs = mock_memory.update.call_args
|
||||||
assert kwargs["metadata"] is None
|
assert "metadata" not in kwargs
|
||||||
|
|
||||||
def test_missing_text_returns_422(self, client):
|
def test_expiration_date_forwarded_without_text(self, client, mock_memory):
|
||||||
"""text is required — omitting it should fail validation."""
|
resp = client.put("/memories/mem-1", json={"expiration_date": "2999-01-01"})
|
||||||
resp = client.put("/memories/mem-1", json={"metadata": {"k": "v"}})
|
assert resp.status_code == 200
|
||||||
assert resp.status_code == 422
|
_, 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):
|
def test_dict_not_passed_as_data(self, client, mock_memory):
|
||||||
"""Regression test for #3933: the entire dict must NOT be passed as data."""
|
"""Regression test for #3933: the entire dict must NOT be passed as data."""
|
||||||
|
|||||||
Reference in New Issue
Block a user