agent_sdk/web/
provider.rs1use anyhow::{Context, Result};
4use async_trait::async_trait;
5use serde::{Deserialize, Serialize};
6
7#[derive(Clone, Debug, Serialize, Deserialize)]
9pub struct SearchResult {
10 pub title: String,
12 pub url: String,
14 pub snippet: String,
16 pub published_date: Option<String>,
18}
19
20#[derive(Clone, Debug, Serialize, Deserialize)]
22pub struct SearchResponse {
23 pub query: String,
25 pub results: Vec<SearchResult>,
27 pub total_results: Option<u64>,
29}
30
31#[async_trait]
52pub trait SearchProvider: Send + Sync {
53 async fn search(&self, query: &str, max_results: usize) -> Result<SearchResponse>;
64
65 fn provider_name(&self) -> &'static str;
67}
68
69#[derive(Clone)]
81pub struct BraveSearchProvider {
82 client: reqwest::Client,
83 api_key: String,
84}
85
86impl BraveSearchProvider {
87 #[must_use]
93 pub fn new(api_key: impl Into<String>) -> Self {
94 Self {
95 client: reqwest::Client::new(),
96 api_key: api_key.into(),
97 }
98 }
99
100 #[must_use]
102 pub fn with_client(client: reqwest::Client, api_key: impl Into<String>) -> Self {
103 Self {
104 client,
105 api_key: api_key.into(),
106 }
107 }
108}
109
110mod brave_api {
112 use serde::Deserialize;
113
114 #[derive(Debug, Deserialize)]
115 pub struct BraveSearchResponse {
116 pub query: Option<BraveQuery>,
117 pub web: Option<BraveWebResults>,
118 }
119
120 #[derive(Debug, Deserialize)]
121 pub struct BraveQuery {
122 pub original: String,
123 }
124
125 #[derive(Debug, Deserialize)]
126 pub struct BraveWebResults {
127 pub results: Vec<BraveWebResult>,
128 }
129
130 #[derive(Debug, Deserialize)]
131 pub struct BraveWebResult {
132 pub title: String,
133 pub url: String,
134 pub description: Option<String>,
135 pub age: Option<String>,
136 }
137}
138
139#[async_trait]
140impl SearchProvider for BraveSearchProvider {
141 async fn search(&self, query: &str, max_results: usize) -> Result<SearchResponse> {
142 let url = "https://api.search.brave.com/res/v1/web/search";
143
144 let response = self
145 .client
146 .get(url)
147 .header("X-Subscription-Token", &self.api_key)
148 .header("Accept", "application/json")
149 .query(&[
150 ("q", query),
151 ("count", &max_results.to_string()),
152 ("text_decorations", "false"),
153 ])
154 .send()
155 .await
156 .context("Failed to send request to Brave Search API")?;
157
158 if !response.status().is_success() {
159 let status = response.status();
160 let body = response.text().await.unwrap_or_default();
161 anyhow::bail!("Brave Search API error: {status} - {body}");
162 }
163
164 let brave_response: brave_api::BraveSearchResponse = response
165 .json()
166 .await
167 .context("Failed to parse Brave Search API response")?;
168
169 let results = brave_response
170 .web
171 .map(|web| {
172 web.results
173 .into_iter()
174 .map(|r| SearchResult {
175 title: r.title,
176 url: r.url,
177 snippet: r.description.unwrap_or_default(),
178 published_date: r.age,
179 })
180 .collect()
181 })
182 .unwrap_or_default();
183
184 let query_str = brave_response
185 .query
186 .map_or_else(|| query.to_string(), |q| q.original);
187
188 Ok(SearchResponse {
189 query: query_str,
190 results,
191 total_results: None,
192 })
193 }
194
195 fn provider_name(&self) -> &'static str {
196 "brave"
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203
204 #[test]
205 fn test_search_result_serialization() {
206 let result = SearchResult {
207 title: "Test Title".into(),
208 url: "https://example.com".into(),
209 snippet: "Test snippet".into(),
210 published_date: Some("2024-01-01".into()),
211 };
212
213 let json = serde_json::to_string(&result).expect("serialize");
214 assert!(json.contains("Test Title"));
215 assert!(json.contains("example.com"));
216 }
217
218 #[test]
219 fn test_search_response_serialization() {
220 let response = SearchResponse {
221 query: "test query".into(),
222 results: vec![SearchResult {
223 title: "Result 1".into(),
224 url: "https://example.com/1".into(),
225 snippet: "First result".into(),
226 published_date: None,
227 }],
228 total_results: Some(100),
229 };
230
231 let json = serde_json::to_string(&response).expect("serialize");
232 assert!(json.contains("test query"));
233 assert!(json.contains("Result 1"));
234 }
235
236 #[test]
237 fn test_brave_provider_creation() {
238 let provider = BraveSearchProvider::new("test-api-key");
239 assert_eq!(provider.provider_name(), "brave");
240 }
241
242 #[test]
243 fn test_brave_provider_with_custom_client() {
244 let client = reqwest::Client::builder()
245 .timeout(std::time::Duration::from_secs(30))
246 .build()
247 .expect("build client");
248
249 let provider = BraveSearchProvider::with_client(client, "test-api-key");
250 assert_eq!(provider.provider_name(), "brave");
251 }
252}