Coverage for src/local_deep_research/web_search_engines/engines/search_engine_ddg.py: 100%
44 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
3from langchain_community.utilities import DuckDuckGoSearchAPIWrapper
4from langchain_core.language_models import BaseLLM
6from ...security.secure_logging import logger
7from ..rate_limiting import RateLimitError
8from ..search_engine_base import BaseSearchEngine, Exposure, Sensitivity
11class DuckDuckGoSearchEngine(BaseSearchEngine):
12 """DuckDuckGo search engine implementation with two-phase retrieval"""
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)
19 is_generic = True
21 def __init__(
22 self,
23 max_results: int = 10,
24 region: str = "us",
25 safe_search: bool = True,
26 llm: Optional[BaseLLM] = None,
27 language: str = "English",
28 include_full_content: bool = False,
29 max_filtered_results: Optional[int] = None,
30 settings_snapshot: Optional[Dict[str, Any]] = None,
31 ):
32 """
33 Initialize the DuckDuckGo search engine.
35 Args:
36 max_results: Maximum number of search results
37 region: Region code for search results
38 safe_search: Whether to enable safe search
39 llm: Language model for relevance filtering
40 language: Language for content processing
41 include_full_content: Whether to include full webpage content in results
42 """
43 max_results = min(max_results, 50)
44 # Initialize the BaseSearchEngine with LLM, max_filtered_results, and max_results
45 super().__init__(
46 llm=llm,
47 max_filtered_results=max_filtered_results,
48 max_results=max_results,
49 include_full_content=include_full_content,
50 settings_snapshot=settings_snapshot,
51 )
52 self.region = region
53 self.safe_search = safe_search
54 self.language = language
56 # Initialize the DuckDuckGo wrapper
57 self.engine = DuckDuckGoSearchAPIWrapper(
58 region=region,
59 max_results=max_results,
60 safesearch="moderate" if safe_search else "off",
61 )
63 # Initialize FullSearchResults if full content is requested
64 self._init_full_search(
65 web_search=self.engine,
66 language=language,
67 max_results=max_results,
68 region=region,
69 time_period="y",
70 safe_search="Moderate" if safe_search else "Off",
71 )
73 def _get_previews(self, query: str) -> List[Dict[str, Any]]:
74 """
75 Get preview information (titles and snippets) for initial search results.
77 Args:
78 query: The search query
80 Returns:
81 List of preview dictionaries with 'id', 'title', and 'snippet' keys
82 """
83 try:
84 # Get search results from DuckDuckGo
85 results = self.engine.results(query, max_results=self.max_results)
87 if not isinstance(results, list):
88 return []
90 # Process results to get previews
91 previews = []
92 for i, result in enumerate(results):
93 preview = {
94 "id": result.get("link"), # Use URL as ID for DDG
95 "title": result.get("title", ""),
96 "snippet": result.get("snippet", ""),
97 "link": result.get("link", ""),
98 }
100 previews.append(preview)
102 return previews
104 except Exception as e:
105 error_msg = str(e)
106 sanitized = self._sanitize_error_message(error_msg)
107 logger.exception(
108 f"Error getting DuckDuckGo previews ({type(e).__name__}): {sanitized}"
109 )
111 # Check for known rate limit patterns
112 if "202 Ratelimit" in error_msg or "ratelimit" in error_msg.lower():
113 raise RateLimitError(f"DuckDuckGo rate limit hit: {sanitized}")
114 if "403" in error_msg or "forbidden" in error_msg.lower():
115 raise RateLimitError(
116 f"DuckDuckGo access forbidden (possible rate limit): {sanitized}"
117 )
118 if (
119 "timeout" in error_msg.lower()
120 or "timed out" in error_msg.lower()
121 ):
122 # Timeouts can sometimes indicate rate limiting
123 raise RateLimitError(
124 f"DuckDuckGo timeout (possible rate limit): {sanitized}"
125 )
127 return []
129 def _get_full_content(
130 self, relevant_items: List[Dict[str, Any]]
131 ) -> List[Dict[str, Any]]:
132 """
133 Get full content for the relevant items by using FullSearchResults.
135 Args:
136 relevant_items: List of relevant preview dictionaries
138 Returns:
139 List of result dictionaries with full content
140 """
141 # If we have FullSearchResults, use it to get full content
142 if hasattr(self, "full_search"):
143 return self.full_search._get_full_content(relevant_items)
145 # Otherwise, just return the relevant items without full content
146 return relevant_items