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}