use std::sync::{Arc, Mutex}; use std::time::Instant; use crate::Result; use anyhow::Context; use async_trait::async_trait; use serde_json::{json, Value}; use super::{Tool, ToolResult}; use crate::consent::ConsentManager; use crate::credentials::CredentialManager; use crate::encryption::VaultHandle; pub struct WebSearchTool { consent_manager: Arc>, _credential_manager: Option>, browser: duckduckgo::browser::Browser, } impl WebSearchTool { pub fn new( consent_manager: Arc>, credential_manager: Option>, _vault: Option>>, ) -> Self { // Create a reqwest client compatible with duckduckgo crate (v0.11) let client = reqwest_011::Client::new(); let browser = duckduckgo::browser::Browser::new(client); Self { consent_manager, _credential_manager: credential_manager, browser, } } } #[async_trait] impl Tool for WebSearchTool { fn name(&self) -> &'static str { "web_search" } fn description(&self) -> &'static str { "Search the web for information using DuckDuckGo API" } fn schema(&self) -> Value { json!({ "type": "object", "properties": { "query": { "type": "string", "minLength": 1, "maxLength": 500, "description": "Search query" }, "max_results": { "type": "integer", "minimum": 1, "maximum": 10, "default": 5, "description": "Maximum number of results" } }, "required": ["query"], "additionalProperties": false }) } fn requires_network(&self) -> bool { true } async fn execute(&self, args: Value) -> Result { let start = Instant::now(); // Check if consent has been granted (non-blocking check) // Consent should have been granted via TUI dialog before tool execution { let consent = self .consent_manager .lock() .expect("Consent manager mutex poisoned"); if !consent.has_consent(self.name()) { return Ok(ToolResult::error( "Consent not granted for web search. This should have been handled by the TUI.", )); } } let query = args .get("query") .and_then(Value::as_str) .context("Missing query parameter")?; let max_results = args.get("max_results").and_then(Value::as_u64).unwrap_or(5) as usize; let user_agent = duckduckgo::user_agents::get("firefox").unwrap_or( "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:91.0) Gecko/20100101 Firefox/91.0", ); // Detect if this is a news query - use news endpoint for better snippets let is_news_query = query.to_lowercase().contains("news") || query.to_lowercase().contains("latest") || query.to_lowercase().contains("today") || query.to_lowercase().contains("recent"); let mut formatted_results = Vec::new(); if is_news_query { // Use news endpoint which returns excerpts/snippets let news_results = self .browser .news(query, "wt-wt", false, Some(max_results), user_agent) .await .context("DuckDuckGo news search failed")?; for result in news_results { formatted_results.push(json!({ "title": result.title, "url": result.url, "snippet": result.body, // news has body/excerpt "source": result.source, "date": result.date })); } } else { // Use lite search for general queries (fast but no snippets) let search_results = self .browser .lite_search(query, "wt-wt", Some(max_results), user_agent) .await .context("DuckDuckGo search failed")?; for result in search_results { formatted_results.push(json!({ "title": result.title, "url": result.url, "snippet": result.snippet })); } } let mut result = ToolResult::success(json!({ "query": query, "results": formatted_results, "total_found": formatted_results.len() })); result.duration = start.elapsed(); Ok(result) } }