From 861bc024e8de5e57dd69e1ff452f0ef31f453b7d Mon Sep 17 00:00:00 2001 From: RerankerGuo <121015044+RerankerGuo@users.noreply.github.com> Date: Wed, 5 Aug 2026 13:52:59 +0800 Subject: [PATCH] fix(search): bound external provider requests --- .../tree_text_memory/retrieve/bochasearch.py | 9 +++- .../retrieve/internet_retriever.py | 9 +++- .../textual/test_internet_search_timeouts.py | 51 +++++++++++++++++++ 3 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 tests/memories/textual/test_internet_search_timeouts.py diff --git a/src/memos/memories/textual/tree_text_memory/retrieve/bochasearch.py b/src/memos/memories/textual/tree_text_memory/retrieve/bochasearch.py index 8d68e6ea7..02797670d 100644 --- a/src/memos/memories/textual/tree_text_memory/retrieve/bochasearch.py +++ b/src/memos/memories/textual/tree_text_memory/retrieve/bochasearch.py @@ -23,6 +23,8 @@ logger = get_logger(__name__) +_REQUEST_TIMEOUT_SECONDS = 30 + class BochaAISearchAPI: """BochaAI Search API Client""" @@ -102,7 +104,12 @@ def search_ai( def _post(self, url: str, body: dict) -> list[dict]: """Send POST request and parse BochaAI search results.""" try: - resp = requests.post(url, headers=self.headers, json=body) + resp = requests.post( + url, + headers=self.headers, + json=body, + timeout=_REQUEST_TIMEOUT_SECONDS, + ) resp.raise_for_status() raw_data = resp.json() diff --git a/src/memos/memories/textual/tree_text_memory/retrieve/internet_retriever.py b/src/memos/memories/textual/tree_text_memory/retrieve/internet_retriever.py index 819b4e369..18e0d64fb 100644 --- a/src/memos/memories/textual/tree_text_memory/retrieve/internet_retriever.py +++ b/src/memos/memories/textual/tree_text_memory/retrieve/internet_retriever.py @@ -14,6 +14,9 @@ ) +_REQUEST_TIMEOUT_SECONDS = 30 + + class GoogleCustomSearchAPI: """Google Custom Search API Client""" @@ -59,7 +62,11 @@ def search(self, query: str, num_results: int | None = None, start_index: int = } try: - response = requests.get(self.base_url, params=params) + response = requests.get( + self.base_url, + params=params, + timeout=_REQUEST_TIMEOUT_SECONDS, + ) response.raise_for_status() return response.json() except requests.exceptions.RequestException as e: diff --git a/tests/memories/textual/test_internet_search_timeouts.py b/tests/memories/textual/test_internet_search_timeouts.py new file mode 100644 index 000000000..16e452ddc --- /dev/null +++ b/tests/memories/textual/test_internet_search_timeouts.py @@ -0,0 +1,51 @@ +from unittest.mock import MagicMock, patch + +from memos.memories.textual.tree_text_memory.retrieve.bochasearch import BochaAISearchAPI +from memos.memories.textual.tree_text_memory.retrieve.internet_retriever import ( + GoogleCustomSearchAPI, +) + + +@patch("memos.memories.textual.tree_text_memory.retrieve.bochasearch.requests.post") +def test_bocha_search_uses_bounded_request_timeout(mock_post): + response = MagicMock() + response.json.return_value = {"data": {"webPages": {"value": []}}} + mock_post.return_value = response + + BochaAISearchAPI("api-key").search_web("query") + + mock_post.assert_called_once_with( + "https://api.bochaai.com/v1/web-search", + headers={ + "Authorization": "Bearer api-key", + "Content-Type": "application/json", + }, + json={ + "query": "query", + "summary": True, + "freshness": "noLimit", + "count": 20, + }, + timeout=30, + ) + + +@patch("memos.memories.textual.tree_text_memory.retrieve.internet_retriever.requests.get") +def test_google_search_uses_bounded_request_timeout(mock_get): + response = MagicMock() + response.json.return_value = {"items": []} + mock_get.return_value = response + + GoogleCustomSearchAPI("api-key", "engine-id").search("query") + + mock_get.assert_called_once_with( + "https://www.googleapis.com/customsearch/v1", + params={ + "key": "api-key", + "cx": "engine-id", + "q": "query", + "num": 10, + "start": 1, + }, + timeout=30, + )