rainy_sdk/endpoints/
search.rs

1//! Web Search endpoint for Tavily-powered web research
2//!
3//! This endpoint provides web search and content extraction capabilities.
4//! Requires a Cowork plan with web_research feature enabled.
5
6use crate::{
7    error::{RainyError, Result},
8    search::{ExtractRequest, ExtractResponse, SearchOptions, SearchRequest, SearchResponse},
9    RainyClient,
10};
11
12impl RainyClient {
13    /// Perform a web search using Tavily API.
14    ///
15    /// This method requires a Cowork plan with web_research feature enabled.
16    /// It searches the web and returns relevant results with optional AI-generated answer.
17    ///
18    /// # Arguments
19    ///
20    /// * `query` - The search query string.
21    /// * `options` - Search options (depth, max results, domains, etc.)
22    ///
23    /// # Returns
24    ///
25    /// A `Result` containing `SearchResponse` on success, or `RainyError` on failure.
26    ///
27    /// # Example
28    ///
29    /// ```rust,no_run
30    /// # use rainy_sdk::{RainyClient, search::{SearchOptions}};
31    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
32    /// let client = RainyClient::with_api_key("your-api-key")?;
33    ///
34    /// // Basic search
35    /// let results = client.web_search("Rust programming language", None).await?;
36    /// for result in &results.results {
37    ///     println!("{}: {}", result.title, result.url);
38    /// }
39    ///
40    /// // Advanced search with AI answer
41    /// let options = SearchOptions::advanced()
42    ///     .with_max_results(5)
43    ///     .with_domains(vec!["rust-lang.org".to_string()]);
44    /// let results = client.web_search("Rust async tutorial", Some(options)).await?;
45    ///
46    /// if let Some(answer) = &results.answer {
47    ///     println!("AI Answer: {}", answer);
48    /// }
49    /// # Ok(())
50    /// # }
51    /// ```
52    pub async fn web_search(
53        &self,
54        query: impl Into<String>,
55        options: Option<SearchOptions>,
56    ) -> Result<SearchResponse> {
57        let opts = options.unwrap_or_else(SearchOptions::basic);
58        let request = SearchRequest::new(query.into(), &opts);
59
60        let url = format!("{}/api/v1/search", self.auth_config().base_url);
61
62        let response = self
63            .http_client()
64            .post(&url)
65            .json(&request)
66            .send()
67            .await
68            .map_err(|e| RainyError::NetworkError(e.to_string()))?;
69
70        if response.status().as_u16() == 403 {
71            return Err(RainyError::FeatureNotAvailable {
72                feature: "web_research".to_string(),
73                message: "Web search requires a Cowork plan with web_research enabled".to_string(),
74            });
75        }
76
77        self.handle_response(response).await
78    }
79
80    /// Extract content from URLs using Tavily API.
81    ///
82    /// This method fetches and extracts the main content from a list of URLs.
83    /// Requires a Cowork plan with web_research feature enabled.
84    ///
85    /// # Arguments
86    ///
87    /// * `urls` - Vector of URLs to extract content from (max 10).
88    ///
89    /// # Returns
90    ///
91    /// A `Result` containing `ExtractResponse` with successful and failed extractions.
92    ///
93    /// # Example
94    ///
95    /// ```rust,no_run
96    /// # use rainy_sdk::RainyClient;
97    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
98    /// let client = RainyClient::with_api_key("your-api-key")?;
99    ///
100    /// let response = client.extract_content(vec![
101    ///     "https://www.rust-lang.org/".to_string(),
102    ///     "https://docs.rs/".to_string(),
103    /// ]).await?;
104    ///
105    /// for extracted in &response.results {
106    ///     println!("URL: {}", extracted.url);
107    ///     println!("Content length: {} bytes", extracted.raw_content.len());
108    /// }
109    ///
110    /// if !response.failed_results.is_empty() {
111    ///     println!("Failed: {} URLs", response.failed_results.len());
112    /// }
113    /// # Ok(())
114    /// # }
115    /// ```
116    pub async fn extract_content(&self, urls: Vec<String>) -> Result<ExtractResponse> {
117        if urls.is_empty() {
118            return Err(RainyError::ValidationError(
119                "At least one URL is required".to_string(),
120            ));
121        }
122
123        if urls.len() > 10 {
124            return Err(RainyError::ValidationError(
125                "Maximum 10 URLs allowed per request".to_string(),
126            ));
127        }
128
129        let request = ExtractRequest { urls };
130        let url = format!("{}/api/v1/search/extract", self.auth_config().base_url);
131
132        let response = self
133            .http_client()
134            .post(&url)
135            .json(&request)
136            .send()
137            .await
138            .map_err(|e| RainyError::NetworkError(e.to_string()))?;
139
140        if response.status().as_u16() == 403 {
141            return Err(RainyError::FeatureNotAvailable {
142                feature: "web_research".to_string(),
143                message: "Content extraction requires a Cowork plan with web_research enabled"
144                    .to_string(),
145            });
146        }
147
148        self.handle_response(response).await
149    }
150
151    /// Check if web search feature is available for the current plan.
152    ///
153    /// This is a convenience method that checks the Cowork capabilities.
154    pub async fn can_web_search(&self) -> bool {
155        self.can_use_feature("web_research").await
156    }
157}