fix: add timestamps for DELETE operations in history (#4492)
This commit is contained in:
@@ -1320,12 +1320,19 @@ class Memory(MemoryBase):
|
||||
if existing_memory is None:
|
||||
raise ValueError(f"Memory with id {memory_id} not found")
|
||||
prev_value = existing_memory.payload.get("data", "")
|
||||
|
||||
# Preserve original created_at and record deletion time
|
||||
created_at = _normalize_iso_timestamp_to_utc(existing_memory.payload.get("created_at"))
|
||||
updated_at = datetime.now(timezone.utc).isoformat()
|
||||
|
||||
self.vector_store.delete(vector_id=memory_id)
|
||||
self.db.add_history(
|
||||
memory_id,
|
||||
prev_value,
|
||||
None,
|
||||
"DELETE",
|
||||
created_at=created_at,
|
||||
updated_at=updated_at,
|
||||
actor_id=existing_memory.payload.get("actor_id"),
|
||||
role=existing_memory.payload.get("role"),
|
||||
is_deleted=1,
|
||||
@@ -2460,6 +2467,10 @@ class AsyncMemory(MemoryBase):
|
||||
raise ValueError(f"Memory with id {memory_id} not found")
|
||||
prev_value = existing_memory.payload.get("data", "")
|
||||
|
||||
# Preserve original created_at and record deletion time
|
||||
created_at = _normalize_iso_timestamp_to_utc(existing_memory.payload.get("created_at"))
|
||||
updated_at = datetime.now(timezone.utc).isoformat()
|
||||
|
||||
await asyncio.to_thread(self.vector_store.delete, vector_id=memory_id)
|
||||
await asyncio.to_thread(
|
||||
self.db.add_history,
|
||||
@@ -2467,6 +2478,8 @@ class AsyncMemory(MemoryBase):
|
||||
prev_value,
|
||||
None,
|
||||
"DELETE",
|
||||
created_at=created_at,
|
||||
updated_at=updated_at,
|
||||
actor_id=existing_memory.payload.get("actor_id"),
|
||||
role=existing_memory.payload.get("role"),
|
||||
is_deleted=1,
|
||||
|
||||
+145
-4
@@ -1,4 +1,5 @@
|
||||
import json
|
||||
from datetime import datetime
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
@@ -10,7 +11,7 @@ from mem0.memory.utils import normalize_facts
|
||||
|
||||
class MockVectorMemory:
|
||||
"""Mock memory object for testing incomplete payloads."""
|
||||
|
||||
|
||||
def __init__(self, memory_id: str, payload: dict, score: float = 0.8):
|
||||
self.id = memory_id
|
||||
self.payload = payload
|
||||
@@ -104,7 +105,7 @@ def test_collection_name_preserved_after_reset(mock_sqlite, mock_llm_factory, mo
|
||||
|
||||
reset_calls = [call for call in mock_vector_factory.call_args_list if len(mock_vector_factory.call_args_list) > 2]
|
||||
if reset_calls:
|
||||
reset_config = reset_calls[-1][0][1]
|
||||
reset_config = reset_calls[-1][0][1]
|
||||
assert reset_config.collection_name == test_collection_name, f"Reset used wrong collection name: {reset_config.collection_name}"
|
||||
|
||||
|
||||
@@ -129,13 +130,13 @@ def test_search_handles_incomplete_payloads(mock_sqlite, mock_llm_factory, mock_
|
||||
complete_memory = MockVectorMemory("mem_2", {"data": "content", "hash": "def456"})
|
||||
|
||||
mock_vector_store.search.return_value = [incomplete_memory, complete_memory]
|
||||
|
||||
|
||||
mock_embedder = MagicMock()
|
||||
mock_embedder.embed.return_value = [0.1, 0.2, 0.3]
|
||||
memory.embedding_model = mock_embedder
|
||||
|
||||
result = memory._search_vector_store("test", {"user_id": "test"}, 10)
|
||||
|
||||
|
||||
assert len(result) == 2
|
||||
memories_by_id = {mem["id"]: mem for mem in result}
|
||||
|
||||
@@ -572,3 +573,143 @@ def test_update_infer_true_caches_embedding_on_llm_rewrite(mock_sqlite, mock_llm
|
||||
# It should NOT be called a 3rd time inside _update_memory
|
||||
assert embedder.embed.call_count == 2
|
||||
mock_vector_store.update.assert_called_once()
|
||||
|
||||
|
||||
@patch('mem0.utils.factory.EmbedderFactory.create')
|
||||
@patch('mem0.utils.factory.VectorStoreFactory.create')
|
||||
@patch('mem0.utils.factory.LlmFactory.create')
|
||||
@patch('mem0.memory.main.SQLiteManager')
|
||||
def test_delete_memory_history_has_timestamps(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
|
||||
"""
|
||||
Test that deleting a memory records created_at and updated_at in history.
|
||||
|
||||
Ensures DELETE operations preserve the original creation timestamp
|
||||
and record the deletion time for proper audit trails.
|
||||
"""
|
||||
mock_embedder_factory.return_value = MagicMock()
|
||||
mock_vector_store = MagicMock()
|
||||
mock_vector_factory.return_value = mock_vector_store
|
||||
mock_llm_factory.return_value = MagicMock()
|
||||
mock_sqlite.return_value = MagicMock()
|
||||
|
||||
from mem0.memory.main import Memory as MemoryClass
|
||||
config = MemoryConfig()
|
||||
memory = MemoryClass(config)
|
||||
|
||||
existing_memory = MagicMock()
|
||||
existing_memory.payload = {
|
||||
"data": "I like Python.",
|
||||
"created_at": "2024-01-01T00:00:00+00:00",
|
||||
"actor_id": None,
|
||||
"role": None,
|
||||
}
|
||||
mock_vector_store.get.return_value = existing_memory
|
||||
|
||||
memory.delete("mem-123")
|
||||
|
||||
call_kwargs = memory.db.add_history.call_args.kwargs
|
||||
assert call_kwargs["created_at"] == "2024-01-01T00:00:00+00:00"
|
||||
assert call_kwargs["updated_at"] is not None
|
||||
datetime.fromisoformat(call_kwargs["updated_at"]) # verify valid ISO timestamp
|
||||
|
||||
|
||||
@patch('mem0.utils.factory.EmbedderFactory.create')
|
||||
@patch('mem0.utils.factory.VectorStoreFactory.create')
|
||||
@patch('mem0.utils.factory.LlmFactory.create')
|
||||
@patch('mem0.memory.main.SQLiteManager')
|
||||
def test_delete_memory_normalizes_non_utc_created_at(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
|
||||
"""Test that non-UTC created_at timestamps are normalized to UTC on delete."""
|
||||
mock_embedder_factory.return_value = MagicMock()
|
||||
mock_vector_store = MagicMock()
|
||||
mock_vector_factory.return_value = mock_vector_store
|
||||
mock_llm_factory.return_value = MagicMock()
|
||||
mock_sqlite.return_value = MagicMock()
|
||||
|
||||
from mem0.memory.main import Memory as MemoryClass
|
||||
config = MemoryConfig()
|
||||
memory = MemoryClass(config)
|
||||
|
||||
existing_memory = MagicMock()
|
||||
existing_memory.payload = {
|
||||
"data": "I like Python.",
|
||||
"created_at": "2024-01-01T05:00:00+05:00", # UTC+5
|
||||
"actor_id": None,
|
||||
"role": None,
|
||||
}
|
||||
mock_vector_store.get.return_value = existing_memory
|
||||
|
||||
memory.delete("mem-123")
|
||||
|
||||
call_kwargs = memory.db.add_history.call_args.kwargs
|
||||
assert call_kwargs["created_at"] == "2024-01-01T00:00:00+00:00" # normalized to UTC
|
||||
|
||||
|
||||
@patch('mem0.utils.factory.EmbedderFactory.create')
|
||||
@patch('mem0.utils.factory.VectorStoreFactory.create')
|
||||
@patch('mem0.utils.factory.LlmFactory.create')
|
||||
@patch('mem0.memory.main.SQLiteManager')
|
||||
def test_delete_memory_missing_created_at(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
|
||||
"""Test that delete works when created_at is absent from the payload (pre-existing memories)."""
|
||||
mock_embedder_factory.return_value = MagicMock()
|
||||
mock_vector_store = MagicMock()
|
||||
mock_vector_factory.return_value = mock_vector_store
|
||||
mock_llm_factory.return_value = MagicMock()
|
||||
mock_sqlite.return_value = MagicMock()
|
||||
|
||||
from mem0.memory.main import Memory as MemoryClass
|
||||
config = MemoryConfig()
|
||||
memory = MemoryClass(config)
|
||||
|
||||
existing_memory = MagicMock()
|
||||
existing_memory.payload = {
|
||||
"data": "I like Python.",
|
||||
"actor_id": None,
|
||||
"role": None,
|
||||
}
|
||||
mock_vector_store.get.return_value = existing_memory
|
||||
|
||||
memory.delete("mem-123")
|
||||
|
||||
call_kwargs = memory.db.add_history.call_args.kwargs
|
||||
assert call_kwargs["created_at"] is None
|
||||
assert call_kwargs["updated_at"] is not None
|
||||
datetime.fromisoformat(call_kwargs["updated_at"]) # verify valid ISO timestamp
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch('mem0.utils.factory.EmbedderFactory.create')
|
||||
@patch('mem0.utils.factory.VectorStoreFactory.create')
|
||||
@patch('mem0.utils.factory.LlmFactory.create')
|
||||
@patch('mem0.memory.main.SQLiteManager')
|
||||
async def test_async_delete_memory_history_has_timestamps(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
|
||||
"""
|
||||
Test that async deleting a memory records created_at and updated_at in history.
|
||||
|
||||
Ensures async DELETE operations preserve the original creation timestamp
|
||||
and record the deletion time for proper audit trails.
|
||||
"""
|
||||
mock_embedder_factory.return_value = MagicMock()
|
||||
mock_vector_store = MagicMock()
|
||||
mock_vector_factory.return_value = mock_vector_store
|
||||
mock_llm_factory.return_value = MagicMock()
|
||||
mock_sqlite.return_value = MagicMock()
|
||||
|
||||
from mem0.memory.main import AsyncMemory
|
||||
config = MemoryConfig()
|
||||
memory = AsyncMemory(config)
|
||||
|
||||
existing_memory = MagicMock()
|
||||
existing_memory.payload = {
|
||||
"data": "I like Python.",
|
||||
"created_at": "2024-01-01T00:00:00+00:00",
|
||||
"actor_id": None,
|
||||
"role": None,
|
||||
}
|
||||
mock_vector_store.get.return_value = existing_memory
|
||||
|
||||
await memory.delete("mem-123")
|
||||
|
||||
call_kwargs = memory.db.add_history.call_args.kwargs
|
||||
assert call_kwargs["created_at"] == "2024-01-01T00:00:00+00:00"
|
||||
assert call_kwargs["updated_at"] is not None
|
||||
datetime.fromisoformat(call_kwargs["updated_at"]) # verify valid ISO timestamp
|
||||
|
||||
Reference in New Issue
Block a user