Coverage for src/local_deep_research/web_search_engines/engines/search_engine_tavily.py: 98%
67 statements
« prev ^ index » next coverage.py v7.15.1, created at 2026-07-20 01:24 +0000
« prev ^ index » next coverage.py v7.15.1, created at 2026-07-20 01:24 +0000
1from typing import Any, Dict, List, Optional
3import requests
4from langchain_core.language_models import BaseLLM
5from ...security.secure_logging import logger
7from ...security.safe_requests import safe_post
8from ..rate_limiting import RateLimitError
9from ..search_engine_base import BaseSearchEngine, Exposure, Sensitivity
12class TavilySearchEngine(BaseSearchEngine):
13 """Tavily search engine implementation with two-phase approach"""
15 # Mark as public search engine
16 is_public = True
17 egress_sensitivity = Sensitivity.NON_SENSITIVE
18 egress_exposure = Exposure.EXPOSING
19 # Mark as generic search engine (general web search)
20 is_generic = True
22 def __init__(
23 self,
24 max_results: int = 10,
25 region: str = "US",
26 time_period: str = "y",
27 safe_search: bool = True,
28 search_language: str = "English",
29 api_key: Optional[str] = None,
30 llm: Optional[BaseLLM] = None,
31 include_full_content: bool = True,
32 max_filtered_results: Optional[int] = None,
33 search_depth: str = "basic",
34 include_domains: Optional[List[str]] = None,
35 exclude_domains: Optional[List[str]] = None,
36 settings_snapshot: Optional[Dict[str, Any]] = None,
37 **kwargs,
38 ):
39 """
40 Initialize the Tavily search engine.
42 Args:
43 max_results: Maximum number of search results
44 region: Region code for search results (not used by Tavily currently)
45 time_period: Time period for search results (not used by Tavily currently)
46 safe_search: Whether to enable safe search (not used by Tavily currently)
47 search_language: Language for search results (not used by Tavily currently)
48 api_key: Tavily API key (can also be set via LDR_SEARCH_ENGINE_WEB_TAVILY_API_KEY env var or in UI settings)
49 llm: Language model for relevance filtering
50 include_full_content: Whether to include full webpage content in results
51 max_filtered_results: Maximum number of results to keep after filtering
52 search_depth: "basic" or "advanced" - controls search quality vs speed
53 include_domains: List of domains to include in search
54 exclude_domains: List of domains to exclude from search
55 settings_snapshot: Settings snapshot for thread context
56 **kwargs: Additional parameters (ignored but accepted for compatibility)
57 """
58 # Initialize the BaseSearchEngine with LLM, max_filtered_results, and max_results
59 super().__init__(
60 llm=llm,
61 max_filtered_results=max_filtered_results,
62 max_results=max_results,
63 include_full_content=include_full_content,
64 settings_snapshot=settings_snapshot,
65 )
66 self.search_depth = search_depth
67 self.include_domains = include_domains or []
68 self.exclude_domains = exclude_domains or []
70 # Get API key - check params, settings, or env vars
71 tavily_api_key = self._resolve_api_key(
72 api_key,
73 "search.engine.web.tavily.api_key",
74 engine_name="Tavily",
75 settings_snapshot=settings_snapshot,
76 )
78 self.api_key = tavily_api_key
79 self.base_url = "https://api.tavily.com"
81 # If full content is requested, initialize FullSearchResults
82 if include_full_content:
83 # Create a simple wrapper for Tavily API calls
84 class TavilyWrapper:
85 def __init__(self, parent):
86 self.parent = parent
88 def run(self, query):
89 return self.parent._get_previews(query)
91 self._init_full_search(
92 web_search=TavilyWrapper(self),
93 language=search_language,
94 max_results=max_results,
95 region=region,
96 time_period=time_period,
97 safe_search="moderate" if safe_search else "off",
98 )
100 def _get_previews(self, query: str) -> List[Dict[str, Any]]:
101 """
102 Get preview information from Tavily Search.
104 Args:
105 query: The search query
107 Returns:
108 List of preview dictionaries
109 """
110 logger.info("Getting search results from Tavily")
112 try:
113 # Prepare the request payload
114 payload = {
115 "api_key": self.api_key,
116 "query": query[:400], # Limit query length
117 "search_depth": self.search_depth,
118 "max_results": min(
119 20, self.max_results
120 ), # Tavily has a max limit
121 "include_answer": False, # We don't need the AI answer
122 "include_images": False, # We don't need images
123 "include_raw_content": self.include_full_content, # Get content if requested
124 }
126 # Add domain filters if specified
127 if self.include_domains:
128 payload["include_domains"] = self.include_domains
129 if self.exclude_domains:
130 payload["exclude_domains"] = self.exclude_domains
132 # Apply rate limiting before request
133 self._last_wait_time = self.rate_tracker.apply_rate_limit(
134 self.engine_type
135 )
137 # Make the API request
138 response = safe_post(
139 f"{self.base_url}/search",
140 json=payload,
141 headers={"Content-Type": "application/json"},
142 timeout=30,
143 )
145 # Check for rate limits
146 self._raise_if_rate_limit(response.status_code)
148 response.raise_for_status()
150 # Parse the response
151 data = response.json()
152 results = data.get("results", [])
154 # Format results as previews
155 previews = []
156 for i, result in enumerate(results):
157 url = self._clean_result_url(result.get("url"))
158 preview = {
159 "id": url or str(i), # Use URL as ID
160 "title": result.get("title", ""),
161 "link": url,
162 "snippet": result.get(
163 "content", ""
164 ), # Tavily calls it "content"
165 "displayed_link": url,
166 "position": i,
167 }
169 # Store full Tavily result for later
170 preview["_full_result"] = result
172 previews.append(preview)
174 # Store the previews for potential full content retrieval
175 self._search_results = previews
177 return previews
179 except RateLimitError:
180 raise # Re-raise rate limit errors
181 except requests.exceptions.RequestException as e:
182 safe_msg = self._scrub_error(e)
183 logger.warning(f"Error getting Tavily results: {safe_msg}")
184 self._raise_if_rate_limit(e)
185 return []
186 except Exception as e:
187 safe_msg = self._scrub_error(e)
188 logger.warning(
189 f"Unexpected error getting Tavily results: {safe_msg}"
190 )
191 return []
193 def _get_full_content(
194 self, relevant_items: List[Dict[str, Any]]
195 ) -> List[Dict[str, Any]]:
196 """
197 Get full content for the relevant search results.
198 Extends base implementation to include Tavily's raw_content.
200 Args:
201 relevant_items: List of relevant preview dictionaries
203 Returns:
204 List of result dictionaries with full content if available
205 """
206 results = super()._get_full_content(relevant_items)
208 # If Tavily provided raw_content and full content is requested, use it
209 if self.include_full_content:
210 for result in results:
211 if "raw_content" in result: 211 ↛ 210line 211 didn't jump to line 210 because the condition on line 211 was always true
212 result["content"] = result.get(
213 "raw_content", result.get("content", "")
214 )
216 return results