agent_sdk/web/
provider.rs

1//! Search provider trait and implementations.
2
3use anyhow::{Context, Result};
4use async_trait::async_trait;
5use serde::{Deserialize, Serialize};
6
7/// A single search result.
8#[derive(Clone, Debug, Serialize, Deserialize)]
9pub struct SearchResult {
10    /// Title of the search result.
11    pub title: String,
12    /// URL of the result.
13    pub url: String,
14    /// Snippet/description of the result.
15    pub snippet: String,
16    /// Publication date, if available.
17    pub published_date: Option<String>,
18}
19
20/// Response from a search query.
21#[derive(Clone, Debug, Serialize, Deserialize)]
22pub struct SearchResponse {
23    /// The original search query.
24    pub query: String,
25    /// List of search results.
26    pub results: Vec<SearchResult>,
27    /// Total number of results available (if known).
28    pub total_results: Option<u64>,
29}
30
31/// Trait for search providers.
32///
33/// Implement this trait to add support for different search engines.
34///
35/// # Example
36///
37/// ```ignore
38/// struct MySearchProvider { /* ... */ }
39///
40/// #[async_trait]
41/// impl SearchProvider for MySearchProvider {
42///     async fn search(&self, query: &str, max_results: usize) -> Result<SearchResponse> {
43///         // Implementation
44///     }
45///
46///     fn provider_name(&self) -> &'static str {
47///         "my-provider"
48///     }
49/// }
50/// ```
51#[async_trait]
52pub trait SearchProvider: Send + Sync {
53    /// Execute a search query.
54    ///
55    /// # Arguments
56    ///
57    /// * `query` - The search query string
58    /// * `max_results` - Maximum number of results to return
59    ///
60    /// # Errors
61    ///
62    /// Returns an error if the search request fails.
63    async fn search(&self, query: &str, max_results: usize) -> Result<SearchResponse>;
64
65    /// Get the provider name for logging/debugging.
66    fn provider_name(&self) -> &'static str;
67}
68
69/// Brave Search API provider.
70///
71/// Uses the Brave Search API to perform web searches.
72/// Requires a Brave Search API key from <https://brave.com/search/api/>
73///
74/// # Example
75///
76/// ```ignore
77/// let provider = BraveSearchProvider::new("your-api-key");
78/// let results = provider.search("rust programming", 10).await?;
79/// ```
80#[derive(Clone)]
81pub struct BraveSearchProvider {
82    client: reqwest::Client,
83    api_key: String,
84}
85
86impl BraveSearchProvider {
87    /// Create a new Brave Search provider.
88    ///
89    /// # Arguments
90    ///
91    /// * `api_key` - Brave Search API key
92    #[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    /// Create a provider with a custom HTTP client.
101    #[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
110/// Brave Search API response structures
111mod 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}