(&self, input: &serde_json::Value)
| 547 | } |
| 548 | |
| 549 | async fn web_search(&self, input: &serde_json::Value) -> AppResult<String> { |
| 550 | let query = input["query"] |
| 551 | .as_str() |
| 552 | .ok_or_else(|| AppError::Validation("Missing 'query' field".to_string()))?; |
| 553 | let client = crate::services::http_client::request_client()?; |
| 554 | let url = format!( |
| 555 | "https://api.duckduckgo.com/?q={}&format=json&no_html=1&skip_disambig=1", |
| 556 | urlencoding::encode(query) |
| 557 | ); |
| 558 | |
| 559 | let response = client |
| 560 | .get(&url) |
| 561 | .header("User-Agent", "enowX-Coder/1.0") |
| 562 | .send() |
| 563 | .await |
| 564 | .map_err(AppError::from)?; |
| 565 | |
| 566 | let body = response.text().await.map_err(AppError::from)?; |
| 567 | |
| 568 | // Parse DuckDuckGo response into readable format |
| 569 | let parsed: Value = serde_json::from_str(&body).unwrap_or_default(); |
| 570 | let mut results = Vec::new(); |
| 571 | |
| 572 | // Abstract/instant answer |
| 573 | if let Some(abstract_text) = parsed["AbstractText"].as_str() { |
| 574 | if !abstract_text.is_empty() { |
| 575 | results.push(format!("## Summary\n{}", abstract_text)); |
| 576 | if let Some(url) = parsed["AbstractURL"].as_str() { |
| 577 | results.push(format!("Source: {}", url)); |
| 578 | } |
| 579 | } |
| 580 | } |
| 581 | |
| 582 | // Answer (direct) |
| 583 | if let Some(answer) = parsed["Answer"].as_str() { |
| 584 | if !answer.is_empty() { |
| 585 | results.push(format!("## Answer\n{}", answer)); |
| 586 | } |
| 587 | } |
| 588 | |
| 589 | // Related topics |
| 590 | if let Some(topics) = parsed["RelatedTopics"].as_array() { |
| 591 | let topic_entries: Vec<String> = topics |
| 592 | .iter() |
| 593 | .filter_map(|t| { |
| 594 | let text = t["Text"].as_str()?; |
| 595 | let url = t["FirstURL"].as_str().unwrap_or(""); |
| 596 | if text.is_empty() { |
| 597 | return None; |
| 598 | } |
| 599 | Some(format!("- {} ({})", text, url)) |
| 600 | }) |
| 601 | .take(8) |
| 602 | .collect(); |
| 603 | if !topic_entries.is_empty() { |
| 604 | results.push(format!("## Related\n{}", topic_entries.join("\n"))); |
| 605 | } |
| 606 | } |
no test coverage detected