| 188 | } |
| 189 | |
| 190 | async fn execute(&self, args: &serde_json::Value, ctx: &ToolContext) -> Result<ToolOutput> { |
| 191 | // Validate: return error on unknown fields to catch misconfiguration like `engine` vs `engines` |
| 192 | if let Some(obj) = args.as_object() { |
| 193 | let valid_fields = ["query", "engines", "limit", "timeout", "proxy", "format"]; |
| 194 | for key in obj.keys() { |
| 195 | if !valid_fields.contains(&key.as_str()) { |
| 196 | return Ok(ToolOutput::error(format!( |
| 197 | "web_search: unknown parameter '{}' - did you mean 'engines'? \ |
| 198 | Use 'engines' (plural) as the field name, not 'engine' (singular)", |
| 199 | key |
| 200 | ))); |
| 201 | } |
| 202 | } |
| 203 | } |
| 204 | |
| 205 | let query_str = match args.get("query").and_then(|v| v.as_str()) { |
| 206 | Some(q) => q, |
| 207 | None => return Ok(ToolOutput::error("query parameter is required")), |
| 208 | }; |
| 209 | |
| 210 | if query_str.trim().is_empty() { |
| 211 | return Ok(ToolOutput::error("query must not be empty")); |
| 212 | } |
| 213 | |
| 214 | // Get configuration from context or use defaults |
| 215 | let config = ctx.search_config.as_ref(); |
| 216 | let headless_config = config.and_then(|c| c.headless.as_ref()); |
| 217 | |
| 218 | let default_timeout = config.map(|c| c.timeout).unwrap_or(10); |
| 219 | let default_engines: Vec<&str> = if let Some(cfg) = config { |
| 220 | // Build default engines list from enabled engines in config |
| 221 | cfg.engines |
| 222 | .iter() |
| 223 | .filter(|(_, engine_cfg)| engine_cfg.enabled) |
| 224 | .map(|(name, _)| name.as_str()) |
| 225 | .collect() |
| 226 | } else { |
| 227 | vec!["ddg", "wiki"] |
| 228 | }; |
| 229 | |
| 230 | let engines: Vec<&str> = args |
| 231 | .get("engines") |
| 232 | .and_then(|v| { |
| 233 | if let Some(arr) = v.as_array() { |
| 234 | Some(arr.iter().filter_map(|v| v.as_str()).collect::<Vec<_>>()) |
| 235 | } else { |
| 236 | // Handle comma-separated string like "baidu,ddg" or single engine like "baidu" |
| 237 | v.as_str().map(|s| { |
| 238 | s.split(',') |
| 239 | .map(str::trim) |
| 240 | .filter(|s| !s.is_empty()) |
| 241 | .collect() |
| 242 | }) |
| 243 | } |
| 244 | }) |
| 245 | .unwrap_or_else(|| default_engines.clone()); |
| 246 | |
| 247 | let limit = args |