feat(oss): accept text in Memory.update(), deprecate data (#6044)

This commit is contained in:
Kartik
2026-07-09 19:32:51 +05:30
committed by GitHub
parent 5dbf071356
commit 99206f0c64
17 changed files with 657 additions and 81 deletions
@@ -15,6 +15,7 @@ Adding memory is how Mem0 captures useful details from a conversation so your ag
- **Infer**: Controls whether Mem0 extracts structured memories (`infer=True`, default) or stores raw messages. - **Infer**: Controls whether Mem0 extracts structured memories (`infer=True`, default) or stores raw messages.
- **Metadata**: Optional filters (e.g., `{"category": "movie_recommendations"}`) that improve retrieval later. - **Metadata**: Optional filters (e.g., `{"category": "movie_recommendations"}`) that improve retrieval later.
- **User / Session identifiers**: `user_id`, `agent_id`, `app_id`, or `run_id` that scope the memory for future searches. - **User / Session identifiers**: `user_id`, `agent_id`, `app_id`, or `run_id` that scope the memory for future searches.
- **expiration_date**: Optional `YYYY-MM-DD` date after which the memory is treated as expired. Use `expirationDate` in the JavaScript SDKs. Expired memories are hidden from `search` and `get_all` unless you pass `show_expired` (`showExpired` in JavaScript); fetching by ID still returns them.
## How does it work? ## How does it work?
@@ -105,6 +106,9 @@ result = m.add(messages, user_id="alice", metadata={"category": "movie_recommend
# Optionally store raw messages without inference # Optionally store raw messages without inference
result = m.add(messages, user_id="alice", metadata={"category": "movie_recommendations"}, infer=False) result = m.add(messages, user_id="alice", metadata={"category": "movie_recommendations"}, infer=False)
# Optionally set an expiration date (YYYY-MM-DD)
result = m.add(messages, user_id="alice", expiration_date="2030-01-31")
``` ```
```javascript JavaScript ```javascript JavaScript
@@ -123,6 +127,12 @@ const result = memory.add(messages, {
userId: "alice", userId: "alice",
metadata: { category: "preferences" } metadata: { category: "preferences" }
}); });
// Optionally set an expiration date (YYYY-MM-DD)
const expiring = memory.add(messages, {
userId: "alice",
expirationDate: "2030-01-31",
});
``` ```
</CodeGroup> </CodeGroup>
@@ -12,7 +12,7 @@ Mem0’s update operation lets you fix or enrich an existing memory without dele
## Key terms ## Key terms
- **memory_id**: Unique identifier returned by `add` or `search` results. - **memory_id**: Unique identifier returned by `add` or `search` results.
- **text** / **data**: New content that replaces the stored memory value. - **text**: New content that replaces the stored memory value. In the Python OSS SDK, `data` is a deprecated alias for `text`.
- **metadata**: Optional key-value pairs you update alongside the text. - **metadata**: Optional key-value pairs you update alongside the text.
- **timestamp**: Unix epoch (int/float) or ISO 8601 string to override the memory's timestamp. - **timestamp**: Unix epoch (int/float) or ISO 8601 string to override the memory's timestamp.
- **batch_update**: Platform API that edits multiple memories in a single request. - **batch_update**: Platform API that edits multiple memories in a single request.
@@ -110,17 +110,47 @@ from mem0 import Memory
memory = Memory() memory = Memory()
# Replace the content
memory.update( memory.update(
memory_id="mem_123", memory_id="mem_123",
data="Alex now prefers decaf coffee", text="Alex now prefers decaf coffee",
)
# Update content plus metadata and an expiration date (None clears it)
memory.update(
memory_id="mem_123",
text="Alex now prefers decaf coffee",
metadata={"category": "preferences"},
expiration_date="2030-01-31",
) )
``` ```
```
```javascript JavaScript
import { Memory } from "mem0ai/oss";
const memory = new Memory();
// Replace the content
await memory.update("mem_123", { text: "Alex now prefers decaf coffee" });
// Update content plus metadata and an expiration date (null clears it)
await memory.update("mem_123", {
text: "Alex now prefers decaf coffee",
metadata: { category: "preferences" },
expirationDate: "2030-01-31",
});
// Update metadata only, leaving the stored text untouched
await memory.update("mem_123", { metadata: { category: "preferences" } });
``` ```
</CodeGroup> </CodeGroup>
<Note> <Note>
OSS JavaScript SDK does not expose `update` yet: use the REST API or Python SDK when self-hosting. In both OSS SDKs the content is optional — pass only `metadata` and/or an expiration date to update those while keeping the existing content. At least one of the three must be provided, otherwise the call raises.
</Note>
<Note>
`data` is a deprecated alias for `text` in both OSS SDKs (`data=` in Python, `{ data: ... }` in JavaScript). It still works but logs a warning; prefer `text`. In JavaScript, passing a bare string is shorthand for `{ text }`, so `update(memoryId, "new text")` also still works.
</Note> </Note>
## Tips ## Tips
@@ -138,7 +168,7 @@ memory.update(
| Capability | Mem0 Platform | Mem0 OSS | | Capability | Mem0 Platform | Mem0 OSS |
| --- | --- | --- | | --- | --- | --- |
| Update call | `client.update(memory_id, {...})` | `memory.update(memory_id, data=...)` | | Update call | `client.update(memory_id, {...})` | `memory.update(memory_id, text=...)` |
| Batch updates | `client.batch_update` (up to 1000 memories) | Script your own loop or bulk job | | Batch updates | `client.batch_update` (up to 1000 memories) | Script your own loop or bulk job |
| Dashboard visibility | Inspect updates in the UI | Inspect via logs or custom tooling | | Dashboard visibility | Inspect updates in the UI | Inspect via logs or custom tooling |
| Immutable handling | Returns descriptive error | Raises exception: delete and re-add | | Immutable handling | Returns descriptive error | Raises exception: delete and re-add |
+2 -2
View File
@@ -61,7 +61,7 @@ client.get_all(user_id="alice")
client.get(memory_id="<id>") client.get(memory_id="<id>")
# Update # Update
client.update(memory_id="<id>", data="Alice loves mountain hiking") client.update(memory_id="<id>", text="Alice loves mountain hiking")
# Delete # Delete
client.delete(memory_id="<id>") client.delete(memory_id="<id>")
@@ -118,7 +118,7 @@ m.get_all(user_id="alice")
m.get(memory_id="<id>") m.get(memory_id="<id>")
# Update # Update
m.update(memory_id="<id>", data="Alice loves mountain hiking") m.update(memory_id="<id>", text="Alice loves mountain hiking")
# Delete # Delete
m.delete(memory_id="<id>") m.delete(memory_id="<id>")
+2 -2
View File
@@ -36,7 +36,7 @@ icon: "bolt"
| Search memories | `await memory.search(...)` | Returns dict with `results`, identical shape. | | Search memories | `await memory.search(...)` | Returns dict with `results`, identical shape. |
| List memories | `await memory.get_all(...)` | Filter by `user_id`, `agent_id`, `run_id`. | | List memories | `await memory.get_all(...)` | Filter by `user_id`, `agent_id`, `run_id`. |
| Retrieve memory | `await memory.get(memory_id=...)` | Raises `ValueError` if ID is invalid. | | Retrieve memory | `await memory.get(memory_id=...)` | Raises `ValueError` if ID is invalid. |
| Update memory | `await memory.update(memory_id=..., data=...)` | Accepts partial updates. | | Update memory | `await memory.update(memory_id=..., text=...)` | Accepts partial updates. |
| Delete memory | `await memory.delete(memory_id=...)` | Returns confirmation payload. | | Delete memory | `await memory.delete(memory_id=...)` | Returns confirmation payload. |
| Delete in bulk | `await memory.delete_all(...)` | Requires at least one scope filter. | | Delete in bulk | `await memory.delete_all(...)` | Requires at least one scope filter. |
| History | `await memory.history(memory_id=...)` | Fetches change log for auditing. | | History | `await memory.history(memory_id=...)` | Fetches change log for auditing. |
@@ -185,7 +185,7 @@ specific_memory = await memory.get(memory_id="memory-id-here")
# Update a memory # Update a memory
updated_memory = await memory.update( updated_memory = await memory.update(
memory_id="memory-id-here", memory_id="memory-id-here",
data="I'm travelling to Seattle" text="I'm travelling to Seattle"
) )
# Delete a memory # Delete a memory
+1 -1
View File
@@ -129,7 +129,7 @@ matches = await memory.search(
```python ```python
await memory.update( await memory.update(
memory_id=matches["results"][0]["id"], memory_id=matches["results"][0]["id"],
data="Morgan avoids shellfish and prefers boutique hotels in central Tokyo.", text="Morgan avoids shellfish and prefers boutique hotels in central Tokyo.",
) )
``` ```
</Step> </Step>
+29 -1
View File
@@ -110,14 +110,42 @@ You only need to provide API keys - all other settings are optional.
### Methods ### Methods
- `add(messages: string | Message[], userId?: string, ...): Promise<SearchResult>` - `add(messages: string | Message[], userId?: string, ...): Promise<SearchResult>`
- Options include `metadata`, `infer`, and `expirationDate` (a `YYYY-MM-DD` date after which the memory is treated as expired).
- `search(query: string, userId?: string, ...): Promise<SearchResult>` - `search(query: string, userId?: string, ...): Promise<SearchResult>`
- Expired memories are omitted unless you pass `showExpired: true`.
- `get(memoryId: string): Promise<MemoryItem | null>` - `get(memoryId: string): Promise<MemoryItem | null>`
- `update(memoryId: string, data: string): Promise<{ message: string }>` - Fetching by ID returns the memory even if it has expired.
- `getAll(options): Promise<SearchResult>`
- Expired memories are omitted unless you pass `showExpired: true`.
- `update(memoryId: string, config: string | UpdateMemoryOptions): Promise<{ message: string }>`
- `UpdateMemoryOptions` is `{ text?, data?, metadata?, expirationDate? }`. At least one must be
provided; omitted fields are left untouched, and `expirationDate: null` clears an existing
expiry. `data` is a deprecated alias for `text`.
- Passing a bare string is shorthand for `{ text }`, so `update(memoryId, "new text")` still works.
- `delete(memoryId: string): Promise<{ message: string }>` - `delete(memoryId: string): Promise<{ message: string }>`
- `deleteAll(userId?: string, ...): Promise<{ message: string }>` - `deleteAll(userId?: string, ...): Promise<{ message: string }>`
- `history(memoryId: string): Promise<any[]>` - `history(memoryId: string): Promise<any[]>`
- `reset(): Promise<void>` - `reset(): Promise<void>`
```typescript
// Replace the content
await memory.update(memoryId, { text: "Alex now prefers decaf coffee" });
// Update metadata only, leaving the stored text untouched
await memory.update(memoryId, { metadata: { category: "preferences" } });
// Expire the memory on a given day, or clear an existing expiry
await memory.update(memoryId, { expirationDate: "2030-01-31" });
await memory.update(memoryId, { expirationDate: null });
// Include expired memories in reads
await memory.getAll({ filters: { user_id: "alice" }, showExpired: true });
await memory.search("coffee", {
filters: { user_id: "alice" },
showExpired: true,
});
```
### Try the Example ### Try the Example
We provide a comprehensive example in `examples/basic.ts` that demonstrates all the features including: We provide a comprehensive example in `examples/basic.ts` that demonstrates all the features including:
+3 -4
View File
@@ -73,10 +73,9 @@ async function runTests(memory: Memory) {
} }
// Updating this memory // Updating this memory
const result4 = await memory.update( const result4 = await memory.update(result1.results[0].id, {
result1.results[0].id, text: "I love India, it is my favorite country.",
"I love India, it is my favorite country.", });
);
console.log("Updated memory:", result4); console.log("Updated memory:", result4);
// Get all memories // Get all memories
+3 -4
View File
@@ -55,10 +55,9 @@ export async function runTests(memory: Memory) {
} }
// Updating this memory // Updating this memory
const result4 = await memory.update( const result4 = await memory.update(result1.results[0].id, {
result1.results[0].id, text: "I love India, it is my favorite country.",
"I love India, it is my favorite country.", });
);
console.log("Updated memory:", result4); console.log("Updated memory:", result4);
// Get all memories // Get all memories
+96 -26
View File
@@ -36,6 +36,7 @@ import {
SearchMemoryOptions, SearchMemoryOptions,
DeleteAllMemoryOptions, DeleteAllMemoryOptions,
GetAllMemoryOptions, GetAllMemoryOptions,
UpdateMemoryOptions,
UpdateProjectOptions, UpdateProjectOptions,
} from "./memory.types"; } from "./memory.types";
import { parse_vision_messages } from "../utils/memory"; import { parse_vision_messages } from "../utils/memory";
@@ -72,6 +73,8 @@ import {
ScoredResult, ScoredResult,
} from "../utils/scoring"; } from "../utils/scoring";
import { getDefaultVectorStoreDbPath } from "../utils/sqlite"; import { getDefaultVectorStoreDbPath } from "../utils/sqlite";
import { logger } from "../utils/logger";
import { normalizeExpirationDate, payloadIsExpired } from "../utils/expiration";
import { getOrCreateMem0UserId } from "../../../client/config"; import { getOrCreateMem0UserId } from "../../../client/config";
// Entity params that must be passed via filters - check both snake_case and camelCase // Entity params that must be passed via filters - check both snake_case and camelCase
@@ -713,6 +716,11 @@ export class Memory {
if (agentId) filters.agent_id = metadata.agent_id = agentId; if (agentId) filters.agent_id = metadata.agent_id = agentId;
if (runId) filters.run_id = metadata.run_id = runId; if (runId) filters.run_id = metadata.run_id = runId;
// Normalize expiration date into the stored metadata (round-trips via get()).
if (config.expirationDate != null) {
metadata.expiration_date = normalizeExpirationDate(config.expirationDate);
}
if (!filters.user_id && !filters.agent_id && !filters.run_id) { if (!filters.user_id && !filters.agent_id && !filters.run_id) {
throw new Error( throw new Error(
"One of the filters: userId, agentId or runId is required!", "One of the filters: userId, agentId or runId is required!",
@@ -1307,7 +1315,12 @@ export class Memory {
: {}; : {};
await this._ensureInitialized(); await this._ensureInitialized();
const { topK = 20, threshold = 0.1, explain = false } = config; const {
topK = 20,
threshold = 0.1,
explain = false,
showExpired = false,
} = config;
await this._captureEvent("search", { await this._captureEvent("search", {
query_length: query.length, query_length: query.length,
@@ -1475,11 +1488,13 @@ export class Memory {
} }
// Step 7: Build candidate set from semantic results // Step 7: Build candidate set from semantic results
const candidates = semanticResults.map((mem) => ({ const candidates = semanticResults
id: String(mem.id), .filter((mem) => showExpired || !payloadIsExpired(mem.payload))
score: mem.score ?? 0, .map((mem) => ({
payload: mem.payload || {}, id: String(mem.id),
})); score: mem.score ?? 0,
payload: mem.payload || {},
}));
// Step 8: Score and rank // Step 8: Score and rank
const scoredResults = scoreAndRank( const scoredResults = scoreAndRank(
@@ -1563,11 +1578,49 @@ export class Memory {
return result; return result;
} }
async update(memoryId: string, data: string): Promise<{ message: string }> { async update(
memoryId: string,
config: string | UpdateMemoryOptions,
): Promise<{ message: string }> {
await this._ensureInitialized(); await this._ensureInitialized();
await this._captureEvent("update", { memory_id: memoryId }); await this._captureEvent("update", { memory_id: memoryId });
const embedding = await this.embedder.embed(data);
await this.updateMemory(memoryId, data, { [data]: embedding }); const options: UpdateMemoryOptions =
typeof config === "string" ? { text: config } : config;
const { data, metadata, expirationDate } = options;
let text = options.text;
if (data != null) {
logger.warn(
"The `data` option of update() is deprecated and will be removed in " +
"the next major release. Use `text` instead.",
);
if (text == null) {
text = data;
}
}
if (text == null && metadata == null && expirationDate === undefined) {
throw new Error(
"At least one of text, metadata, or expirationDate must be provided.",
);
}
const updateMetadata: Record<string, any> = { ...metadata };
if (expirationDate !== undefined) {
updateMetadata.expiration_date =
expirationDate === null
? null
: normalizeExpirationDate(expirationDate);
}
const existingEmbeddings: Record<string, number[]> = {};
if (text != null) {
existingEmbeddings[text] = await this.embedder.embed(text);
}
await this.updateMemory(memoryId, text, existingEmbeddings, updateMetadata);
const result = { message: "Memory updated successfully!" }; const result = { message: "Memory updated successfully!" };
await this._displayFirstRunNotice("update"); await this._displayFirstRunNotice("update");
return result; return result;
@@ -1704,7 +1757,7 @@ export class Memory {
await this._ensureInitialized(); await this._ensureInitialized();
const { topK = 20 } = config; const { topK = 20, showExpired = false } = config;
// Validate and trim entity IDs in filters. Drop keys that resolve to // Validate and trim entity IDs in filters. Drop keys that resolve to
// undefined so downstream vector stores don't receive // undefined so downstream vector stores don't receive
@@ -1733,7 +1786,13 @@ export class Memory {
); );
} }
const [memories] = await this.vectorStore.list(filters, topK); // Over-fetch so expired memories dropped below still leave topK survivors.
const fetchLimit = showExpired ? topK : Math.max(topK * 4, 60);
const [memories] = await this.vectorStore.list(filters, fetchLimit);
const visibleMemories = showExpired
? memories
: memories.filter((mem) => !payloadIsExpired(mem.payload));
const excludedKeys = new Set([ const excludedKeys = new Set([
"user_id", "user_id",
@@ -1746,7 +1805,7 @@ export class Memory {
"textLemmatized", "textLemmatized",
"attributedTo", "attributedTo",
]); ]);
const results = memories.map((mem) => ({ const results = visibleMemories.slice(0, topK).map((mem) => ({
id: mem.id, id: mem.id,
memory: mem.payload.data, memory: mem.payload.data,
hash: mem.payload.hash, hash: mem.payload.hash,
@@ -1807,7 +1866,7 @@ export class Memory {
private async updateMemory( private async updateMemory(
memoryId: string, memoryId: string,
data: string, data: string | undefined,
existingEmbeddings: Record<string, number[]>, existingEmbeddings: Record<string, number[]>,
metadata: Record<string, any> = {}, metadata: Record<string, any> = {},
): Promise<string> { ): Promise<string> {
@@ -1817,15 +1876,24 @@ export class Memory {
} }
const prevValue = existingMemory.payload.data; const prevValue = existingMemory.payload.data;
// Metadata-only update: fall back to the stored text so we can re-index it.
const newData = data ?? prevValue;
if (typeof newData !== "string") {
throw new Error(
`Memory with ID ${memoryId} does not have text content to update`,
);
}
const textChanged = newData !== prevValue;
const embedding = const embedding =
existingEmbeddings[data] || (await this.embedder.embed(data)); existingEmbeddings[newData] || (await this.embedder.embed(newData));
const newMetadata = { const newMetadata = {
...existingMemory.payload, ...existingMemory.payload,
...metadata, ...metadata,
data, data: newData,
hash: createHash("md5").update(data).digest("hex"), hash: createHash("md5").update(newData).digest("hex"),
textLemmatized: lemmatizeForBm25(data), textLemmatized: lemmatizeForBm25(newData),
createdAt: existingMemory.payload.createdAt, createdAt: existingMemory.payload.createdAt,
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
}; };
@@ -1834,20 +1902,22 @@ export class Memory {
await this.db.addHistory( await this.db.addHistory(
memoryId, memoryId,
prevValue, prevValue,
data, newData,
"UPDATE", "UPDATE",
newMetadata.createdAt, newMetadata.createdAt,
newMetadata.updatedAt, newMetadata.updatedAt,
); );
// Entity-store cleanup: strip this memory's id from old-text entities, // Entity-store cleanup only when the text changed: strip this memory's id
// then re-extract entities from the new text and link them back. // from old-text entities, then re-extract from the new text and link back.
try { if (textChanged) {
const sessionFilters = this._sessionFiltersFromPayload(newMetadata); try {
await this._removeMemoryFromEntityStore(memoryId, sessionFilters); const sessionFilters = this._sessionFiltersFromPayload(newMetadata);
await this._linkEntitiesForMemory(memoryId, data, sessionFilters); await this._removeMemoryFromEntityStore(memoryId, sessionFilters);
} catch (e) { await this._linkEntitiesForMemory(memoryId, newData, sessionFilters);
console.warn(`Entity store cleanup/link failed during update: ${e}`); } catch (e) {
console.warn(`Entity store cleanup/link failed during update: ${e}`);
}
} }
return memoryId; return memoryId;
@@ -12,6 +12,22 @@ export interface AddMemoryOptions extends Entity {
filters?: SearchFilters; filters?: SearchFilters;
infer?: boolean; infer?: boolean;
timestamp?: number | string | Date | null; timestamp?: number | string | Date | null;
/** Date (YYYY-MM-DD) after which the memory is considered expired. */
expirationDate?: string | null;
}
export interface UpdateMemoryOptions {
/** New content to update the memory with. */
text?: string;
/**
* New content to update the memory with.
* @deprecated Use `text` instead. Will be removed in the next major release.
*/
data?: string;
/** Metadata merged into the memory's existing metadata. */
metadata?: Record<string, any>;
/** Date (YYYY-MM-DD) after which the memory expires, or `null` to clear it. */
expirationDate?: string | null;
} }
export interface SearchMemoryOptions { export interface SearchMemoryOptions {
@@ -20,11 +36,15 @@ export interface SearchMemoryOptions {
threshold?: number; threshold?: number;
explain?: boolean; explain?: boolean;
referenceDate?: number | string | Date | null; referenceDate?: number | string | Date | null;
/** Include expired memories in the results. Defaults to false. */
showExpired?: boolean;
} }
export interface GetAllMemoryOptions { export interface GetAllMemoryOptions {
topK?: number; topK?: number;
filters?: SearchFilters; filters?: SearchFilters;
/** Include expired memories in the results. Defaults to false. */
showExpired?: boolean;
} }
export interface DeleteAllMemoryOptions extends Entity {} export interface DeleteAllMemoryOptions extends Entity {}
+52
View File
@@ -0,0 +1,52 @@
/**
* Expiration date handling for memories.
*
* Memories may carry an `expiration_date` (YYYY-MM-DD) after which they are
* hidden from `getAll()` and `search()` unless `showExpired` is set.
*/
const EXPIRATION_DATE_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
function todayUtc(): string {
return new Date().toISOString().slice(0, 10);
}
/**
* Normalize a user-supplied expiration date to a YYYY-MM-DD string.
*
* Deliberately stricter than `new Date(value)`, which accepts formats the
* Python SDK rejects ("12/31/2099", "2099") and resolves them against the
* local timezone, shifting the calendar day. It also silently rolls invalid
* dates over — `new Date("2099-02-30T00:00:00Z")` yields March 2nd.
*/
export function normalizeExpirationDate(value: string): string {
const match = EXPIRATION_DATE_PATTERN.exec(value);
if (match) {
const [, year, month, day] = match;
const parsed = new Date(`${value}T00:00:00Z`);
if (
!Number.isNaN(parsed.getTime()) &&
parsed.getUTCFullYear() === Number(year) &&
parsed.getUTCMonth() === Number(month) - 1 &&
parsed.getUTCDate() === Number(day)
) {
return value;
}
}
throw new Error("expirationDate must be a valid date in YYYY-MM-DD format.");
}
/** True when the payload carries an expiration date strictly before today (UTC). */
export function payloadIsExpired(
payload: Record<string, any> | null | undefined,
) {
const raw = payload?.expiration_date;
if (!raw) return false;
try {
// YYYY-MM-DD sorts lexicographically the same way it sorts chronologically.
return normalizeExpirationDate(String(raw)) < todayUtc();
} catch {
// Unparseable stored value: treat as non-expiring rather than hiding data.
return false;
}
}
+310 -6
View File
@@ -5,6 +5,7 @@
/// <reference types="jest" /> /// <reference types="jest" />
import { Memory } from "../src/memory"; import { Memory } from "../src/memory";
import type { MemoryItem, SearchResult } from "../src/types"; import type { MemoryItem, SearchResult } from "../src/types";
import { logger } from "../src/utils/logger";
jest.setTimeout(30000); jest.setTimeout(30000);
@@ -150,7 +151,7 @@ describe("Memory - update()", () => {
infer: false, infer: false,
}); });
const id = addResult.results[0].id; const id = addResult.results[0].id;
const result = await memory.update(id, "Updated"); const result = await memory.update(id, { text: "Updated" });
expect(result.message).toBe("Memory updated successfully!"); expect(result.message).toBe("Memory updated successfully!");
}); });
@@ -160,7 +161,7 @@ describe("Memory - update()", () => {
infer: false, infer: false,
}); });
const id = addResult.results[0].id; const id = addResult.results[0].id;
await memory.update(id, "After update"); await memory.update(id, { text: "After update" });
const item: MemoryItem | null = await memory.get(id); const item: MemoryItem | null = await memory.get(id);
expect(item!.memory).toBe("After update"); expect(item!.memory).toBe("After update");
}); });
@@ -174,7 +175,7 @@ describe("Memory - update()", () => {
const before: MemoryItem | null = await memory.get(id); const before: MemoryItem | null = await memory.get(id);
const originalCreatedAt = before!.createdAt; const originalCreatedAt = before!.createdAt;
await memory.update(id, "New text"); await memory.update(id, { text: "New text" });
const after: MemoryItem | null = await memory.get(id); const after: MemoryItem | null = await memory.get(id);
expect(after!.createdAt).toBe(originalCreatedAt); expect(after!.createdAt).toBe(originalCreatedAt);
expect(after!.updatedAt).toBeDefined(); expect(after!.updatedAt).toBeDefined();
@@ -187,7 +188,7 @@ describe("Memory - update()", () => {
}); });
const id = addResult.results[0].id; const id = addResult.results[0].id;
const before: MemoryItem | null = await memory.get(id); const before: MemoryItem | null = await memory.get(id);
await memory.update(id, "Completely different text"); await memory.update(id, { text: "Completely different text" });
const after: MemoryItem | null = await memory.get(id); const after: MemoryItem | null = await memory.get(id);
expect(after!.hash).not.toBe(before!.hash); expect(after!.hash).not.toBe(before!.hash);
}); });
@@ -199,7 +200,7 @@ describe("Memory - update()", () => {
infer: false, infer: false,
}); });
const id = addResult.results[0].id; const id = addResult.results[0].id;
await memory.update(id, "Updated text"); await memory.update(id, { text: "Updated text" });
const after: MemoryItem | null = await memory.get(id); const after: MemoryItem | null = await memory.get(id);
expect(after!.memory).toBe("Updated text"); expect(after!.memory).toBe("Updated text");
expect(after!.metadata).toEqual( expect(after!.metadata).toEqual(
@@ -208,6 +209,309 @@ describe("Memory - update()", () => {
}); });
}); });
// ─── update() options: text / data / metadata / expirationDate ───
describe("Memory - update() options", () => {
let memory: Memory;
let warnSpy: jest.SpyInstance;
const userId = `update_options_${Date.now()}`;
beforeAll(async () => {
memory = createMemory();
});
beforeEach(() => {
warnSpy = jest.spyOn(logger, "warn").mockImplementation(() => {});
});
afterEach(() => {
warnSpy.mockRestore();
});
afterAll(async () => {
await memory.reset();
});
async function seed(text: string): Promise<string> {
const addResult: SearchResult = await memory.add(text, {
userId,
infer: false,
});
return addResult.results[0].id;
}
test("accepts an options object with text, without warning", async () => {
const id = await seed("Options before");
await memory.update(id, { text: "Options after" });
const after: MemoryItem | null = await memory.get(id);
expect(after!.memory).toBe("Options after");
expect(warnSpy).not.toHaveBeenCalled();
});
test("accepts a bare text string, as on main", async () => {
const id = await seed("Bare before");
await memory.update(id, "Bare after");
const after: MemoryItem | null = await memory.get(id);
expect(after!.memory).toBe("Bare after");
});
test("accepts the deprecated data alias and warns", async () => {
const id = await seed("Alias before");
await memory.update(id, { data: "Alias after" });
const after: MemoryItem | null = await memory.get(id);
expect(after!.memory).toBe("Alias after");
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("deprecated"));
});
// An empty `text` is content, so it must beat `data`. Guards against `||=`,
// which would treat "" as absent and store the `data` value instead.
test("text wins over data, even when text is empty", async () => {
const id = await seed("Both before");
await memory.update(id, { text: "", data: "From data" });
const after: MemoryItem | null = await memory.get(id);
expect(after!.memory).toBe("");
});
// Same trap on the guard: "" must not read as "no field provided".
test("an empty bare string is content, not a missing argument", async () => {
const id = await seed("Empty bare");
await memory.update(id, "");
const after: MemoryItem | null = await memory.get(id);
expect(after!.memory).toBe("");
});
test("updates metadata without touching the stored text", async () => {
const id = await seed("Metadata only");
await memory.update(id, { metadata: { category: "solo" } });
const after: MemoryItem | null = await memory.get(id);
expect(after!.memory).toBe("Metadata only");
expect(after!.metadata).toEqual(
expect.objectContaining({ category: "solo" }),
);
});
test("sets an expiration date without touching the stored text", async () => {
const id = await seed("Expiry only");
await memory.update(id, { expirationDate: "2099-12-31" });
const after: MemoryItem | null = await memory.get(id);
expect(after!.memory).toBe("Expiry only");
expect(after!.metadata).toEqual(
expect.objectContaining({ expiration_date: "2099-12-31" }),
);
});
// Python raises on `data=None` / `metadata=None`, so loose `== null` is the
// right check for both. `{}` is a real value and must not raise.
test.each([{}, { data: null }, { metadata: null }])(
"throws when %p provides nothing updatable",
async (options) => {
const id = await seed("Nothing to update");
await expect(memory.update(id, options as any)).rejects.toThrow(
"At least one of text, metadata, or expirationDate must be provided.",
);
expect(warnSpy).not.toHaveBeenCalled();
},
);
test("accepts empty metadata as an updatable field", async () => {
const id = await seed("Empty metadata");
await expect(memory.update(id, { metadata: {} })).resolves.toEqual({
message: "Memory updated successfully!",
});
});
});
// ─── expiration date parsing ─────────────────────────────
describe("Memory - expiration date parsing", () => {
let memory: Memory;
const userId = `expiry_parse_test_${Date.now()}`;
beforeAll(async () => {
memory = createMemory();
});
afterAll(async () => {
await memory.reset();
});
// `new Date(...)` accepts all of these; Python's date.fromisoformat rejects
// them. The first two die to the format regex, the last two only to the
// UTC component round-trip: they match YYYY-MM-DD but are not real days.
const rejected = [
"12/31/2099", // also shifts a day west of UTC
"2099-12-31T23:00:00",
"2099-02-30", // rolls over to 2099-03-02
"2100-02-29", // 2100 is not a leap year
];
test.each(rejected)("add() rejects %p", async (value) => {
await expect(
memory.add("Bad expiry", { userId, infer: false, expirationDate: value }),
).rejects.toThrow("YYYY-MM-DD");
});
// add() and update() share normalizeExpirationDate(); this checks the wiring.
test("update() rejects a malformed expiration date", async () => {
const addResult: SearchResult = await memory.add("Good", {
userId,
infer: false,
});
await expect(
memory.update(addResult.results[0].id, { expirationDate: "12/31/2099" }),
).rejects.toThrow("YYYY-MM-DD");
});
// "2096-02-29" is a real leap day: it must survive the component check.
test.each(["2099-12-31", "2096-02-29"])(
"stores %p verbatim",
async (value) => {
const addResult: SearchResult = await memory.add("Good expiry", {
userId,
infer: false,
expirationDate: value,
});
const item: MemoryItem | null = await memory.get(addResult.results[0].id);
expect(item!.metadata!.expiration_date).toBe(value);
},
);
});
// ─── expired memories are hidden on read ─────────────────
describe("Memory - expired memories", () => {
let memory: Memory;
const userId = `expired_test_${Date.now()}`;
const today = new Date().toISOString().slice(0, 10);
beforeAll(async () => {
memory = createMemory();
});
afterAll(async () => {
await memory.reset();
});
async function seed(text: string, expirationDate?: string): Promise<string> {
const addResult: SearchResult = await memory.add(text, {
userId,
infer: false,
...(expirationDate ? { expirationDate } : {}),
});
return addResult.results[0].id;
}
async function seedLiveAndDead(scopedUser: string): Promise<void> {
await memory.add("Live memory", { userId: scopedUser, infer: false });
await memory.add("Dead memory", {
userId: scopedUser,
infer: false,
expirationDate: "2020-01-01",
});
}
test("getAll() hides expired memories unless showExpired", async () => {
const scopedUser = `${userId}_getall`;
await seedLiveAndDead(scopedUser);
const filters = { user_id: scopedUser };
const hidden: SearchResult = await memory.getAll({ filters });
expect(hidden.results.map((r) => r.memory)).toEqual(["Live memory"]);
const shown: SearchResult = await memory.getAll({
filters,
showExpired: true,
});
expect(shown.results.map((r) => r.memory).sort()).toEqual([
"Dead memory",
"Live memory",
]);
});
test("search() hides expired memories unless showExpired", async () => {
const scopedUser = `${userId}_search`;
await seedLiveAndDead(scopedUser);
const filters = { user_id: scopedUser };
const hidden: SearchResult = await memory.search("memory", { filters });
expect(hidden.results.map((r) => r.memory)).toEqual(["Live memory"]);
const shown: SearchResult = await memory.search("memory", {
filters,
showExpired: true,
});
expect(shown.results.map((r) => r.memory).sort()).toEqual([
"Dead memory",
"Live memory",
]);
});
test("a memory expiring today is not yet expired", async () => {
const scopedUser = `${userId}_today`;
await memory.add("Expires today", {
userId: scopedUser,
infer: false,
expirationDate: today,
});
const result: SearchResult = await memory.getAll({
filters: { user_id: scopedUser },
});
expect(result.results).toHaveLength(1);
});
test("get() still returns an expired memory by ID", async () => {
const id = await seed("Fetch by id", "2020-01-01");
const item: MemoryItem | null = await memory.get(id);
expect(item).not.toBeNull();
expect(item!.memory).toBe("Fetch by id");
});
test("clearing the expiration date makes a memory visible again", async () => {
const scopedUser = `${userId}_revive`;
const addResult: SearchResult = await memory.add("Revived", {
userId: scopedUser,
infer: false,
expirationDate: "2020-01-01",
});
const id = addResult.results[0].id;
const before: SearchResult = await memory.getAll({
filters: { user_id: scopedUser },
});
expect(before.results).toHaveLength(0);
await memory.update(id, { expirationDate: null });
const after: SearchResult = await memory.getAll({
filters: { user_id: scopedUser },
});
expect(after.results.map((r) => r.memory)).toEqual(["Revived"]);
});
test("getAll() still fills topK when expired memories are present", async () => {
const scopedUser = `${userId}_topk`;
for (let i = 0; i < 3; i++) {
await memory.add(`Dead ${i}`, {
userId: scopedUser,
infer: false,
expirationDate: "2020-01-01",
});
}
for (let i = 0; i < 3; i++) {
await memory.add(`Live ${i}`, { userId: scopedUser, infer: false });
}
const result: SearchResult = await memory.getAll({
filters: { user_id: scopedUser },
topK: 3,
});
expect(result.results).toHaveLength(3);
expect(result.results.every((r) => r.memory!.startsWith("Live"))).toBe(
true,
);
});
});
// ─── delete() ──────────────────────────────────────────── // ─── delete() ────────────────────────────────────────────
describe("Memory - delete()", () => { describe("Memory - delete()", () => {
@@ -428,7 +732,7 @@ describe("Memory - history()", () => {
userId, userId,
}); });
const id = addResult.results[0].id; const id = addResult.results[0].id;
await memory.update(id, "After"); await memory.update(id, { text: "After" });
const history = await memory.history(id); const history = await memory.history(id);
expect(history.length).toBeGreaterThanOrEqual(2); expect(history.length).toBeGreaterThanOrEqual(2);
}); });
@@ -2154,12 +2154,13 @@ describe("Memory class – backward compat with all providers", () => {
}); });
it("all public methods work after initialization", async () => { it("all public methods work after initialization", async () => {
const memoryId = "3f0d5b6a-9c1e-4a2b-8d7f-1e2c3a4b5c6d";
const mockVStore = createMockVectorStore(); const mockVStore = createMockVectorStore();
mockVStore.search.mockResolvedValue([ mockVStore.search.mockResolvedValue([
{ id: "id-1", payload: { memory: "test", hash: "h" }, score: 0.9 }, { id: memoryId, payload: { memory: "test", hash: "h" }, score: 0.9 },
]); ]);
mockVStore.get.mockResolvedValue({ mockVStore.get.mockResolvedValue({
id: "id-1", id: memoryId,
payload: { payload: {
memory: "test", memory: "test",
hash: "h", hash: "h",
@@ -2170,7 +2171,7 @@ describe("Memory class – backward compat with all providers", () => {
mockVStore.list.mockResolvedValue([ mockVStore.list.mockResolvedValue([
[ [
{ {
id: "id-1", id: memoryId,
payload: { payload: {
memory: "test", memory: "test",
hash: "h", hash: "h",
@@ -2204,15 +2205,15 @@ describe("Memory class – backward compat with all providers", () => {
expect(searchResult).toBeDefined(); expect(searchResult).toBeDefined();
// get // get
const item = await mem.get("id-1"); const item = await mem.get(memoryId);
expect(item).toBeDefined(); expect(item).toBeDefined();
// update // update
const updateResult = await mem.update("id-1", "new data"); const updateResult = await mem.update(memoryId, { text: "new data" });
expect(updateResult.message).toBe("Memory updated successfully!"); expect(updateResult.message).toBe("Memory updated successfully!");
// delete // delete
const deleteResult = await mem.delete("id-1"); const deleteResult = await mem.delete(memoryId);
expect(deleteResult.message).toBe("Memory deleted successfully!"); expect(deleteResult.message).toBe("Memory deleted successfully!");
// deleteAll // deleteAll
@@ -2220,7 +2221,7 @@ describe("Memory class – backward compat with all providers", () => {
expect(deleteAllResult.message).toBe("Memories deleted successfully!"); expect(deleteAllResult.message).toBe("Memories deleted successfully!");
// history // history
const history = await mem.history("id-1"); const history = await mem.history(memoryId);
expect(Array.isArray(history)).toBe(true); expect(Array.isArray(history)).toBe(true);
}); });
+44 -22
View File
@@ -25,15 +25,12 @@ from mem0.configs.prompts import (
from mem0.exceptions import LLMError from mem0.exceptions import LLMError
from mem0.exceptions import ValidationError as Mem0ValidationError from mem0.exceptions import ValidationError as Mem0ValidationError
from mem0.memory.base import MemoryBase from mem0.memory.base import MemoryBase
from mem0.memory.setup import mem0_dir, setup_config
from mem0.memory.storage import SQLiteManager
from mem0.memory.telemetry import MEM0_TELEMETRY, capture_event
from mem0.memory.notices import ( from mem0.memory.notices import (
PERFORMANCE_SLOW_QUERY_THRESHOLD_SECONDS, PERFORMANCE_SLOW_QUERY_THRESHOLD_SECONDS,
detect_scale_threshold_from_add_result,
detect_scale_threshold_from_top_k,
detect_decay_usage_from_delete, detect_decay_usage_from_delete,
detect_decay_usage_from_delete_all, detect_decay_usage_from_delete_all,
detect_scale_threshold_from_add_result,
detect_scale_threshold_from_top_k,
detect_temporal_usage_from_metadata, detect_temporal_usage_from_metadata,
detect_temporal_usage_from_search, detect_temporal_usage_from_search,
display_decay_usage_notice, display_decay_usage_notice,
@@ -51,6 +48,9 @@ from mem0.memory.notices import (
get_temporal_feature_error_message, get_temporal_feature_error_message,
get_temporal_feature_error_message_async, get_temporal_feature_error_message_async,
) )
from mem0.memory.setup import mem0_dir, setup_config
from mem0.memory.storage import SQLiteManager
from mem0.memory.telemetry import MEM0_TELEMETRY, capture_event
from mem0.memory.utils import ( from mem0.memory.utils import (
extract_json, extract_json,
parse_messages, parse_messages,
@@ -1767,30 +1767,41 @@ class Memory(MemoryBase):
def update( def update(
self, self,
memory_id, memory_id,
data: Optional[str] = None, text: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None, metadata: Optional[Dict[str, Any]] = None,
expiration_date: Any = _UNSET, expiration_date: Any = _UNSET,
data: Optional[str] = None,
): ):
""" """
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, optional): New content to update the memory with. text (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. expiration_date (Any, optional): Date in YYYY-MM-DD format, or None to clear it.
data (str, optional): Deprecated alias for ``text``. Will be removed in the next
major release; use ``text`` instead.
Returns: Returns:
dict: Success message indicating the memory was updated. dict: Success message indicating the memory was updated.
Example: Example:
>>> m.update(memory_id="mem_123", data="Likes to play tennis on weekends") >>> m.update(memory_id="mem_123", text="Likes to play tennis on weekends")
{'message': 'Memory updated successfully!'} {'message': 'Memory updated successfully!'}
""" """
capture_event("mem0.update", self, {"memory_id": memory_id, "sync_type": "sync"}) capture_event("mem0.update", self, {"memory_id": memory_id, "sync_type": "sync"})
if data is None and metadata is None and expiration_date is _UNSET: if data is not None:
raise ValueError("At least one of data, metadata, or expiration_date must be provided.") logger.warning(
"The `data` argument to update() is deprecated and will be removed in the "
"next major release. Use `text` instead."
)
if text is None:
text = data
if text is None and metadata is None and expiration_date is _UNSET:
raise ValueError("At least one of text, metadata, or expiration_date must be provided.")
update_metadata = deepcopy(metadata) if metadata is not None else None update_metadata = deepcopy(metadata) if metadata is not None else None
if expiration_date is not _UNSET: if expiration_date is not _UNSET:
@@ -1798,10 +1809,10 @@ class Memory(MemoryBase):
update_metadata["expiration_date"] = _normalize_expiration_date(expiration_date) update_metadata["expiration_date"] = _normalize_expiration_date(expiration_date)
existing_embeddings = {} existing_embeddings = {}
if data is not None: if text is not None:
existing_embeddings[data] = self.embedding_model.embed(data, "update") existing_embeddings[text] = self.embedding_model.embed(text, "update")
self._update_memory(memory_id, data, existing_embeddings, update_metadata) self._update_memory(memory_id, text, 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!"}
@@ -3387,30 +3398,41 @@ class AsyncMemory(MemoryBase):
async def update( async def update(
self, self,
memory_id, memory_id,
data: Optional[str] = None, text: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None, metadata: Optional[Dict[str, Any]] = None,
expiration_date: Any = _UNSET, expiration_date: Any = _UNSET,
data: Optional[str] = None,
): ):
""" """
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, optional): New content to update the memory with. text (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. expiration_date (Any, optional): Date in YYYY-MM-DD format, or None to clear it.
data (str, optional): Deprecated alias for ``text``. Will be removed in the next
major release; use ``text`` instead.
Returns: Returns:
dict: Success message indicating the memory was updated. dict: Success message indicating the memory was updated.
Example: Example:
>>> await m.update(memory_id="mem_123", data="Likes to play tennis on weekends") >>> await m.update(memory_id="mem_123", text="Likes to play tennis on weekends")
{'message': 'Memory updated successfully!'} {'message': 'Memory updated successfully!'}
""" """
capture_event("mem0.update", self, {"memory_id": memory_id, "sync_type": "async"}) capture_event("mem0.update", self, {"memory_id": memory_id, "sync_type": "async"})
if data is None and metadata is None and expiration_date is _UNSET: if data is not None:
raise ValueError("At least one of data, metadata, or expiration_date must be provided.") logger.warning(
"The `data` argument to update() is deprecated and will be removed in the "
"next major release. Use `text` instead."
)
if text is None:
text = data
if text is None and metadata is None and expiration_date is _UNSET:
raise ValueError("At least one of text, metadata, or expiration_date must be provided.")
update_metadata = deepcopy(metadata) if metadata is not None else None update_metadata = deepcopy(metadata) if metadata is not None else None
if expiration_date is not _UNSET: if expiration_date is not _UNSET:
@@ -3418,11 +3440,11 @@ class AsyncMemory(MemoryBase):
update_metadata["expiration_date"] = _normalize_expiration_date(expiration_date) update_metadata["expiration_date"] = _normalize_expiration_date(expiration_date)
existing_embeddings = {} existing_embeddings = {}
if data is not None: if text is not None:
embeddings = await asyncio.to_thread(self.embedding_model.embed, data, "update") embeddings = await asyncio.to_thread(self.embedding_model.embed, text, "update")
existing_embeddings[data] = embeddings existing_embeddings[text] = embeddings
await self._update_memory(memory_id, data, existing_embeddings, update_metadata) await self._update_memory(memory_id, text, 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!"}
@@ -49,7 +49,7 @@ Notes:
| get_all | `memory.get_all(user_id="u")` or `…, filters={"user_id":"u"}` | `memory.get_all(filters={"user_id": "u"}, page=1, page_size=N)` — entity IDs in `filters`; paginated with `page`/`page_size` (**not** `top_k`) | | get_all | `memory.get_all(user_id="u")` or `…, filters={"user_id":"u"}` | `memory.get_all(filters={"user_id": "u"}, page=1, page_size=N)` — entity IDs in `filters`; paginated with `page`/`page_size` (**not** `top_k`) |
| delete_all | `memory.delete_all(user_id="u")` | `memory.delete_all(user_id="u")` — unchanged | | delete_all | `memory.delete_all(user_id="u")` | `memory.delete_all(user_id="u")` — unchanged |
| get | `memory.get(memory_id)` | `memory.get(memory_id)` | | get | `memory.get(memory_id)` | `memory.get(memory_id)` |
| update | `memory.update(memory_id, data=...)` | `memory.update(memory_id, text=...)` — confirm param name against installed sig | | update | `memory.update(memory_id, text=...)` *(`data=` is a deprecated alias)* | `memory.update(memory_id, text=...)` — unchanged |
| delete | `memory.delete(memory_id)` | `memory.delete(memory_id)` | | delete | `memory.delete(memory_id)` | `memory.delete(memory_id)` |
| reset | `memory.reset()` (wipes the local store) | **No global reset.** Use `memory.delete_all(filters=...)` scoped to the relevant entity. Flag this. | | reset | `memory.reset()` (wipes the local store) | **No global reset.** Use `memory.delete_all(filters=...)` scoped to the relevant entity. Flag this. |
+22
View File
@@ -190,6 +190,28 @@ 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_data_is_deprecated_alias_for_text(self, mock_async_memory, mocker, caplog):
mock_async_memory.embedding_model = Mock()
mock_async_memory.embedding_model.embed = Mock(return_value=[0.1, 0.2, 0.3])
mock_async_memory._update_memory = mocker.AsyncMock()
# `data=` still works but emits a deprecation warning
with caplog.at_level(logging.WARNING):
await mock_async_memory.update("test_id", data="via data")
assert any("deprecated" in record.message for record in caplog.records)
mock_async_memory._update_memory.assert_called_once_with(
"test_id", "via data", {"via data": [0.1, 0.2, 0.3]}, None
)
# `text` takes precedence when both are passed
mock_async_memory._update_memory.reset_mock()
await mock_async_memory.update("test_id", text="preferred", data="ignored")
mock_async_memory._update_memory.assert_called_once_with(
"test_id", "preferred", {"preferred": [0.1, 0.2, 0.3]}, None
)
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_async_update_can_change_expiration_date_without_changing_text(self, mock_async_memory, mocker): 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.embedding_model.embed = Mock(return_value=[0.1, 0.2, 0.3])
+19
View File
@@ -1,3 +1,4 @@
import logging
import os import os
from unittest.mock import Mock, patch from unittest.mock import Mock, patch
@@ -212,6 +213,24 @@ def test_update_with_empty_metadata(memory_instance):
) )
def test_update_data_is_deprecated_alias_for_text(memory_instance, caplog):
memory_instance.embedding_model = Mock()
memory_instance.embedding_model.embed = Mock(return_value=[0.1, 0.2, 0.3])
memory_instance._update_memory = Mock()
# `data=` still works but emits a deprecation warning
with caplog.at_level(logging.WARNING):
memory_instance.update("test_id", data="via data")
assert any("deprecated" in record.message for record in caplog.records)
memory_instance._update_memory.assert_called_once_with("test_id", "via data", {"via data": [0.1, 0.2, 0.3]}, None)
# `text` takes precedence when both are passed
memory_instance._update_memory.reset_mock()
memory_instance.update("test_id", text="preferred", data="ignored")
memory_instance._update_memory.assert_called_once_with("test_id", "preferred", {"preferred": [0.1, 0.2, 0.3]}, None)
@pytest.mark.parametrize( @pytest.mark.parametrize(
("expiration_date", "expected_expiration_date"), ("expiration_date", "expected_expiration_date"),
[ [