1use async_trait::async_trait;
4use serde_json::{json, Value};
5use synaptic_core::{SynapticError, Tool};
6
7pub struct BraveSearchTool {
21 client: reqwest::Client,
22 api_key: String,
23 max_results: usize,
24}
25
26impl BraveSearchTool {
27 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 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}