rainy_sdk/endpoints/search.rs
1//! Web Research endpoint
2//!
3//! This endpoint provides web research capabilities via the Rainy API v3 search API.
4
5use crate::{
6 error::{RainyError, Result},
7 search::{DeepResearchResponse, ResearchConfig},
8 RainyClient,
9};
10use serde::Deserialize;
11use serde_json::json;
12
13impl RainyClient {
14 /// Perform deep web research on a topic.
15 ///
16 /// This method leverages the Rainy Agent Network to perform comprehensive
17 /// web research using providers like Exa or Tavily.
18 ///
19 /// # Arguments
20 ///
21 /// * `topic` - The research topic or question.
22 /// * `config` - Research configuration (provider, depth, etc.)
23 ///
24 /// # Returns
25 ///
26 /// A `Result` containing `DeepResearchResponse` on success.
27 ///
28 /// # Example
29 ///
30 /// ```rust,no_run
31 /// # use rainy_sdk::{RainyClient, search::ResearchConfig, models::{ResearchProvider, ResearchDepth}};
32 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
33 /// let client = RainyClient::with_api_key("your-api-key")?;
34 ///
35 /// // Basic research
36 /// let response = client.research("Latest Rust features", None).await?;
37 /// if let Some(content) = response.result {
38 /// println!("Report: {}", content);
39 /// }
40 ///
41 /// // Advanced deep research with Exa
42 /// let config = ResearchConfig::new()
43 /// .with_provider(ResearchProvider::Exa)
44 /// .with_depth(ResearchDepth::Advanced);
45 ///
46 /// let response = client.research("Quantum Computing advances", Some(config)).await?;
47 /// # Ok(())
48 /// # }
49 /// ```
50 pub async fn research(
51 &self,
52 topic: impl Into<String>,
53 config: Option<ResearchConfig>,
54 ) -> Result<DeepResearchResponse> {
55 #[derive(Deserialize)]
56 struct SearchResultItem {
57 title: Option<String>,
58 url: Option<String>,
59 content: Option<String>,
60 snippet: Option<String>,
61 }
62 #[derive(Deserialize)]
63 struct SearchData {
64 results: Vec<SearchResultItem>,
65 }
66 #[derive(Deserialize)]
67 struct SearchEnvelope {
68 success: bool,
69 data: SearchData,
70 }
71
72 let cfg = config.unwrap_or_default();
73 let topic = topic.into();
74 let url = self.api_v1_url("/search");
75 let search_depth = match cfg.depth {
76 crate::models::ResearchDepth::Advanced => "advanced",
77 _ => "basic",
78 };
79 let request = json!({
80 "query": topic,
81 "searchDepth": search_depth,
82 "maxResults": cfg.max_sources.min(20),
83 });
84
85 let response = self
86 .http_client()
87 .post(&url)
88 .json(&request)
89 .send()
90 .await
91 .map_err(|e| RainyError::Network {
92 message: e.to_string(),
93 retryable: true,
94 source_error: Some(e.to_string()),
95 })?;
96
97 let envelope: SearchEnvelope = self.handle_response(response).await?;
98
99 let results_json = envelope
100 .data
101 .results
102 .iter()
103 .map(|item| {
104 json!({
105 "title": item.title,
106 "url": item.url,
107 "snippet": item.snippet.as_ref().or(item.content.as_ref()),
108 })
109 })
110 .collect::<Vec<_>>();
111
112 let synthesized_content = envelope
113 .data
114 .results
115 .iter()
116 .enumerate()
117 .map(|(idx, item)| {
118 let title = item.title.as_deref().unwrap_or("Untitled");
119 let url = item.url.as_deref().unwrap_or("");
120 let snippet = item
121 .snippet
122 .as_deref()
123 .or(item.content.as_deref())
124 .unwrap_or("");
125 format!("{}. {}\n{}\n{}", idx + 1, title, url, snippet)
126 })
127 .collect::<Vec<_>>()
128 .join("\n\n");
129
130 Ok(DeepResearchResponse {
131 success: envelope.success,
132 mode: "sync".to_string(),
133 result: Some(json!({
134 "content": synthesized_content,
135 "results": results_json,
136 })),
137 task_id: None,
138 generated_at: None,
139 provider: Some("tavily".to_string()),
140 message: None,
141 })
142 }
143}