fix(memory): reject empty search queries (#5258)
This commit is contained in:
committed by
GitHub
parent
366945965d
commit
f32eb4406b
@@ -33,6 +33,15 @@ setup_config()
|
||||
ENTITY_PARAMS = frozenset({"user_id", "agent_id", "app_id", "run_id"})
|
||||
|
||||
|
||||
def _validate_and_trim_search_query(query: str) -> str:
|
||||
if not isinstance(query, str):
|
||||
raise ValueError("Invalid query: must be a non-empty string.")
|
||||
trimmed = query.strip()
|
||||
if not trimmed:
|
||||
raise ValueError("Invalid query: cannot be empty or whitespace-only.")
|
||||
return trimmed
|
||||
|
||||
|
||||
def _maybe_alias_anon_to_email(user_email):
|
||||
"""Fire $identify per prior anon ID so PostHog merges them into email.
|
||||
|
||||
@@ -306,6 +315,7 @@ class MemoryClient:
|
||||
|
||||
kwargs = {**(options.model_dump(exclude_unset=True) if options else {}), **kwargs}
|
||||
params = self._prepare_params(kwargs)
|
||||
query = _validate_and_trim_search_query(query)
|
||||
payload = {"query": query, **params}
|
||||
|
||||
response = self.client.post("/v3/memories/search/", json=payload)
|
||||
@@ -1221,6 +1231,7 @@ class AsyncMemoryClient:
|
||||
|
||||
kwargs = {**(options.model_dump(exclude_unset=True) if options else {}), **kwargs}
|
||||
params = self._prepare_params(kwargs)
|
||||
query = _validate_and_trim_search_query(query)
|
||||
payload = {"query": query, **params}
|
||||
|
||||
response = await self.async_client.post("/v3/memories/search/", json=payload)
|
||||
|
||||
@@ -169,6 +169,21 @@ def _validate_search_params(threshold: Optional[float] = None, top_k: Optional[i
|
||||
)
|
||||
|
||||
|
||||
def _validate_and_trim_search_query(query: str) -> str:
|
||||
"""
|
||||
Validates and normalizes a search query before embedding/vector search.
|
||||
|
||||
Raises:
|
||||
ValueError: If query is not a string or is empty/whitespace-only.
|
||||
"""
|
||||
if not isinstance(query, str):
|
||||
raise ValueError("Invalid query: must be a non-empty string.")
|
||||
trimmed = query.strip()
|
||||
if not trimmed:
|
||||
raise ValueError("Invalid query: cannot be empty or whitespace-only.")
|
||||
return trimmed
|
||||
|
||||
|
||||
def _is_sensitive_field(field_name: str) -> bool:
|
||||
"""Check if a field should be redacted for telemetry safety.
|
||||
|
||||
@@ -1178,6 +1193,7 @@ class Memory(MemoryBase):
|
||||
|
||||
# Validate search parameters (before applying defaults)
|
||||
_validate_search_params(threshold=threshold, top_k=top_k)
|
||||
query = _validate_and_trim_search_query(query)
|
||||
|
||||
# Validate and trim entity IDs in filters
|
||||
effective_filters = filters.copy() if filters else {}
|
||||
@@ -2620,6 +2636,7 @@ class AsyncMemory(MemoryBase):
|
||||
|
||||
# Validate search parameters (before applying defaults)
|
||||
_validate_search_params(threshold=threshold, top_k=top_k)
|
||||
query = _validate_and_trim_search_query(query)
|
||||
|
||||
# Validate and trim entity IDs in filters
|
||||
effective_filters = filters.copy() if filters else {}
|
||||
|
||||
@@ -26,6 +26,28 @@ def mock_memory_client():
|
||||
class TestSearchEntityParamRejection:
|
||||
"""Tests that top-level entity params are rejected in search()."""
|
||||
|
||||
@pytest.mark.parametrize("query", ["", " ", "\n\t"])
|
||||
def test_search_rejects_empty_query(self, mock_memory_client, query):
|
||||
"""search() should reject empty or whitespace-only queries before API calls."""
|
||||
with pytest.raises(ValueError, match="Invalid query.*empty or whitespace-only"):
|
||||
mock_memory_client.search(query, filters={"user_id": "u1"})
|
||||
|
||||
mock_memory_client.client.post.assert_not_called()
|
||||
|
||||
def test_search_trims_query_before_api_call(self, mock_memory_client):
|
||||
"""search() should send the normalized query to the API."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = {"results": []}
|
||||
mock_response.raise_for_status.return_value = None
|
||||
mock_memory_client.client.post.return_value = mock_response
|
||||
|
||||
mock_memory_client.search(" test query ", filters={"user_id": "u1"})
|
||||
|
||||
mock_memory_client.client.post.assert_called_once_with(
|
||||
"/v3/memories/search/",
|
||||
json={"query": "test query", "filters": {"user_id": "u1"}},
|
||||
)
|
||||
|
||||
def test_search_rejects_user_id_kwarg(self, mock_memory_client):
|
||||
"""search() should reject user_id as top-level kwarg."""
|
||||
with pytest.raises(ValueError, match=r"user_id"):
|
||||
|
||||
@@ -290,6 +290,29 @@ class TestEntityIdValidation:
|
||||
class TestSearchParamValidation:
|
||||
"""Tests for search parameter validation (threshold and top_k)."""
|
||||
|
||||
@pytest.mark.parametrize("query", ["", " ", "\n\t"])
|
||||
def test_search_rejects_empty_query(self, memory_instance, query):
|
||||
"""Search should reject empty or whitespace-only queries before retrieval work."""
|
||||
memory_instance.embedding_model.embed = Mock()
|
||||
|
||||
with pytest.raises(ValueError, match="Invalid query.*empty or whitespace-only"):
|
||||
memory_instance.search(query, filters={"user_id": "test"})
|
||||
|
||||
memory_instance.embedding_model.embed.assert_not_called()
|
||||
|
||||
def test_search_trims_query_before_embedding(self, memory_instance):
|
||||
"""Search should normalize leading/trailing whitespace before embedding."""
|
||||
mock_memories = []
|
||||
memory_instance.vector_store.search = Mock(return_value=mock_memories)
|
||||
memory_instance.vector_store.keyword_search = Mock(return_value=None)
|
||||
memory_instance.embedding_model.embed = Mock(return_value=[0.1, 0.2, 0.3])
|
||||
|
||||
with patch("mem0.memory.main.lemmatize_for_bm25", return_value="test"), \
|
||||
patch("mem0.memory.main.extract_entities", return_value=[]):
|
||||
memory_instance.search(" test ", filters={"user_id": "test"})
|
||||
|
||||
memory_instance.embedding_model.embed.assert_called_once_with("test", "search")
|
||||
|
||||
def test_search_rejects_threshold_above_1(self, memory_instance):
|
||||
"""Search should reject threshold > 1."""
|
||||
with pytest.raises(ValueError, match="Invalid threshold.*Must be between 0 and 1"):
|
||||
|
||||
Reference in New Issue
Block a user