From 2fbb1676fa9d62e8a35c490d3c70040347b5ee37 Mon Sep 17 00:00:00 2001 From: Travis Frisinger Date: Thu, 9 Oct 2025 19:42:32 -0600 Subject: [PATCH] feat: Add max_results parameter to search tool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add max_results parameter with default value of 5 to search_tool() - Update fetch_with_fallback() to accept and pass max_results - Update Serper client to request exact number of results via API - Update DuckDuckGo client calls to use max_results - Update test expectations to include new parameter - All search-related unit tests passing (16/16) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- docker/clients/serper_client.py | 10 +++++++--- docker/services/search_service.py | 7 ++++--- .../unit/services/test_search_service_with_serper.py | 2 +- docker/tools/search_tool.py | 7 ++++--- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/docker/clients/serper_client.py b/docker/clients/serper_client.py index 839ec44..0cc496d 100644 --- a/docker/clients/serper_client.py +++ b/docker/clients/serper_client.py @@ -35,19 +35,22 @@ def _convert_organic_results(organic_results: list) -> List[APISearchResult]: ] -def fetch_search_results(query: str, api_key: str) -> List[APISearchResult]: +def fetch_search_results( + query: str, api_key: str, max_results: int = 5 +) -> List[APISearchResult]: """ Fetches search results from the Serper API. Args: query: The search query api_key: The Serper API key + max_results: Maximum number of results to return (default: 5) Returns: A list of APISearchResult objects from Serper API """ url = "https://google.serper.dev/search" - payload = json.dumps({"q": query}) + payload = json.dumps({"q": query, "num": max_results}) headers = {"X-API-KEY": api_key, "Content-Type": "application/json"} try: @@ -57,7 +60,8 @@ def fetch_search_results(query: str, api_key: str) -> List[APISearchResult]: # Process and return the search results if "organic" in data: - return _convert_organic_results(data["organic"]) + results = _convert_organic_results(data["organic"]) + return results[:max_results] # Ensure we don't exceed max_results return [] except Exception as e: logger.error(f"Error fetching search results: {str(e)}") diff --git a/docker/services/search_service.py b/docker/services/search_service.py index 8183c4f..c84edf6 100644 --- a/docker/services/search_service.py +++ b/docker/services/search_service.py @@ -16,7 +16,7 @@ def fetch_with_fallback( - query: str, serper_api_key: str = "" + query: str, serper_api_key: str = "", max_results: int = 5 ) -> Tuple[List[APISearchResult], str]: """ Fetch search results with automatic fallback from Serper to DuckDuckGo. @@ -24,6 +24,7 @@ def fetch_with_fallback( Args: query: Search query string serper_api_key: Optional Serper API key + max_results: Maximum number of results to return (default: 5) Returns: Tuple of (results list, source name) @@ -35,13 +36,13 @@ def fetch_with_fallback( if serper_api_key: logger.info("Using Serper API for search") search_source = "Serper API" - api_results = fetch_search_results(query, serper_api_key) + api_results = fetch_search_results(query, serper_api_key, max_results) # Fall back to DuckDuckGo if no API key or no results from Serper if not api_results: _log_fallback_reason(serper_api_key) search_source = "DuckDuckGo (free fallback)" - api_results = fetch_duckduckgo_search_results(query) + api_results = fetch_duckduckgo_search_results(query, max_results) return api_results, search_source diff --git a/docker/tests/unit/services/test_search_service_with_serper.py b/docker/tests/unit/services/test_search_service_with_serper.py index e9f6119..5a6fd1b 100644 --- a/docker/tests/unit/services/test_search_service_with_serper.py +++ b/docker/tests/unit/services/test_search_service_with_serper.py @@ -29,7 +29,7 @@ def test_uses_serper_when_key_provided(self, mock_serper): assert source == "Serper API" assert len(results) == 1 assert results[0].title == "Serper Result" - mock_serper.assert_called_once_with("test query", "fake_key") + mock_serper.assert_called_once_with("test query", "fake_key", 5) @patch("services.search_service.fetch_duckduckgo_search_results") @patch("services.search_service.fetch_search_results") diff --git a/docker/tools/search_tool.py b/docker/tools/search_tool.py index 47f8679..50c6932 100644 --- a/docker/tools/search_tool.py +++ b/docker/tools/search_tool.py @@ -21,7 +21,7 @@ SERPER_API_KEY = os.environ.get("SERPER_API_KEY", "") -async def search_tool(query: str, ctx=None) -> dict: +async def search_tool(query: str, ctx=None, max_results: int = 5) -> dict: """Search the web for information on a given query. This MCP tool searches the web using Serper API (premium) or DuckDuckGo @@ -30,11 +30,12 @@ async def search_tool(query: str, ctx=None) -> dict: Args: query: The search query string ctx: Optional MCP context (may contain authentication headers) + max_results: Maximum number of results to return (default: 5) Returns: Dict representation of SearchResponse model (for MCP JSON serialization) """ - logger.info(f"Processing search request: {query}") + logger.info(f"Processing search request: {query} (max {max_results} results)") # Validate authentication if WEBCAT_API_KEY is set is_valid, error_msg = validate_bearer_token(ctx) @@ -49,7 +50,7 @@ async def search_tool(query: str, ctx=None) -> dict: return response.model_dump() # Fetch results with automatic fallback - api_results, search_source = fetch_with_fallback(query, SERPER_API_KEY) + api_results, search_source = fetch_with_fallback(query, SERPER_API_KEY, max_results) # Check if we got any results if not api_results: