Coverage for fastagency/runtimes/ag2/tools/websurfer.py: 81%

107 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-04-19 12:16 +0000

1from typing import TYPE_CHECKING, Annotated, Any, Optional, Union 1cghbaidef

2 

3from autogen import LLMConfig 1cghbaidef

4from autogen.agentchat import AssistantAgent as AutoGenAssistantAgent 1cghbaidef

5from autogen.agentchat import ConversableAgent as AutoGenConversableAgent 1cghbaidef

6from autogen.agentchat.contrib.web_surfer import WebSurferAgent as AutoGenWebSurferAgent 1cghbaidef

7from pydantic import BaseModel, Field, HttpUrl 1cghbaidef

8 

9if TYPE_CHECKING: 1cghbaidef

10 from autogen.io.run_response import RunResponse 

11 

12__all__ = ["WebSurferAnswer", "WebSurferTool"] 1cghbaidef

13 

14 

15class WebSurferAnswer(BaseModel): 1cghbaidef

16 task: Annotated[str, Field(..., description="The task to be completed")] 1cghbaidef

17 is_successful: Annotated[ 1cghbaidef

18 bool, Field(..., description="Whether the task was successful") 

19 ] 

20 short_answer: Annotated[ 1cghbaidef

21 str, 

22 Field( 

23 ..., 

24 description="The short answer to the task without any explanation", 

25 ), 

26 ] 

27 long_answer: Annotated[ 1cghbaidef

28 str, 

29 Field(..., description="The long answer to the task with explanation"), 

30 ] 

31 visited_links: Annotated[ 1cghbaidef

32 list[HttpUrl], 

33 Field(..., description="The list of visited links to generate the answer"), 

34 ] 

35 

36 @staticmethod 1cghbaidef

37 def get_example_answer() -> "WebSurferAnswer": 1cghbaidef

38 return WebSurferAnswer( 1cbadef

39 task="What is the most popular QLED TV to buy on amazon.com?", 

40 is_successful=True, 

41 short_answer='Amazon Fire TV 55" Omni QLED Series 4K UHD smart TV', 

42 long_answer='Amazon has the best selling page by different categories and there is a category for QLED TVs under electroincs. The most popular QLED TV is Amazon Fire TV 55" Omni QLED Series 4K UHD smart TV, Dolby Vision IQ, Fire TV Ambient Experience, local dimming, hands-free with Alexa. It is the best selling QLED TV on Amazon.', 

43 visited_links=[ 

44 "https://www.amazon.com/Best-Sellers/", 

45 "https://www.amazon.com/Best-Sellers-Electronics-QLED-TVs/", 

46 ], 

47 ) 

48 

49 

50class WebSurferTool: # implements Toolable 1cghbaidef

51 def __init__( 1cghbaidef

52 self, 

53 *, 

54 name_prefix: str, 

55 llm_config: LLMConfig, 

56 summarizer_llm_config: LLMConfig, 

57 viewport_size: int = 4096, 

58 bing_api_key: Optional[str] = None, 

59 max_consecutive_auto_reply: int = 30, 

60 max_links_to_click: int = 10, 

61 websurfer_kwargs: Optional[dict[str, Any]] = None, 

62 assistant_kwargs: Optional[dict[str, Any]] = None, 

63 ): 

64 """Create a new WebSurferChat instance. 

65 

66 Args: 

67 name_prefix (str): The name prefix of the inner AutoGen agents 

68 llm_config (Dict[str, Any]): The LLM configuration 

69 summarizer_llm_config (Dict[str, Any]): The summarizer LLM configuration 

70 viewport_size (int, optional): The viewport size. Defaults to 4096. 

71 bing_api_key (Optional[str], optional): The Bing API key. Defaults to None. 

72 max_consecutive_auto_reply (int, optional): The maximum consecutive auto reply. Defaults to 30. 

73 max_links_to_click (int, optional): The maximum links to click. Defaults to 10. 

74 websurfer_kwargs (Optional[Dict[str, Any]], optional): The WebSurfer kwargs. Defaults to None. 

75 assistant_kwargs (Optional[Dict[str, Any]], optional): The Assistant kwargs. Defaults to None. 

76 """ 

77 if websurfer_kwargs is None: 77 ↛ 79line 77 didn't jump to line 79 because the condition on line 77 was always true1cbadef

78 websurfer_kwargs = {} 1cbadef

79 if assistant_kwargs is None: 79 ↛ 82line 79 didn't jump to line 82 because the condition on line 79 was always true1cbadef

80 assistant_kwargs = {} 1cbadef

81 

82 self.name_prefix = name_prefix 1cbadef

83 self.llm_config = llm_config 1cbadef

84 self.summarizer_llm_config = summarizer_llm_config 1cbadef

85 self.viewport_size = viewport_size 1cbadef

86 self.bing_api_key = bing_api_key 1cbadef

87 self.max_consecutive_auto_reply = max_consecutive_auto_reply 1cbadef

88 self.max_links_to_click = max_links_to_click 1cbadef

89 self.websurfer_kwargs = websurfer_kwargs 1cbadef

90 self.assistant_kwargs = assistant_kwargs 1cbadef

91 

92 self.task = "not set yet" 1cbadef

93 self.last_is_termination_msg_error = "" 1cbadef

94 

95 self.browser_config = { 1cbadef

96 "viewport_size": self.viewport_size, 

97 "bing_api_key": self.bing_api_key, 

98 "request_kwargs": { 

99 "headers": { 

100 "User-Agent": "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36", 

101 } 

102 }, 

103 } 

104 

105 if "human_input_mode" in self.websurfer_kwargs: 105 ↛ 106line 105 didn't jump to line 106 because the condition on line 105 was never true1cbadef

106 self.websurfer_kwargs.pop("human_input_mode") 

107 

108 self.websurfer = AutoGenWebSurferAgent( 1cbadef

109 name=f"{self.name_prefix}_inner_websurfer", 

110 llm_config=self.llm_config, 

111 summarizer_llm_config=self.summarizer_llm_config, 

112 browser_config=self.browser_config, 

113 human_input_mode="NEVER", 

114 is_termination_msg=self.is_termination_msg, 

115 **self.websurfer_kwargs, 

116 ) 

117 

118 if "human_input_mode" in self.assistant_kwargs: 118 ↛ 119line 118 didn't jump to line 119 because the condition on line 118 was never true1cbadef

119 self.assistant_kwargs.pop("human_input_mode") 

120 

121 self.assistant = AutoGenAssistantAgent( 1cbadef

122 name=f"{self.name_prefix}_inner_assistant", 

123 llm_config=self.llm_config, 

124 human_input_mode="NEVER", 

125 system_message=self.system_message, 

126 max_consecutive_auto_reply=self.max_consecutive_auto_reply, 

127 # is_termination_msg=self.is_termination_msg, 

128 **self.assistant_kwargs, 

129 ) 

130 

131 def is_termination_msg(self, msg: dict[str, Any]) -> bool: 1cghbaidef

132 # print(f"is_termination_msg({msg=})") 

133 if ( 133 ↛ 138line 133 didn't jump to line 138 because the condition on line 133 was never true1a

134 "content" in msg 

135 and msg["content"] is not None 

136 and "TERMINATE" in msg["content"] 

137 ): 

138 return True 

139 try: 1a

140 WebSurferAnswer.model_validate_json(msg["content"]) 1a

141 return True 1a

142 except Exception as e: 1a

143 self.last_is_termination_msg_error = str(e) 1a

144 return False 1a

145 

146 def _get_error_message(self, response: "RunResponse") -> Optional[str]: 1cghbaidef

147 messages = [ 1a

148 str(m["content"]) if "content" in m else "" for m in response.messages 

149 ] 

150 last_message = messages[-1] 1a

151 if "TERMINATE" in last_message: 151 ↛ 152line 151 didn't jump to line 152 because the condition on line 151 was never true1a

152 return self.error_message 

153 

154 try: 1a

155 WebSurferAnswer.model_validate_json(last_message) 1a

156 except Exception: 

157 return self.error_message 

158 

159 return None 1a

160 

161 def _get_answer(self, response: "RunResponse") -> WebSurferAnswer: 1cghbaidef

162 messages = [ 1a

163 str(m["content"]) if "content" in m else "" for m in response.messages 

164 ] 

165 last_message = messages[-1] 1a

166 return WebSurferAnswer.model_validate_json(last_message) 1a

167 

168 def _chat_with_websurfer( 1cghbaidef

169 self, message: str, clear_history: bool, **kwargs: Any 

170 ) -> WebSurferAnswer: 

171 msg: Optional[str] = message 1a

172 

173 while msg is not None: 1a

174 response = self.websurfer.run( 1a

175 self.assistant, 

176 clear_history=clear_history, 

177 message=msg, 

178 ) 

179 response.process() 1a

180 msg = self._get_error_message(response) 1a

181 clear_history = False 1a

182 

183 return self._get_answer(response) 1a

184 

185 def _get_error_from_exception(self, task: str, e: Exception) -> str: 1cghbaidef

186 answer = WebSurferAnswer( 

187 task=task, 

188 is_successful=False, 

189 short_answer="unexpected error occurred", 

190 long_answer=str(e), 

191 visited_links=[], 

192 ) 

193 

194 return self.create_final_reply(task, answer) 

195 

196 def create_final_reply(self, task: str, message: WebSurferAnswer) -> str: 1cghbaidef

197 retval = ( 1a

198 "We have successfully completed the task:\n\n" 

199 if message.is_successful 

200 else "We have failed to complete the task:\n\n" 

201 ) 

202 retval += f"{task}\n\n" 1a

203 retval += f"Short answer: {message.short_answer}\n\n" 1a

204 retval += f"Explanation: {message.long_answer}\n\n" 1a

205 retval += "Visited links:\n" 1a

206 for link in message.visited_links: 206 ↛ 207line 206 didn't jump to line 207 because the loop on line 206 never started1a

207 retval += f" - {link}\n" 

208 

209 return retval 1a

210 

211 def create_new_task( 1cghbaidef

212 self, task: Annotated[str, "a new task for websurfer to perform"] 

213 ) -> str: 

214 self.task = task 1a

215 try: 1a

216 answer = self._chat_with_websurfer( 1a

217 message=self.initial_message, 

218 clear_history=True, 

219 ) 

220 except Exception as e: 

221 return self._get_error_from_exception(task, e) 

222 

223 return self.create_final_reply(task, answer) 1a

224 

225 def continue_task_with_additional_instructions( 1cghbaidef

226 self, message: Annotated[str, "a followup message to the existing task"] 

227 ) -> str: 

228 try: 

229 answer = self._chat_with_websurfer( 

230 message=message, 

231 clear_history=False, 

232 ) 

233 except Exception as e: 

234 return self._get_error_from_exception(message, e) 

235 

236 return self.create_final_reply(message, answer) 

237 

238 @property 1cghbaidef

239 def example_answer(self) -> WebSurferAnswer: 1cghbaidef

240 return WebSurferAnswer.get_example_answer() 1cbadef

241 

242 @property 1cghbaidef

243 def initial_message(self) -> str: 1cghbaidef

244 return f"""We are tasked with the following task: 1a

245 

246{self.task} 

247 

248If no link is provided in the task, you should search the internet first to find the relevant information. 

249 

250The focus is on the provided url and its subpages, we do NOT care about the rest of the website i.e. parent pages. 

251e.g. If the url is 'https://www.example.com/products/air-conditioners', we are interested ONLY in the 'air-conditioners' and its subpages. 

252 

253AFTER visiting the home page, create a step-by-step plan BEFORE visiting the other pages. 

254You can click on MAXIMUM {self.max_links_to_click} links. Do NOT try to click all the links on the page, but only the ones which are most relevant for the task (MAX {self.max_links_to_click})! 

255Do NOT visit the same page multiple times, but only once! 

256If your co-speaker repeats the same message, inform him that you have already answered to that message and ask him to proceed with the task. 

257e.g. "I have already answered to that message, please proceed with the task or you will be penalized!" 

258""" 

259 

260 @property 1cghbaidef

261 def error_message(self) -> str: 1cghbaidef

262 return f"""Please output the JSON-encoded answer only in the following message before trying to terminate the chat. 

263 

264IMPORTANT: 

265 - NEVER enclose JSON-encoded answer in any other text or formatting including '```json' ... '```' or similar! 

266 - NEVER write TERMINATE in the same message as the JSON-encoded answer! 

267 

268EXAMPLE: 

269 

270{self.example_answer.model_dump_json()} 

271 

272NEGATIVE EXAMPLES: 

273 

2741. Do NOT include 'TERMINATE' in the same message as the JSON-encoded answer! 

275 

276{self.example_answer.model_dump_json()} 

277 

278TERMINATE 

279 

2802. Do NOT include triple backticks or similar! 

281 

282```json 

283{self.example_answer.model_dump_json()} 

284``` 

285 

286THE LAST ERROR MESSAGE: 

287 

288{self.last_is_termination_msg_error} 

289 

290""" 

291 

292 @property 1cghbaidef

293 def system_message(self) -> str: 1cghbaidef

294 return f"""You are in charge of navigating the web_surfer agent to scrape the web. 1cbadef

295web_surfer is able to CLICK on links, SCROLL down, and scrape the content of the web page. e.g. you cen tell him: "Click the 'Getting Started' result". 

296Each time you receive a reply from web_surfer, you need to tell him what to do next. e.g. "Click the TV link" or "Scroll down". 

297It is very important that you explore ONLY the page links relevant for the task! 

298 

299GUIDELINES: 

300- Once you retrieve the content from the received url, you can tell web_surfer to CLICK on links, SCROLL down... 

301By using these capabilities, you will be able to retrieve MUCH BETTER information from the web page than by just scraping the given URL! 

302You MUST use these capabilities when you receive a task for a specific category/product etc. 

303- do NOT try to create a summary without clicking on any link, because you will be missing a lot of information! 

304- if needed, you can instruct web surfer to SEARCH THE WEB for information. 

305 

306Examples: 

307"Click the 'TVs' result" - This way you will navigate to the TVs section of the page and you will find more information about TVs. 

308"Click 'Electronics' link" - This way you will navigate to the Electronics section of the page and you will find more information about Electronics. 

309"Click the 'Next' button" 

310"Search the internet for the best TV to buy" - this will get links to initial pages to start the search 

311 

312- Do NOT try to click all the links on the page, but only the ones which are RELEVANT for the task! Web pages can be very long and you will be penalized if spend too much time on this task! 

313- Your final goal is to summarize the findings for the given task. The summary must be in English! 

314- Create a summary after you successfully retrieve the information from the web page. 

315- It is useful to include in the summary relevant links where more information can be found. 

316e.g. If the page is offering to sell TVs, you can include a link to the TV section of the page. 

317- If you get some 40x error, please do NOT give up immediately, but try to navigate to another page and continue with the task. 

318Give up only if you get 40x error on ALL the pages which you tried to navigate to. 

319 

320 

321FINAL MESSAGE: 

322Once you have retrieved he wanted information, YOU MUST create JSON-encoded string. Summary created by the web_surfer is not enough! 

323You MUST not include any other text or formatting in the message, only JSON-encoded summary! 

324 

325An example of the JSON-encoded summary: 

326{self.example_answer.model_dump_json()} 

327 

328TERMINATION: 

329When YOU are finished and YOU have created JSON-encoded answer, write a single 'TERMINATE' to end the task. 

330 

331OFTEN MISTAKES: 

332- Web surfer expects you to tell him what LINK NAME to click next, not the relative link. E.g. in case of '[Hardware](/Hardware), the proper command would be 'Click into 'Hardware''. 

333- Links presented are often RELATIVE links, so you need to ADD the DOMAIN to the link to make it work. E.g. link '/products/air-conditioners' should be 'https://www.example.com/products/air-conditioners' 

334- You do NOT need to click on MAX number of links. If you have enough information from the first xy links, you do NOT need to click on the rest of the links! 

335- Do NOT repeat the steps you have already completed! 

336- ALWAYS include the NEXT steps in the message! 

337- Do NOT instruct web_surfer to click on the same link multiple times. If there are some problems with the link, MOVE ON to the next one! 

338- Also, if web_surfer does not understand your message, just MOVE ON to the next link! 

339- NEVER REPEAT the same instructions to web_surfer! If he does not understand the first time, MOVE ON to the next link! 

340- NEVER enclose JSON-encoded answer in any other text or formatting including '```json' ... '```' or similar! 

341""" 

342 

343 def register( 1cghbaidef

344 self, 

345 *, 

346 caller: AutoGenConversableAgent, 

347 executor: Union[AutoGenConversableAgent, list[AutoGenConversableAgent]], 

348 ) -> None: 

349 @caller.register_for_llm( # type: ignore[misc] 1ba

350 name="create_new_websurfing_task", 

351 description="Creates a new task for a websurfer that can include searching or browsing the internet.", 

352 ) 

353 def create_new_task( 1ba

354 task: Annotated[str, "a new task for websurfer to perform"], 

355 ) -> str: 

356 return self.create_new_task(task) 1a

357 

358 @caller.register_for_llm( # type: ignore[misc] 1ba

359 name="continue_websurfing_task_with_additional_instructions", 

360 description="Continue an existing task for a websurfer with additional instructions.", 

361 ) 

362 def continue_task_with_additional_instructions( 1ba

363 message: Annotated[ 

364 str, 

365 "Additional instructions for the task after receiving the initial answer", 

366 ], 

367 ) -> str: 

368 return self.continue_task_with_additional_instructions(message) 

369 

370 executors = executor if isinstance(executor, list) else [executor] 1ba

371 for executor in executors: 1ba

372 executor.register_for_execution(name="create_new_websurfing_task")( 1ba

373 create_new_task 

374 ) 

375 executor.register_for_execution( 1ba

376 name="continue_websurfing_task_with_additional_instructions" 

377 )(continue_task_with_additional_instructions)