Skip to main content

ares/tools/
search.rs

1use crate::tools::registry::Tool;
2use crate::types::Result;
3use async_trait::async_trait;
4use serde_json::{json, Value};
5
6/// Web search tool using DuckDuckGo via daedra.
7pub struct WebSearch {
8    _client: reqwest::Client,
9}
10
11impl WebSearch {
12    /// Creates a new WebSearch tool instance.
13    pub fn new() -> Self {
14        Self {
15            _client: reqwest::Client::new(),
16        }
17    }
18}
19
20impl Default for WebSearch {
21    fn default() -> Self {
22        Self::new()
23    }
24}
25
26#[async_trait]
27impl Tool for WebSearch {
28    fn name(&self) -> &str {
29        "web_search"
30    }
31
32    fn description(&self) -> &str {
33        "Search the web for information using DuckDuckGo. Returns a list of search results with titles, snippets, and URLs."
34    }
35
36    fn parameters_schema(&self) -> Value {
37        json!({
38            "type": "object",
39            "properties": {
40                "query": {
41                    "type": "string",
42                    "description": "The search query to look up"
43                },
44                "max_results": {
45                    "type": "integer",
46                    "description": "Maximum number of results to return (default: 5)",
47                    "default": 5
48                }
49            },
50            "required": ["query"]
51        })
52    }
53
54    async fn execute(&self, args: Value) -> Result<Value> {
55        let query = args["query"]
56            .as_str()
57            .ok_or_else(|| crate::types::AppError::InvalidInput("query is required".to_string()))?;
58
59        let max_results = args["max_results"].as_i64().unwrap_or(5) as usize;
60
61        // Use daedra to perform the search
62        let search_args = daedra::types::SearchArgs {
63            query: query.to_string(),
64            options: Some(daedra::types::SearchOptions {
65                num_results: max_results,
66                ..Default::default()
67            }),
68        };
69
70        let results = daedra::tools::search::perform_search(&search_args)
71            .await
72            .map_err(|e| crate::types::AppError::External(format!("Search failed: {}", e)))?;
73
74        // Convert results to JSON
75        let json_results: Vec<Value> = results
76            .data
77            .into_iter()
78            .map(|result| {
79                json!({
80                    "title": result.title,
81                    "url": result.url,
82                    "snippet": result.description
83                })
84            })
85            .collect();
86
87        Ok(json!({
88            "query": query,
89            "results": json_results,
90            "count": json_results.len()
91        }))
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn test_schema() {
101        let tool = WebSearch::new();
102        let schema = tool.parameters_schema();
103        assert_eq!(schema["type"], "object");
104        assert!(schema["properties"]["query"].is_object());
105        assert!(schema["required"]
106            .as_array()
107            .unwrap()
108            .contains(&json!("query")));
109    }
110
111    #[tokio::test]
112    async fn test_missing_query() {
113        let tool = WebSearch::new();
114        let result = tool.execute(json!({})).await;
115        assert!(result.is_err());
116    }
117}