Coverage for src/local_deep_research/web_search_engines/engines/search_engine_serpapi.py: 100%
39 statements
« prev ^ index » next coverage.py v7.15.1, created at 2026-07-19 23:35 +0000
« prev ^ index » next coverage.py v7.15.1, created at 2026-07-19 23:35 +0000
1from ...security.secure_logging import logger
2from typing import Any, Dict, List, Optional
4from langchain_community.utilities import SerpAPIWrapper
5from langchain_core.language_models import BaseLLM
7from ..rate_limiting import RateLimitError
8from ..search_engine_base import BaseSearchEngine, Exposure, Sensitivity
11class SerpAPISearchEngine(BaseSearchEngine):
12 """Google search engine implementation using SerpAPI with two-phase approach"""
14 # Mark as public search engine
15 is_public = True
16 egress_sensitivity = Sensitivity.NON_SENSITIVE
17 egress_exposure = Exposure.EXPOSING
18 # Mark as generic search engine (general web search via Google)
19 is_generic = True
21 def __init__(
22 self,
23 max_results: int = 10,
24 region: str = "us",
25 time_period: str = "y",
26 safe_search: bool = True,
27 search_language: str = "English",
28 api_key: Optional[str] = None,
29 language_code_mapping: Optional[Dict[str, str]] = None,
30 llm: Optional[BaseLLM] = None,
31 include_full_content: bool = False,
32 max_filtered_results: Optional[int] = None,
33 settings_snapshot: Optional[Dict[str, Any]] = None,
34 **kwargs,
35 ):
36 """
37 Initialize the SerpAPI search engine.
39 Args:
40 max_results: Maximum number of search results
41 region: Region code for search results
42 time_period: Time period for search results
43 safe_search: Whether to enable safe search
44 search_language: Language for search results
45 api_key: SerpAPI API key (can also be set via LDR_SEARCH_ENGINE_WEB_SERPAPI_API_KEY env var or in UI settings)
46 language_code_mapping: Mapping from language names to codes
47 llm: Language model for relevance filtering
48 include_full_content: Whether to include full webpage content in results
49 max_filtered_results: Maximum number of results to keep after filtering
50 settings_snapshot: Settings snapshot for thread context
51 **kwargs: Additional parameters (ignored but accepted for compatibility)
52 """
53 # Initialize the BaseSearchEngine with LLM, max_filtered_results, and max_results
54 super().__init__(
55 llm=llm,
56 max_filtered_results=max_filtered_results,
57 max_results=max_results,
58 include_full_content=include_full_content,
59 settings_snapshot=settings_snapshot,
60 )
62 # Set up language code mapping
63 if language_code_mapping is None:
64 from ...utilities.search_utilities import LANGUAGE_CODE_MAP
66 language_code_mapping = LANGUAGE_CODE_MAP
68 # Get API key - check params, settings, or env vars
69 serpapi_api_key = self._resolve_api_key(
70 api_key,
71 "search.engine.web.serpapi.api_key",
72 engine_name="SerpAPI",
73 settings_snapshot=settings_snapshot,
74 )
75 # Store for error-message redaction (BaseSearchEngine._scrub_error
76 # reads self.api_key via _secret_attrs). SerpAPIWrapper sends the key
77 # as a URL param the regex pass already catches, but storing it gives
78 # the literal-redaction pass a belt-and-suspenders for any other shape.
79 self.api_key = serpapi_api_key
81 # Get language code
82 language_code = language_code_mapping.get(search_language.lower(), "en")
84 # Initialize SerpAPI wrapper
85 self.engine = SerpAPIWrapper(
86 serpapi_api_key=serpapi_api_key,
87 params={
88 "engine": "google",
89 "hl": language_code,
90 "gl": region,
91 "safe": "active" if safe_search else "off",
92 "tbs": f"qdr:{time_period}",
93 # Google's "num" tops out at 100 results per request; cap so a
94 # large user-supplied max_results can't request an unbounded page.
95 # Use the base-class-normalized self.max_results (positive int)
96 # rather than the raw arg, which could be None/str.
97 "num": min(self.max_results, 100),
98 },
99 )
101 # If full content is requested, initialize FullSearchResults
102 self._init_full_search(
103 web_search=self.engine,
104 language=search_language,
105 max_results=max_results,
106 region=region,
107 time_period=time_period,
108 safe_search="Moderate" if safe_search else "Off",
109 )
111 def _get_previews(self, query: str) -> List[Dict[str, Any]]:
112 """
113 Get preview information from SerpAPI.
115 Args:
116 query: The search query
118 Returns:
119 List of preview dictionaries
120 """
121 logger.info("Getting search results from SerpAPI")
123 try:
124 # Get search results from SerpAPI
125 organic_results = self.engine.results(query).get(
126 "organic_results", []
127 )
129 # Format results as previews
130 previews: list[dict[str, Any]] = []
131 for result in organic_results:
132 preview = {
133 "id": result.get(
134 "position", len(previews)
135 ), # Use position as ID
136 "title": result.get("title", ""),
137 "link": result.get("link", ""),
138 "snippet": result.get("snippet", ""),
139 "displayed_link": result.get("displayed_link", ""),
140 "position": result.get("position"),
141 }
143 # Store full SerpAPI result for later
144 preview["_full_result"] = result
146 previews.append(preview)
148 # Store the previews for potential full content retrieval
149 self._search_results = previews
151 return previews
153 except RateLimitError:
154 raise
155 except Exception as e:
156 safe_msg = self._scrub_error(e)
157 logger.warning(f"Error getting SerpAPI results: {safe_msg}")
158 self._raise_if_rate_limit(e)
159 return []