1+ import logging
2+ import time
3+ from typing import Any , Dict , Optional
4+
15from application .agents .tools .base import Tool
2- from duckduckgo_search import DDGS
6+
7+ logger = logging .getLogger (__name__ )
8+
9+ MAX_RETRIES = 3
10+ RETRY_DELAY = 2.0
11+ DEFAULT_TIMEOUT = 15
312
413
514class DuckDuckGoSearchTool (Tool ):
@@ -10,71 +19,123 @@ class DuckDuckGoSearchTool(Tool):
1019
1120 def __init__ (self , config ):
1221 self .config = config
22+ self .timeout = config .get ("timeout" , DEFAULT_TIMEOUT )
23+
24+ def _get_ddgs_client (self ):
25+ from ddgs import DDGS
26+
27+ return DDGS (timeout = self .timeout )
28+
29+ def _execute_with_retry (self , operation , operation_name : str ) -> Dict [str , Any ]:
30+ last_error = None
31+ for attempt in range (1 , MAX_RETRIES + 1 ):
32+ try :
33+ results = operation ()
34+ return {
35+ "status_code" : 200 ,
36+ "results" : list (results ) if results else [],
37+ "message" : f"{ operation_name } completed successfully." ,
38+ }
39+ except Exception as e :
40+ last_error = e
41+ error_str = str (e ).lower ()
42+ if "ratelimit" in error_str or "429" in error_str :
43+ if attempt < MAX_RETRIES :
44+ delay = RETRY_DELAY * attempt
45+ logger .warning (
46+ f"{ operation_name } rate limited, retrying in { delay } s (attempt { attempt } /{ MAX_RETRIES } )"
47+ )
48+ time .sleep (delay )
49+ continue
50+ logger .error (f"{ operation_name } failed: { e } " )
51+ break
52+ return {
53+ "status_code" : 500 ,
54+ "results" : [],
55+ "message" : f"{ operation_name } failed: { str (last_error )} " ,
56+ }
1357
1458 def execute_action (self , action_name , ** kwargs ):
1559 actions = {
1660 "ddg_web_search" : self ._web_search ,
1761 "ddg_image_search" : self ._image_search ,
62+ "ddg_news_search" : self ._news_search ,
1863 }
19-
20- if action_name in actions :
21- return actions [action_name ](** kwargs )
22- else :
64+ if action_name not in actions :
2365 raise ValueError (f"Unknown action: { action_name } " )
66+ return actions [action_name ](** kwargs )
2467
2568 def _web_search (
2669 self ,
27- query ,
28- max_results = 5 ,
29- ):
30- print (f"Performing DuckDuckGo web search for: { query } " )
70+ query : str ,
71+ max_results : int = 5 ,
72+ region : str = "wt-wt" ,
73+ safesearch : str = "moderate" ,
74+ timelimit : Optional [str ] = None ,
75+ ) -> Dict [str , Any ]:
76+ logger .info (f"DuckDuckGo web search: { query } " )
3177
32- try :
33- results = DDGS ().text (
78+ def operation ():
79+ client = self ._get_ddgs_client ()
80+ return client .text (
3481 query ,
35- max_results = max_results ,
82+ region = region ,
83+ safesearch = safesearch ,
84+ timelimit = timelimit ,
85+ max_results = min (max_results , 20 ),
3686 )
3787
38- return {
39- "status_code" : 200 ,
40- "results" : results ,
41- "message" : "Web search completed successfully." ,
42- }
43- except Exception as e :
44- return {
45- "status_code" : 500 ,
46- "message" : f"Web search failed: { str (e )} " ,
47- }
88+ return self ._execute_with_retry (operation , "Web search" )
4889
4990 def _image_search (
5091 self ,
51- query ,
52- max_results = 5 ,
53- ):
54- print (f"Performing DuckDuckGo image search for: { query } " )
55-
56- try :
57- results = DDGS ().images (
58- keywords = query ,
59- max_results = max_results ,
92+ query : str ,
93+ max_results : int = 5 ,
94+ region : str = "wt-wt" ,
95+ safesearch : str = "moderate" ,
96+ timelimit : Optional [str ] = None ,
97+ ) -> Dict [str , Any ]:
98+ logger .info (f"DuckDuckGo image search: { query } " )
99+
100+ def operation ():
101+ client = self ._get_ddgs_client ()
102+ return client .images (
103+ query ,
104+ region = region ,
105+ safesearch = safesearch ,
106+ timelimit = timelimit ,
107+ max_results = min (max_results , 50 ),
108+ )
109+
110+ return self ._execute_with_retry (operation , "Image search" )
111+
112+ def _news_search (
113+ self ,
114+ query : str ,
115+ max_results : int = 5 ,
116+ region : str = "wt-wt" ,
117+ safesearch : str = "moderate" ,
118+ timelimit : Optional [str ] = None ,
119+ ) -> Dict [str , Any ]:
120+ logger .info (f"DuckDuckGo news search: { query } " )
121+
122+ def operation ():
123+ client = self ._get_ddgs_client ()
124+ return client .news (
125+ query ,
126+ region = region ,
127+ safesearch = safesearch ,
128+ timelimit = timelimit ,
129+ max_results = min (max_results , 20 ),
60130 )
61131
62- return {
63- "status_code" : 200 ,
64- "results" : results ,
65- "message" : "Image search completed successfully." ,
66- }
67- except Exception as e :
68- return {
69- "status_code" : 500 ,
70- "message" : f"Image search failed: { str (e )} " ,
71- }
132+ return self ._execute_with_retry (operation , "News search" )
72133
73134 def get_actions_metadata (self ):
74135 return [
75136 {
76137 "name" : "ddg_web_search" ,
77- "description" : "Perform a web search using DuckDuckGo." ,
138+ "description" : "Search the web using DuckDuckGo. Returns titles, URLs, and snippets ." ,
78139 "parameters" : {
79140 "type" : "object" ,
80141 "properties" : {
@@ -84,25 +145,59 @@ def get_actions_metadata(self):
84145 },
85146 "max_results" : {
86147 "type" : "integer" ,
87- "description" : "Number of results to return (default: 5)" ,
148+ "description" : "Number of results (default: 5, max: 20)" ,
149+ },
150+ "region" : {
151+ "type" : "string" ,
152+ "description" : "Region code (default: wt-wt for worldwide, us-en for US)" ,
153+ },
154+ "timelimit" : {
155+ "type" : "string" ,
156+ "description" : "Time filter: d (day), w (week), m (month), y (year)" ,
88157 },
89158 },
90159 "required" : ["query" ],
91160 },
92161 },
93162 {
94163 "name" : "ddg_image_search" ,
95- "description" : "Perform an image search using DuckDuckGo." ,
164+ "description" : "Search for images using DuckDuckGo. Returns image URLs and metadata ." ,
96165 "parameters" : {
97166 "type" : "object" ,
98167 "properties" : {
99168 "query" : {
100169 "type" : "string" ,
101- "description" : "Search query" ,
170+ "description" : "Image search query" ,
102171 },
103172 "max_results" : {
104173 "type" : "integer" ,
105- "description" : "Number of results to return (default: 5, max: 50)" ,
174+ "description" : "Number of results (default: 5, max: 50)" ,
175+ },
176+ "region" : {
177+ "type" : "string" ,
178+ "description" : "Region code (default: wt-wt for worldwide)" ,
179+ },
180+ },
181+ "required" : ["query" ],
182+ },
183+ },
184+ {
185+ "name" : "ddg_news_search" ,
186+ "description" : "Search for news articles using DuckDuckGo. Returns recent news." ,
187+ "parameters" : {
188+ "type" : "object" ,
189+ "properties" : {
190+ "query" : {
191+ "type" : "string" ,
192+ "description" : "News search query" ,
193+ },
194+ "max_results" : {
195+ "type" : "integer" ,
196+ "description" : "Number of results (default: 5, max: 20)" ,
197+ },
198+ "timelimit" : {
199+ "type" : "string" ,
200+ "description" : "Time filter: d (day), w (week), m (month)" ,
106201 },
107202 },
108203 "required" : ["query" ],
0 commit comments