Skip to main content

synaptic_tools/
brave.rs

1//! Brave Search API tool for privacy-focused web search.
2
3use async_trait::async_trait;
4use serde_json::{json, Value};
5use synaptic_core::{SynapticError, Tool};
6
7/// Brave Search API tool for web search with privacy focus.
8///
9/// Requires a Brave Search API key. Get one from <https://brave.com/search/api/>.
10///
11/// # Example
12///
13/// ```rust,ignore
14/// use synaptic_tools::BraveSearchTool;
15/// use synaptic_core::Tool;
16///
17/// let tool = BraveSearchTool::new("your-api-key").with_max_results(5);
18/// let result = tool.call(serde_json::json!({"query": "Rust async runtime"})).await?;
19/// ```
20pub struct BraveSearchTool {
21    client: reqwest::Client,
22    api_key: String,
23    max_results: usize,
24}
25
26impl BraveSearchTool {
27    /// Create a new `BraveSearchTool` with the given API key.
28    pub fn new(api_key: impl Into<String>) -> Self {
29        Self {
30            client: reqwest::Client::new(),
31            api_key: api_key.into(),
32            max_results: 5,
33        }
34    }
35
36    /// Set the maximum number of results to return.
37    pub fn with_max_results(mut self, n: usize) -> Self {
38        self.max_results = n;
39        self
40    }
41}
42
43#[async_trait]
44impl Tool for BraveSearchTool {
45    fn name(&self) -> &'static str {
46        "brave_search"
47    }
48
49    fn description(&self) -> &'static str {
50        "Search the web using Brave Search API. Returns titles, URLs, and descriptions of relevant results."
51    }
52
53    fn parameters(&self) -> Option<Value> {
54        Some(json!({
55            "type": "object",
56            "properties": {
57                "query": {
58                    "type": "string",
59                    "description": "The search query"
60                }
61            },
62            "required": ["query"]
63        }))
64    }
65
66    async fn call(&self, args: Value) -> Result<Value, SynapticError> {
67        let query = args["query"]
68            .as_str()
69            .ok_or_else(|| SynapticError::Tool("missing 'query' parameter".to_string()))?;
70
71        let resp = self
72            .client
73            .get("https://api.search.brave.com/res/v1/web/search")
74            .query(&[("q", query), ("count", &self.max_results.to_string())])
75            .header("X-Subscription-Token", &self.api_key)
76            .header("Accept", "application/json")
77            .send()
78            .await
79            .map_err(|e| SynapticError::Tool(format!("Brave Search request: {e}")))?;
80
81        let status = resp.status().as_u16();
82        let body: Value = resp
83            .json()
84            .await
85            .map_err(|e| SynapticError::Tool(format!("Brave Search parse: {e}")))?;
86
87        if status != 200 {
88            return Err(SynapticError::Tool(format!(
89                "Brave Search error ({}): {}",
90                status, body
91            )));
92        }
93
94        let results = body["web"]["results"]
95            .as_array()
96            .map(|arr| {
97                arr.iter()
98                    .map(|r| {
99                        json!({
100                            "title": r["title"],
101                            "url": r["url"],
102                            "description": r["description"],
103                        })
104                    })
105                    .collect::<Vec<_>>()
106            })
107            .unwrap_or_default();
108
109        Ok(json!({ "query": query, "results": results }))
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn tool_metadata() {
119        let tool = BraveSearchTool::new("test-key");
120        assert_eq!(tool.name(), "brave_search");
121        assert!(!tool.description().is_empty());
122        assert_eq!(tool.max_results, 5);
123    }
124
125    #[test]
126    fn tool_schema() {
127        let tool = BraveSearchTool::new("test-key");
128        let schema = tool.parameters().unwrap();
129        assert_eq!(schema["type"], "object");
130        assert!(schema["properties"]["query"].is_object());
131    }
132
133    #[test]
134    fn builder_max_results() {
135        let tool = BraveSearchTool::new("test-key").with_max_results(10);
136        assert_eq!(tool.max_results, 10);
137    }
138
139    #[tokio::test]
140    async fn missing_query_returns_error() {
141        let tool = BraveSearchTool::new("test-key");
142        let result = tool.call(json!({})).await;
143        assert!(result.is_err());
144        assert!(result.unwrap_err().to_string().contains("query"));
145    }
146}