feat(memory): add OSS-to-Platform notices (#5494)
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
import pytest
|
||||
|
||||
from mem0.memory import main as memory_main
|
||||
from mem0.memory.main import AsyncMemory, Memory
|
||||
|
||||
|
||||
def test_sync_project_update_decay_true_raises_with_notice_message(monkeypatch):
|
||||
calls = []
|
||||
|
||||
def get_error(sync_type, trigger_function, trigger_parameter):
|
||||
calls.append((sync_type, trigger_function, trigger_parameter))
|
||||
return "blocked decay"
|
||||
|
||||
monkeypatch.setattr(memory_main, "get_decay_feature_error_message", get_error)
|
||||
|
||||
with pytest.raises(ValueError, match="blocked decay"):
|
||||
Memory.__new__(Memory).project.update(decay=True)
|
||||
|
||||
assert calls == [("sync", "project.update", "decay")]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_project_update_decay_true_raises_with_notice_message(monkeypatch):
|
||||
calls = []
|
||||
|
||||
async def get_error(sync_type, trigger_function, trigger_parameter):
|
||||
calls.append((sync_type, trigger_function, trigger_parameter))
|
||||
return "blocked async decay"
|
||||
|
||||
monkeypatch.setattr(memory_main, "get_decay_feature_error_message_async", get_error)
|
||||
|
||||
with pytest.raises(ValueError, match="blocked async decay"):
|
||||
await AsyncMemory.__new__(AsyncMemory).project.update(decay=True)
|
||||
|
||||
assert calls == [("async", "project.update", "decay")]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("kwargs", [{}, {"decay": False}])
|
||||
def test_sync_project_update_non_trigger_raises_plain_error_without_notice(monkeypatch, kwargs):
|
||||
monkeypatch.setattr(
|
||||
memory_main,
|
||||
"get_decay_feature_error_message",
|
||||
lambda *args: pytest.fail("decay feature notice should not run"),
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="Project updates are not supported by the OSS Memory SDK."):
|
||||
Memory.__new__(Memory).project.update(**kwargs)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("kwargs", [{}, {"decay": False}])
|
||||
async def test_async_project_update_non_trigger_raises_plain_error_without_notice(monkeypatch, kwargs):
|
||||
monkeypatch.setattr(
|
||||
memory_main,
|
||||
"get_decay_feature_error_message_async",
|
||||
lambda *args: pytest.fail("decay feature notice should not run"),
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="Project updates are not supported by the OSS Memory SDK."):
|
||||
await AsyncMemory.__new__(AsyncMemory).project.update(**kwargs)
|
||||
@@ -0,0 +1,214 @@
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from mem0.memory import main as memory_main
|
||||
from mem0.memory.main import AsyncMemory, Memory
|
||||
|
||||
|
||||
def make_sync_memory():
|
||||
memory = Memory.__new__(Memory)
|
||||
memory.vector_store = MagicMock()
|
||||
memory._delete_memory = MagicMock()
|
||||
return memory
|
||||
|
||||
|
||||
def make_async_memory():
|
||||
memory = AsyncMemory.__new__(AsyncMemory)
|
||||
memory.vector_store = MagicMock()
|
||||
memory._delete_memory = AsyncMock()
|
||||
return memory
|
||||
|
||||
|
||||
def test_sync_delete_decay_usage_runs_after_success(monkeypatch):
|
||||
memory = make_sync_memory()
|
||||
existing_memory = SimpleNamespace(id="memory-1")
|
||||
memory.vector_store.get.return_value = existing_memory
|
||||
decay_notice = MagicMock()
|
||||
first_run_notice = MagicMock()
|
||||
monkeypatch.setattr(memory_main, "capture_event", MagicMock())
|
||||
monkeypatch.setattr(
|
||||
memory_main,
|
||||
"detect_decay_usage_from_delete",
|
||||
MagicMock(return_value=("delete_count", "repeated_deletes", 5, None)),
|
||||
)
|
||||
monkeypatch.setattr(memory_main, "display_decay_usage_notice", decay_notice)
|
||||
monkeypatch.setattr(memory_main, "display_first_run_notice", first_run_notice)
|
||||
|
||||
result = Memory.delete(memory, "memory-1")
|
||||
|
||||
assert result == {"message": "Memory deleted successfully!"}
|
||||
memory._delete_memory.assert_called_once_with("memory-1", existing_memory)
|
||||
decay_notice.assert_called_once_with(
|
||||
memory,
|
||||
"sync",
|
||||
"delete",
|
||||
"delete_count",
|
||||
"repeated_deletes",
|
||||
5,
|
||||
None,
|
||||
)
|
||||
first_run_notice.assert_not_called()
|
||||
|
||||
|
||||
def test_sync_delete_below_threshold_uses_first_run_notice(monkeypatch):
|
||||
memory = make_sync_memory()
|
||||
memory.vector_store.get.return_value = SimpleNamespace(id="memory-1")
|
||||
decay_notice = MagicMock()
|
||||
first_run_notice = MagicMock()
|
||||
monkeypatch.setattr(memory_main, "capture_event", MagicMock())
|
||||
monkeypatch.setattr(memory_main, "detect_decay_usage_from_delete", MagicMock(return_value=None))
|
||||
monkeypatch.setattr(memory_main, "display_decay_usage_notice", decay_notice)
|
||||
monkeypatch.setattr(memory_main, "display_first_run_notice", first_run_notice)
|
||||
|
||||
Memory.delete(memory, "memory-1")
|
||||
|
||||
decay_notice.assert_not_called()
|
||||
first_run_notice.assert_called_once_with(memory, "sync", "delete")
|
||||
|
||||
|
||||
def test_sync_delete_failure_does_not_trigger_decay_usage_notice(monkeypatch):
|
||||
memory = make_sync_memory()
|
||||
memory.vector_store.get.return_value = None
|
||||
detect_decay = MagicMock()
|
||||
decay_notice = MagicMock()
|
||||
first_run_notice = MagicMock()
|
||||
monkeypatch.setattr(memory_main, "capture_event", MagicMock())
|
||||
monkeypatch.setattr(memory_main, "detect_decay_usage_from_delete", detect_decay)
|
||||
monkeypatch.setattr(memory_main, "display_decay_usage_notice", decay_notice)
|
||||
monkeypatch.setattr(memory_main, "display_first_run_notice", first_run_notice)
|
||||
|
||||
with pytest.raises(ValueError, match="Memory with id memory-1 not found"):
|
||||
Memory.delete(memory, "memory-1")
|
||||
|
||||
detect_decay.assert_not_called()
|
||||
decay_notice.assert_not_called()
|
||||
first_run_notice.assert_not_called()
|
||||
|
||||
|
||||
def test_sync_delete_all_decay_usage_runs_after_success(monkeypatch):
|
||||
memory = make_sync_memory()
|
||||
memories = [SimpleNamespace(id="memory-1"), SimpleNamespace(id="memory-2")]
|
||||
memory.vector_store.list.return_value = (memories, None)
|
||||
decay_notice = MagicMock()
|
||||
first_run_notice = MagicMock()
|
||||
detect_decay = MagicMock(return_value=("delete_all", "bulk_delete", None, 2))
|
||||
monkeypatch.setattr(memory_main, "capture_event", MagicMock())
|
||||
monkeypatch.setattr(memory_main, "detect_decay_usage_from_delete_all", detect_decay)
|
||||
monkeypatch.setattr(memory_main, "display_decay_usage_notice", decay_notice)
|
||||
monkeypatch.setattr(memory_main, "display_first_run_notice", first_run_notice)
|
||||
|
||||
result = Memory.delete_all(memory, user_id="u1")
|
||||
|
||||
assert result == {"message": "Memories deleted successfully!"}
|
||||
assert memory._delete_memory.call_count == 2
|
||||
detect_decay.assert_called_once_with(2)
|
||||
decay_notice.assert_called_once_with(
|
||||
memory,
|
||||
"sync",
|
||||
"delete_all",
|
||||
"delete_all",
|
||||
"bulk_delete",
|
||||
None,
|
||||
2,
|
||||
)
|
||||
first_run_notice.assert_not_called()
|
||||
|
||||
|
||||
def test_sync_delete_all_zero_deletes_uses_first_run_notice(monkeypatch):
|
||||
memory = make_sync_memory()
|
||||
memory.vector_store.list.return_value = ([], None)
|
||||
decay_notice = MagicMock()
|
||||
first_run_notice = MagicMock()
|
||||
monkeypatch.setattr(memory_main, "capture_event", MagicMock())
|
||||
monkeypatch.setattr(memory_main, "detect_decay_usage_from_delete_all", MagicMock(return_value=None))
|
||||
monkeypatch.setattr(memory_main, "display_decay_usage_notice", decay_notice)
|
||||
monkeypatch.setattr(memory_main, "display_first_run_notice", first_run_notice)
|
||||
|
||||
Memory.delete_all(memory, user_id="u1")
|
||||
|
||||
decay_notice.assert_not_called()
|
||||
first_run_notice.assert_called_once_with(memory, "sync", "delete_all")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_delete_decay_usage_runs_after_success(monkeypatch):
|
||||
memory = make_async_memory()
|
||||
existing_memory = SimpleNamespace(id="memory-1")
|
||||
memory.vector_store.get.return_value = existing_memory
|
||||
decay_notice = AsyncMock()
|
||||
first_run_notice = AsyncMock()
|
||||
monkeypatch.setattr(memory_main, "capture_event", MagicMock())
|
||||
monkeypatch.setattr(
|
||||
memory_main,
|
||||
"detect_decay_usage_from_delete",
|
||||
MagicMock(return_value=("delete_count", "repeated_deletes", 5, None)),
|
||||
)
|
||||
monkeypatch.setattr(memory_main, "display_decay_usage_notice_async", decay_notice)
|
||||
monkeypatch.setattr(memory_main, "display_first_run_notice_async", first_run_notice)
|
||||
|
||||
result = await AsyncMemory.delete(memory, "memory-1")
|
||||
|
||||
assert result == {"message": "Memory deleted successfully!"}
|
||||
memory._delete_memory.assert_awaited_once_with("memory-1", existing_memory)
|
||||
decay_notice.assert_awaited_once_with(
|
||||
memory,
|
||||
"async",
|
||||
"delete",
|
||||
"delete_count",
|
||||
"repeated_deletes",
|
||||
5,
|
||||
None,
|
||||
)
|
||||
first_run_notice.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_delete_failure_does_not_trigger_decay_usage_notice(monkeypatch):
|
||||
memory = make_async_memory()
|
||||
memory.vector_store.get.return_value = None
|
||||
detect_decay = MagicMock()
|
||||
decay_notice = AsyncMock()
|
||||
first_run_notice = AsyncMock()
|
||||
monkeypatch.setattr(memory_main, "capture_event", MagicMock())
|
||||
monkeypatch.setattr(memory_main, "detect_decay_usage_from_delete", detect_decay)
|
||||
monkeypatch.setattr(memory_main, "display_decay_usage_notice_async", decay_notice)
|
||||
monkeypatch.setattr(memory_main, "display_first_run_notice_async", first_run_notice)
|
||||
|
||||
with pytest.raises(ValueError, match="Memory with id memory-1 not found"):
|
||||
await AsyncMemory.delete(memory, "memory-1")
|
||||
|
||||
detect_decay.assert_not_called()
|
||||
decay_notice.assert_not_awaited()
|
||||
first_run_notice.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_delete_all_decay_usage_runs_after_success(monkeypatch):
|
||||
memory = make_async_memory()
|
||||
memories = [SimpleNamespace(id="memory-1"), SimpleNamespace(id="memory-2")]
|
||||
memory.vector_store.list.return_value = (memories, None)
|
||||
decay_notice = AsyncMock()
|
||||
first_run_notice = AsyncMock()
|
||||
detect_decay = MagicMock(return_value=("delete_all", "bulk_delete", None, 2))
|
||||
monkeypatch.setattr(memory_main, "capture_event", MagicMock())
|
||||
monkeypatch.setattr(memory_main, "detect_decay_usage_from_delete_all", detect_decay)
|
||||
monkeypatch.setattr(memory_main, "display_decay_usage_notice_async", decay_notice)
|
||||
monkeypatch.setattr(memory_main, "display_first_run_notice_async", first_run_notice)
|
||||
|
||||
result = await AsyncMemory.delete_all(memory, user_id="u1")
|
||||
|
||||
assert result == {"message": "Memories deleted successfully!"}
|
||||
assert memory._delete_memory.await_count == 2
|
||||
detect_decay.assert_called_once_with(2)
|
||||
decay_notice.assert_awaited_once_with(
|
||||
memory,
|
||||
"async",
|
||||
"delete_all",
|
||||
"delete_all",
|
||||
"bulk_delete",
|
||||
None,
|
||||
2,
|
||||
)
|
||||
first_run_notice.assert_not_awaited()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,445 @@
|
||||
from copy import deepcopy
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from mem0.memory import notices
|
||||
from mem0.memory import main as memory_main
|
||||
from mem0.memory.main import AsyncMemory, Memory
|
||||
|
||||
|
||||
class FakeFlags:
|
||||
def __init__(self, variant, payload):
|
||||
self.variant = variant
|
||||
self.payload = payload
|
||||
|
||||
def get_flag(self, key):
|
||||
assert key == notices.FLAG_KEY
|
||||
return self.variant
|
||||
|
||||
def get_flag_payload(self, key):
|
||||
assert key == notices.FLAG_KEY
|
||||
return self.payload
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_notice_process_state():
|
||||
notices._performance_slow_query_capacity_reached_in_process = False
|
||||
yield
|
||||
notices._performance_slow_query_capacity_reached_in_process = False
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def notice_harness(monkeypatch):
|
||||
config = {}
|
||||
telemetry = MagicMock()
|
||||
telemetry.user_id = "oss-user"
|
||||
|
||||
def write_config(updated):
|
||||
saved = deepcopy(updated)
|
||||
config.clear()
|
||||
config.update(saved)
|
||||
|
||||
monkeypatch.setattr(notices, "_load_config", lambda: config)
|
||||
monkeypatch.setattr(notices, "_write_config", write_config)
|
||||
monkeypatch.setattr(notices.telemetry_module, "MEM0_TELEMETRY", True)
|
||||
monkeypatch.setattr(notices.telemetry_module, "_get_oss_telemetry", lambda: telemetry)
|
||||
return config, telemetry
|
||||
|
||||
|
||||
def configure_flag(telemetry, variant, payload):
|
||||
flags = FakeFlags(variant, payload)
|
||||
telemetry.posthog.evaluate_flags.return_value = flags
|
||||
return flags
|
||||
|
||||
|
||||
def performance_payload(copy="Performance CTA", enabled=True, notice_type="log_line"):
|
||||
payload = {
|
||||
"notices": {
|
||||
"performance_slow_query": {
|
||||
"enabled": enabled,
|
||||
"notice_type": notice_type,
|
||||
}
|
||||
}
|
||||
}
|
||||
if copy is not None:
|
||||
payload["notices"]["performance_slow_query"]["copy"] = copy
|
||||
return payload
|
||||
|
||||
|
||||
def make_sync_memory(search_results=None):
|
||||
memory = Memory.__new__(Memory)
|
||||
memory.api_version = "v1.1"
|
||||
memory.reranker = None
|
||||
memory._search_vector_store = MagicMock(return_value=search_results or [])
|
||||
return memory
|
||||
|
||||
|
||||
def make_async_memory(search_results=None):
|
||||
memory = AsyncMemory.__new__(AsyncMemory)
|
||||
memory.api_version = "v1.1"
|
||||
memory.reranker = None
|
||||
memory._search_vector_store = AsyncMock(return_value=search_results or [])
|
||||
return memory
|
||||
|
||||
|
||||
def test_sync_slow_search_triggers_performance_notice_after_success(monkeypatch):
|
||||
results = [{"id": "m1"}, {"id": "m2"}]
|
||||
memory = make_sync_memory(search_results=results)
|
||||
performance_notice = MagicMock()
|
||||
temporal_notice = MagicMock()
|
||||
first_run_notice = MagicMock()
|
||||
monkeypatch.setattr(memory_main, "capture_event", MagicMock())
|
||||
monkeypatch.setattr(memory_main.time, "perf_counter", MagicMock(side_effect=[100.0, 102.1]))
|
||||
monkeypatch.setattr(memory_main, "display_performance_slow_query_notice", performance_notice)
|
||||
monkeypatch.setattr(memory_main, "display_temporal_usage_notice", temporal_notice)
|
||||
monkeypatch.setattr(memory_main, "display_first_run_notice", first_run_notice)
|
||||
|
||||
result = Memory.search(memory, "favorite drink", filters={"user_id": "u1"}, top_k=3)
|
||||
|
||||
assert result == {"results": results}
|
||||
memory._search_vector_store.assert_called_once()
|
||||
performance_notice.assert_called_once_with(memory, "sync", "search", pytest.approx(2.1), 3, 2)
|
||||
temporal_notice.assert_not_called()
|
||||
first_run_notice.assert_not_called()
|
||||
|
||||
|
||||
def test_sync_fast_search_uses_first_run_notice(monkeypatch):
|
||||
memory = make_sync_memory()
|
||||
performance_notice = MagicMock()
|
||||
first_run_notice = MagicMock()
|
||||
monkeypatch.setattr(memory_main, "capture_event", MagicMock())
|
||||
monkeypatch.setattr(memory_main.time, "perf_counter", MagicMock(side_effect=[100.0, 101.0]))
|
||||
monkeypatch.setattr(memory_main, "display_performance_slow_query_notice", performance_notice)
|
||||
monkeypatch.setattr(memory_main, "display_temporal_usage_notice", MagicMock())
|
||||
monkeypatch.setattr(memory_main, "display_first_run_notice", first_run_notice)
|
||||
|
||||
Memory.search(memory, "favorite drink", filters={"user_id": "u1"})
|
||||
|
||||
performance_notice.assert_not_called()
|
||||
first_run_notice.assert_called_once_with(memory, "sync", "search")
|
||||
|
||||
|
||||
def test_sync_failed_search_does_not_trigger_performance_notice(monkeypatch):
|
||||
memory = make_sync_memory()
|
||||
memory._search_vector_store.side_effect = RuntimeError("search failure")
|
||||
performance_notice = MagicMock()
|
||||
first_run_notice = MagicMock()
|
||||
monkeypatch.setattr(memory_main, "capture_event", MagicMock())
|
||||
monkeypatch.setattr(memory_main.time, "perf_counter", MagicMock(return_value=100.0))
|
||||
monkeypatch.setattr(memory_main, "display_performance_slow_query_notice", performance_notice)
|
||||
monkeypatch.setattr(memory_main, "display_temporal_usage_notice", MagicMock())
|
||||
monkeypatch.setattr(memory_main, "display_first_run_notice", first_run_notice)
|
||||
|
||||
with pytest.raises(RuntimeError, match="search failure"):
|
||||
Memory.search(memory, "favorite drink", filters={"user_id": "u1"})
|
||||
|
||||
performance_notice.assert_not_called()
|
||||
first_run_notice.assert_not_called()
|
||||
|
||||
|
||||
def test_sync_temporal_usage_takes_precedence_over_slow_search(monkeypatch):
|
||||
memory = make_sync_memory()
|
||||
performance_notice = MagicMock()
|
||||
temporal_notice = MagicMock()
|
||||
first_run_notice = MagicMock()
|
||||
monkeypatch.setattr(memory_main, "capture_event", MagicMock())
|
||||
monkeypatch.setattr(memory_main.time, "perf_counter", MagicMock(side_effect=[100.0, 102.1]))
|
||||
monkeypatch.setattr(memory_main, "display_performance_slow_query_notice", performance_notice)
|
||||
monkeypatch.setattr(memory_main, "display_temporal_usage_notice", temporal_notice)
|
||||
monkeypatch.setattr(memory_main, "display_first_run_notice", first_run_notice)
|
||||
|
||||
Memory.search(memory, "what happened last week?", filters={"user_id": "u1"})
|
||||
|
||||
temporal_notice.assert_called_once_with(memory, "sync", "search", "query", "relative_phrase")
|
||||
performance_notice.assert_not_called()
|
||||
first_run_notice.assert_not_called()
|
||||
|
||||
|
||||
def test_sync_scale_takes_precedence_over_slow_search(monkeypatch):
|
||||
memory = make_sync_memory()
|
||||
performance_notice = MagicMock()
|
||||
scale_notice = MagicMock()
|
||||
first_run_notice = MagicMock()
|
||||
monkeypatch.setattr(memory_main, "capture_event", MagicMock())
|
||||
monkeypatch.setattr(memory_main.time, "perf_counter", MagicMock(side_effect=[100.0, 102.1]))
|
||||
monkeypatch.setattr(memory_main, "display_performance_slow_query_notice", performance_notice)
|
||||
monkeypatch.setattr(memory_main, "display_scale_threshold_notice", scale_notice)
|
||||
monkeypatch.setattr(memory_main, "display_first_run_notice", first_run_notice)
|
||||
|
||||
Memory.search(memory, "favorite drink", filters={"user_id": "u1"}, top_k=50)
|
||||
|
||||
scale_notice.assert_called_once_with(
|
||||
memory,
|
||||
"sync",
|
||||
"search",
|
||||
"top_k",
|
||||
"high_top_k",
|
||||
50,
|
||||
None,
|
||||
notices.SCALE_TOP_K_THRESHOLD,
|
||||
)
|
||||
performance_notice.assert_not_called()
|
||||
first_run_notice.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_slow_search_triggers_performance_notice_after_success(monkeypatch):
|
||||
results = [{"id": "m1"}]
|
||||
memory = make_async_memory(search_results=results)
|
||||
performance_notice = AsyncMock()
|
||||
temporal_notice = AsyncMock()
|
||||
first_run_notice = AsyncMock()
|
||||
monkeypatch.setattr(memory_main, "capture_event", MagicMock())
|
||||
monkeypatch.setattr(memory_main.time, "perf_counter", MagicMock(side_effect=[100.0, 102.1]))
|
||||
monkeypatch.setattr(memory_main, "display_performance_slow_query_notice_async", performance_notice)
|
||||
monkeypatch.setattr(memory_main, "display_temporal_usage_notice_async", temporal_notice)
|
||||
monkeypatch.setattr(memory_main, "display_first_run_notice_async", first_run_notice)
|
||||
|
||||
result = await AsyncMemory.search(memory, "favorite drink", filters={"user_id": "u1"}, top_k=4)
|
||||
|
||||
assert result == {"results": results}
|
||||
memory._search_vector_store.assert_awaited_once()
|
||||
performance_notice.assert_awaited_once_with(memory, "async", "search", pytest.approx(2.1), 4, 1)
|
||||
temporal_notice.assert_not_awaited()
|
||||
first_run_notice.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_fast_search_uses_first_run_notice(monkeypatch):
|
||||
memory = make_async_memory()
|
||||
performance_notice = AsyncMock()
|
||||
first_run_notice = AsyncMock()
|
||||
monkeypatch.setattr(memory_main, "capture_event", MagicMock())
|
||||
monkeypatch.setattr(memory_main.time, "perf_counter", MagicMock(side_effect=[100.0, 101.0]))
|
||||
monkeypatch.setattr(memory_main, "display_performance_slow_query_notice_async", performance_notice)
|
||||
monkeypatch.setattr(memory_main, "display_temporal_usage_notice_async", AsyncMock())
|
||||
monkeypatch.setattr(memory_main, "display_first_run_notice_async", first_run_notice)
|
||||
|
||||
await AsyncMemory.search(memory, "favorite drink", filters={"user_id": "u1"})
|
||||
|
||||
performance_notice.assert_not_awaited()
|
||||
first_run_notice.assert_awaited_once_with(memory, "async", "search")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_failed_search_does_not_trigger_performance_notice(monkeypatch):
|
||||
memory = make_async_memory()
|
||||
memory._search_vector_store.side_effect = RuntimeError("search failure")
|
||||
performance_notice = AsyncMock()
|
||||
first_run_notice = AsyncMock()
|
||||
monkeypatch.setattr(memory_main, "capture_event", MagicMock())
|
||||
monkeypatch.setattr(memory_main.time, "perf_counter", MagicMock(return_value=100.0))
|
||||
monkeypatch.setattr(memory_main, "display_performance_slow_query_notice_async", performance_notice)
|
||||
monkeypatch.setattr(memory_main, "display_temporal_usage_notice_async", AsyncMock())
|
||||
monkeypatch.setattr(memory_main, "display_first_run_notice_async", first_run_notice)
|
||||
|
||||
with pytest.raises(RuntimeError, match="search failure"):
|
||||
await AsyncMemory.search(memory, "favorite drink", filters={"user_id": "u1"})
|
||||
|
||||
performance_notice.assert_not_awaited()
|
||||
first_run_notice.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_temporal_usage_takes_precedence_over_slow_search(monkeypatch):
|
||||
memory = make_async_memory()
|
||||
performance_notice = AsyncMock()
|
||||
temporal_notice = AsyncMock()
|
||||
first_run_notice = AsyncMock()
|
||||
monkeypatch.setattr(memory_main, "capture_event", MagicMock())
|
||||
monkeypatch.setattr(memory_main.time, "perf_counter", MagicMock(side_effect=[100.0, 102.1]))
|
||||
monkeypatch.setattr(memory_main, "display_performance_slow_query_notice_async", performance_notice)
|
||||
monkeypatch.setattr(memory_main, "display_temporal_usage_notice_async", temporal_notice)
|
||||
monkeypatch.setattr(memory_main, "display_first_run_notice_async", first_run_notice)
|
||||
|
||||
await AsyncMemory.search(memory, "what happened last week?", filters={"user_id": "u1"})
|
||||
|
||||
temporal_notice.assert_awaited_once_with(memory, "async", "search", "query", "relative_phrase")
|
||||
performance_notice.assert_not_awaited()
|
||||
first_run_notice.assert_not_awaited()
|
||||
|
||||
|
||||
def test_performance_slow_query_displayed_logs_and_captures_event(notice_harness, capsys):
|
||||
config, telemetry = notice_harness
|
||||
flags = configure_flag(telemetry, "displayed", performance_payload())
|
||||
|
||||
notices.display_performance_slow_query_notice(
|
||||
MagicMock(),
|
||||
"sync",
|
||||
"search",
|
||||
elapsed_seconds=2.345,
|
||||
top_k=20,
|
||||
result_count=7,
|
||||
)
|
||||
|
||||
assert capsys.readouterr().err == "Performance CTA\n"
|
||||
telemetry.posthog.evaluate_flags.assert_called_once_with("oss-user", flag_keys=[notices.FLAG_KEY])
|
||||
telemetry.capture_event.assert_called_once()
|
||||
event_name, props = telemetry.capture_event.call_args.args
|
||||
assert event_name == notices.NOTICE_EVENT
|
||||
assert props["notice_id"] == "performance_slow_query"
|
||||
assert props["notice_type"] == "log_line"
|
||||
assert props["variant"] == "displayed"
|
||||
assert props["displayed"] is True
|
||||
assert props["payload"] == "Performance CTA"
|
||||
assert props["bypass_reason"] is None
|
||||
assert props["disabled_reason"] is None
|
||||
assert props["notice_config_found"] is True
|
||||
assert props["sync_type"] == "sync"
|
||||
assert props["trigger_function"] == "search"
|
||||
assert props["trigger_reason"] == "slow_query"
|
||||
assert props["elapsed_ms"] == 2345
|
||||
assert props["threshold_ms"] == 2000
|
||||
assert props["top_k"] == 20
|
||||
assert props["result_count"] == 7
|
||||
assert telemetry.capture_event.call_args.kwargs["flags"] is flags
|
||||
assert len(config["notice_state"]["performance_slow_query"]["events"]) == 1
|
||||
|
||||
|
||||
def test_performance_slow_query_holdout_is_silent_but_captures_event(notice_harness, capsys):
|
||||
_, telemetry = notice_harness
|
||||
configure_flag(telemetry, "holdout", performance_payload())
|
||||
|
||||
notices.display_performance_slow_query_notice(
|
||||
MagicMock(),
|
||||
"sync",
|
||||
"search",
|
||||
elapsed_seconds=2.1,
|
||||
top_k=10,
|
||||
result_count=2,
|
||||
)
|
||||
|
||||
assert capsys.readouterr().err == ""
|
||||
props = telemetry.capture_event.call_args.args[1]
|
||||
assert props["displayed"] is False
|
||||
assert props["bypass_reason"] == "holdout"
|
||||
assert props["trigger_reason"] == "slow_query"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("payload", "expected_reason", "expected_found"),
|
||||
[
|
||||
({}, "missing_notice_config", False),
|
||||
({"notices": {}}, "missing_notice_config", False),
|
||||
({"notices": "not-an-object"}, "missing_notice_config", False),
|
||||
(performance_payload(copy=None), "missing_copy", True),
|
||||
(performance_payload(enabled=False, copy="hidden"), "payload_disabled", True),
|
||||
],
|
||||
)
|
||||
def test_performance_slow_query_bad_or_disabled_payload_is_silent_and_safe(
|
||||
notice_harness, payload, expected_reason, expected_found, capsys
|
||||
):
|
||||
_, telemetry = notice_harness
|
||||
configure_flag(telemetry, "displayed", payload)
|
||||
|
||||
notices.display_performance_slow_query_notice(
|
||||
MagicMock(),
|
||||
"sync",
|
||||
"search",
|
||||
elapsed_seconds=2.1,
|
||||
top_k=10,
|
||||
result_count=2,
|
||||
)
|
||||
|
||||
assert capsys.readouterr().err == ""
|
||||
props = telemetry.capture_event.call_args.args[1]
|
||||
assert props["displayed"] is False
|
||||
assert props["bypass_reason"] == expected_reason
|
||||
assert props["notice_config_found"] is expected_found
|
||||
|
||||
|
||||
@pytest.mark.parametrize("variant", [None, False])
|
||||
def test_performance_slow_query_blunt_flag_disable_does_not_capture_or_consume(
|
||||
notice_harness, variant, capsys
|
||||
):
|
||||
config, telemetry = notice_harness
|
||||
configure_flag(telemetry, variant, performance_payload())
|
||||
|
||||
notices.display_performance_slow_query_notice(
|
||||
MagicMock(),
|
||||
"sync",
|
||||
"search",
|
||||
elapsed_seconds=2.1,
|
||||
top_k=10,
|
||||
result_count=2,
|
||||
)
|
||||
|
||||
assert capsys.readouterr().err == ""
|
||||
telemetry.capture_event.assert_not_called()
|
||||
assert config.get("notice_state") is None
|
||||
|
||||
|
||||
def test_performance_slow_query_telemetry_disabled_does_not_touch_posthog_or_state(
|
||||
monkeypatch, capsys
|
||||
):
|
||||
load_config = MagicMock(return_value={})
|
||||
write_config = MagicMock()
|
||||
get_telemetry = MagicMock()
|
||||
|
||||
monkeypatch.setattr(notices, "_load_config", load_config)
|
||||
monkeypatch.setattr(notices, "_write_config", write_config)
|
||||
monkeypatch.setattr(notices.telemetry_module, "MEM0_TELEMETRY", False)
|
||||
monkeypatch.setattr(notices.telemetry_module, "_get_oss_telemetry", get_telemetry)
|
||||
|
||||
notices.display_performance_slow_query_notice(
|
||||
MagicMock(),
|
||||
"sync",
|
||||
"search",
|
||||
elapsed_seconds=2.1,
|
||||
top_k=10,
|
||||
result_count=2,
|
||||
)
|
||||
|
||||
load_config.assert_not_called()
|
||||
write_config.assert_not_called()
|
||||
get_telemetry.assert_not_called()
|
||||
assert capsys.readouterr().err == ""
|
||||
|
||||
|
||||
def test_performance_slow_query_cap_blocks_before_posthog_eval(notice_harness, capsys):
|
||||
config, telemetry = notice_harness
|
||||
configure_flag(telemetry, "displayed", performance_payload())
|
||||
|
||||
for _ in range(notices.PERFORMANCE_SLOW_QUERY_CAP):
|
||||
notices.display_performance_slow_query_notice(
|
||||
MagicMock(),
|
||||
"sync",
|
||||
"search",
|
||||
elapsed_seconds=2.1,
|
||||
top_k=10,
|
||||
result_count=2,
|
||||
)
|
||||
|
||||
notices.display_performance_slow_query_notice(
|
||||
MagicMock(),
|
||||
"sync",
|
||||
"search",
|
||||
elapsed_seconds=2.1,
|
||||
top_k=10,
|
||||
result_count=2,
|
||||
)
|
||||
|
||||
assert capsys.readouterr().err == "Performance CTA\n" * notices.PERFORMANCE_SLOW_QUERY_CAP
|
||||
assert telemetry.posthog.evaluate_flags.call_count == notices.PERFORMANCE_SLOW_QUERY_CAP
|
||||
assert telemetry.capture_event.call_count == notices.PERFORMANCE_SLOW_QUERY_CAP
|
||||
assert len(config["notice_state"]["performance_slow_query"]["events"]) == notices.PERFORMANCE_SLOW_QUERY_CAP
|
||||
|
||||
|
||||
def test_performance_slow_query_props_do_not_include_raw_user_inputs(notice_harness):
|
||||
_, telemetry = notice_harness
|
||||
configure_flag(telemetry, "displayed", performance_payload(copy="safe copy"))
|
||||
|
||||
notices.display_performance_slow_query_notice(
|
||||
MagicMock(),
|
||||
"sync",
|
||||
"search",
|
||||
elapsed_seconds=2.1,
|
||||
top_k=10,
|
||||
result_count=2,
|
||||
)
|
||||
|
||||
props = telemetry.capture_event.call_args.args[1]
|
||||
assert "favorite drink" not in str(props)
|
||||
assert "user_id" not in str(props)
|
||||
assert "green tea" not in str(props)
|
||||
@@ -0,0 +1,96 @@
|
||||
import pytest
|
||||
|
||||
from mem0.memory import main as memory_main
|
||||
from mem0.memory.main import AsyncMemory, Memory
|
||||
|
||||
|
||||
def test_sync_add_timestamp_raises_before_validation(monkeypatch):
|
||||
calls = []
|
||||
|
||||
def get_error(sync_type, trigger_function, trigger_parameter):
|
||||
calls.append((sync_type, trigger_function, trigger_parameter))
|
||||
return "blocked timestamp"
|
||||
|
||||
monkeypatch.setattr(memory_main, "get_temporal_feature_error_message", get_error)
|
||||
|
||||
with pytest.raises(ValueError, match="blocked timestamp"):
|
||||
Memory.add(Memory.__new__(Memory), "hello", timestamp=123)
|
||||
|
||||
assert calls == [("sync", "add", "timestamp")]
|
||||
|
||||
|
||||
def test_sync_search_reference_date_raises_before_validation(monkeypatch):
|
||||
calls = []
|
||||
|
||||
def get_error(sync_type, trigger_function, trigger_parameter):
|
||||
calls.append((sync_type, trigger_function, trigger_parameter))
|
||||
return "blocked reference date"
|
||||
|
||||
monkeypatch.setattr(memory_main, "get_temporal_feature_error_message", get_error)
|
||||
|
||||
with pytest.raises(ValueError, match="blocked reference date"):
|
||||
Memory.search(Memory.__new__(Memory), "what happened last week?", reference_date="2025-03-21")
|
||||
|
||||
assert calls == [("sync", "search", "reference_date")]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_add_timestamp_raises_before_validation(monkeypatch):
|
||||
calls = []
|
||||
|
||||
async def get_error(sync_type, trigger_function, trigger_parameter):
|
||||
calls.append((sync_type, trigger_function, trigger_parameter))
|
||||
return "blocked async timestamp"
|
||||
|
||||
monkeypatch.setattr(memory_main, "get_temporal_feature_error_message_async", get_error)
|
||||
|
||||
with pytest.raises(ValueError, match="blocked async timestamp"):
|
||||
await AsyncMemory.add(AsyncMemory.__new__(AsyncMemory), "hello", timestamp=123)
|
||||
|
||||
assert calls == [("async", "add", "timestamp")]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_search_reference_date_raises_before_validation(monkeypatch):
|
||||
calls = []
|
||||
|
||||
async def get_error(sync_type, trigger_function, trigger_parameter):
|
||||
calls.append((sync_type, trigger_function, trigger_parameter))
|
||||
return "blocked async reference date"
|
||||
|
||||
monkeypatch.setattr(memory_main, "get_temporal_feature_error_message_async", get_error)
|
||||
|
||||
with pytest.raises(ValueError, match="blocked async reference date"):
|
||||
await AsyncMemory.search(
|
||||
AsyncMemory.__new__(AsyncMemory),
|
||||
"what happened last week?",
|
||||
reference_date="2025-03-21",
|
||||
)
|
||||
|
||||
assert calls == [("async", "search", "reference_date")]
|
||||
|
||||
|
||||
def test_sync_add_without_timestamp_does_not_call_temporal_feature_notice(monkeypatch):
|
||||
get_error = monkeypatch.setattr(
|
||||
memory_main,
|
||||
"get_temporal_feature_error_message",
|
||||
lambda *args: pytest.fail("temporal feature notice should not run"),
|
||||
)
|
||||
|
||||
with pytest.raises(Exception, match="At least one of 'user_id', 'agent_id', or 'run_id'"):
|
||||
Memory.add(Memory.__new__(Memory), "hello")
|
||||
|
||||
assert get_error is None
|
||||
|
||||
|
||||
def test_sync_search_without_reference_date_does_not_call_temporal_feature_notice(monkeypatch):
|
||||
get_error = monkeypatch.setattr(
|
||||
memory_main,
|
||||
"get_temporal_feature_error_message",
|
||||
lambda *args: pytest.fail("temporal feature notice should not run"),
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="filters must contain"):
|
||||
Memory.search(Memory.__new__(Memory), "hello")
|
||||
|
||||
assert get_error is None
|
||||
@@ -0,0 +1,202 @@
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from mem0.memory import main as memory_main
|
||||
from mem0.memory.main import AsyncMemory, Memory
|
||||
|
||||
|
||||
def make_sync_memory():
|
||||
memory = Memory.__new__(Memory)
|
||||
memory.config = SimpleNamespace(llm=SimpleNamespace(config={}))
|
||||
memory.api_version = "v1.1"
|
||||
memory.reranker = None
|
||||
memory._add_to_vector_store = MagicMock(return_value=[])
|
||||
memory._search_vector_store = MagicMock(return_value=[])
|
||||
return memory
|
||||
|
||||
|
||||
def make_async_memory():
|
||||
memory = AsyncMemory.__new__(AsyncMemory)
|
||||
memory.config = SimpleNamespace(llm=SimpleNamespace(config={}))
|
||||
memory.api_version = "v1.1"
|
||||
memory.reranker = None
|
||||
memory._add_to_vector_store = AsyncMock(return_value=[])
|
||||
memory._search_vector_store = AsyncMock(return_value=[])
|
||||
return memory
|
||||
|
||||
|
||||
def test_sync_add_temporal_metadata_triggers_notice_after_success(monkeypatch):
|
||||
memory = make_sync_memory()
|
||||
temporal_notice = MagicMock()
|
||||
first_run_notice = MagicMock()
|
||||
monkeypatch.setattr(memory_main, "display_temporal_usage_notice", temporal_notice)
|
||||
monkeypatch.setattr(memory_main, "display_first_run_notice", first_run_notice)
|
||||
|
||||
result = Memory.add(
|
||||
memory,
|
||||
"The user visited Paris.",
|
||||
user_id="u1",
|
||||
metadata={"event_date": "2025-04-09"},
|
||||
infer=False,
|
||||
)
|
||||
|
||||
assert result == {"results": []}
|
||||
memory._add_to_vector_store.assert_called_once()
|
||||
temporal_notice.assert_called_once_with(memory, "sync", "add", "metadata", "date_like_metadata")
|
||||
first_run_notice.assert_not_called()
|
||||
|
||||
|
||||
def test_sync_add_non_temporal_metadata_uses_first_run_notice(monkeypatch):
|
||||
memory = make_sync_memory()
|
||||
temporal_notice = MagicMock()
|
||||
first_run_notice = MagicMock()
|
||||
monkeypatch.setattr(memory_main, "display_temporal_usage_notice", temporal_notice)
|
||||
monkeypatch.setattr(memory_main, "display_first_run_notice", first_run_notice)
|
||||
|
||||
Memory.add(memory, "The user likes tea.", user_id="u1", metadata={"topic": "drink"}, infer=False)
|
||||
|
||||
temporal_notice.assert_not_called()
|
||||
first_run_notice.assert_called_once_with(memory, "sync", "add")
|
||||
|
||||
|
||||
def test_sync_add_failure_does_not_trigger_temporal_usage_notice(monkeypatch):
|
||||
memory = make_sync_memory()
|
||||
memory._add_to_vector_store.side_effect = RuntimeError("vector failure")
|
||||
temporal_notice = MagicMock()
|
||||
first_run_notice = MagicMock()
|
||||
monkeypatch.setattr(memory_main, "display_temporal_usage_notice", temporal_notice)
|
||||
monkeypatch.setattr(memory_main, "display_first_run_notice", first_run_notice)
|
||||
|
||||
with pytest.raises(RuntimeError, match="vector failure"):
|
||||
Memory.add(
|
||||
memory,
|
||||
"The user visited Paris.",
|
||||
user_id="u1",
|
||||
metadata={"event_date": "2025-04-09"},
|
||||
infer=False,
|
||||
)
|
||||
|
||||
temporal_notice.assert_not_called()
|
||||
first_run_notice.assert_not_called()
|
||||
|
||||
|
||||
def test_sync_search_temporal_query_triggers_notice_after_success(monkeypatch):
|
||||
memory = make_sync_memory()
|
||||
temporal_notice = MagicMock()
|
||||
first_run_notice = MagicMock()
|
||||
monkeypatch.setattr(memory_main, "capture_event", MagicMock())
|
||||
monkeypatch.setattr(memory_main, "display_temporal_usage_notice", temporal_notice)
|
||||
monkeypatch.setattr(memory_main, "display_first_run_notice", first_run_notice)
|
||||
|
||||
result = Memory.search(memory, "what happened last week?", filters={"user_id": "u1"})
|
||||
|
||||
assert result == {"results": []}
|
||||
memory._search_vector_store.assert_called_once()
|
||||
temporal_notice.assert_called_once_with(memory, "sync", "search", "query", "relative_phrase")
|
||||
first_run_notice.assert_not_called()
|
||||
|
||||
|
||||
def test_sync_search_temporal_filter_triggers_notice_after_success(monkeypatch):
|
||||
memory = make_sync_memory()
|
||||
temporal_notice = MagicMock()
|
||||
monkeypatch.setattr(memory_main, "capture_event", MagicMock())
|
||||
monkeypatch.setattr(memory_main, "display_temporal_usage_notice", temporal_notice)
|
||||
monkeypatch.setattr(memory_main, "display_first_run_notice", MagicMock())
|
||||
|
||||
Memory.search(
|
||||
memory,
|
||||
"favorite drink",
|
||||
filters={"user_id": "u1", "created_at": {"gte": "2025-04-01"}},
|
||||
)
|
||||
|
||||
temporal_notice.assert_called_once_with(memory, "sync", "search", "filter", "date_range_filter")
|
||||
|
||||
|
||||
def test_sync_search_failure_does_not_trigger_temporal_usage_notice(monkeypatch):
|
||||
memory = make_sync_memory()
|
||||
memory._search_vector_store.side_effect = RuntimeError("search failure")
|
||||
temporal_notice = MagicMock()
|
||||
monkeypatch.setattr(memory_main, "capture_event", MagicMock())
|
||||
monkeypatch.setattr(memory_main, "display_temporal_usage_notice", temporal_notice)
|
||||
monkeypatch.setattr(memory_main, "display_first_run_notice", MagicMock())
|
||||
|
||||
with pytest.raises(RuntimeError, match="search failure"):
|
||||
Memory.search(memory, "what happened last week?", filters={"user_id": "u1"})
|
||||
|
||||
temporal_notice.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_add_temporal_metadata_triggers_notice_after_success(monkeypatch):
|
||||
memory = make_async_memory()
|
||||
temporal_notice = AsyncMock()
|
||||
first_run_notice = AsyncMock()
|
||||
monkeypatch.setattr(memory_main, "display_temporal_usage_notice_async", temporal_notice)
|
||||
monkeypatch.setattr(memory_main, "display_first_run_notice_async", first_run_notice)
|
||||
|
||||
result = await AsyncMemory.add(
|
||||
memory,
|
||||
"The user visited Paris.",
|
||||
user_id="u1",
|
||||
metadata={"event_date": "2025-04-09"},
|
||||
infer=False,
|
||||
)
|
||||
|
||||
assert result == {"results": []}
|
||||
memory._add_to_vector_store.assert_awaited_once()
|
||||
temporal_notice.assert_awaited_once_with(memory, "async", "add", "metadata", "date_like_metadata")
|
||||
first_run_notice.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_add_runs_scale_detection_in_thread(monkeypatch):
|
||||
memory = make_async_memory()
|
||||
scale_detector = MagicMock(return_value=("memory_count", "memory_count_threshold", None, 2000, 2000))
|
||||
scale_notice = AsyncMock()
|
||||
first_run_notice = AsyncMock()
|
||||
to_thread_calls = []
|
||||
|
||||
async def to_thread(fn, *args, **kwargs):
|
||||
to_thread_calls.append((fn, args, kwargs))
|
||||
return fn(*args, **kwargs)
|
||||
|
||||
monkeypatch.setattr(memory_main, "detect_scale_threshold_from_add_result", scale_detector)
|
||||
monkeypatch.setattr(memory_main.asyncio, "to_thread", to_thread)
|
||||
monkeypatch.setattr(memory_main, "display_scale_threshold_notice_async", scale_notice)
|
||||
monkeypatch.setattr(memory_main, "display_first_run_notice_async", first_run_notice)
|
||||
|
||||
result = await AsyncMemory.add(memory, "The user likes tea.", user_id="u1", infer=False)
|
||||
|
||||
assert result == {"results": []}
|
||||
assert to_thread_calls == [(scale_detector, (memory, []), {})]
|
||||
scale_detector.assert_called_once_with(memory, [])
|
||||
scale_notice.assert_awaited_once_with(
|
||||
memory,
|
||||
"async",
|
||||
"add",
|
||||
"memory_count",
|
||||
"memory_count_threshold",
|
||||
None,
|
||||
2000,
|
||||
2000,
|
||||
)
|
||||
first_run_notice.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_search_temporal_query_triggers_notice_after_success(monkeypatch):
|
||||
memory = make_async_memory()
|
||||
temporal_notice = AsyncMock()
|
||||
first_run_notice = AsyncMock()
|
||||
monkeypatch.setattr(memory_main, "capture_event", MagicMock())
|
||||
monkeypatch.setattr(memory_main, "display_temporal_usage_notice_async", temporal_notice)
|
||||
monkeypatch.setattr(memory_main, "display_first_run_notice_async", first_run_notice)
|
||||
|
||||
result = await AsyncMemory.search(memory, "what happened last week?", filters={"user_id": "u1"})
|
||||
|
||||
assert result == {"results": []}
|
||||
memory._search_vector_store.assert_awaited_once()
|
||||
temporal_notice.assert_awaited_once_with(memory, "async", "search", "query", "relative_phrase")
|
||||
first_run_notice.assert_not_awaited()
|
||||
Reference in New Issue
Block a user