Skip to main content

kagi_sdk/session_web/
models.rs

1use crate::{
2    boundary::{HttpUrl, NonBlankString, NonEmptyString},
3    error::KagiError,
4};
5use serde_json::{Map, Value};
6
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct SearchRequest {
9    pub query: NonEmptyString,
10}
11
12impl SearchRequest {
13    pub fn new(query: impl Into<String>) -> Result<Self, KagiError> {
14        Ok(Self {
15            query: NonEmptyString::new("query", query)?,
16        })
17    }
18
19    pub(crate) fn into_query(self) -> Vec<(String, String)> {
20        vec![("q".to_string(), self.query.as_str().to_string())]
21    }
22}
23
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct SearchResult {
26    pub title: String,
27    pub url: String,
28    pub snippet: Option<String>,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct SearchResponse {
33    pub results: Vec<SearchResult>,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct SummarizeRequest {
38    pub input: SummarizeInput,
39    pub options: SummarizeOptions,
40}
41
42impl SummarizeRequest {
43    pub fn from_url(url: impl AsRef<str>) -> Result<Self, KagiError> {
44        Ok(Self {
45            input: SummarizeInput::Url(HttpUrl::new("url", url)?),
46            options: SummarizeOptions::default(),
47        })
48    }
49
50    pub fn from_text(text: impl Into<String>) -> Result<Self, KagiError> {
51        Ok(Self {
52            input: SummarizeInput::Text(NonBlankString::new("text", text)?),
53            options: SummarizeOptions::default(),
54        })
55    }
56
57    pub fn with_summary_type(mut self, summary_type: SummaryType) -> Self {
58        self.options.summary_type = Some(summary_type);
59        self
60    }
61
62    pub fn with_target_language(
63        mut self,
64        target_language: impl Into<String>,
65    ) -> Result<Self, KagiError> {
66        self.options.target_language =
67            Some(NonEmptyString::new("target_language", target_language)?);
68        Ok(self)
69    }
70
71    pub(crate) fn into_query(self, stream: bool) -> Option<Vec<(String, String)>> {
72        let SummarizeInput::Url(url) = self.input else {
73            return None;
74        };
75
76        let mut query = vec![("url".to_string(), url.as_str().to_string())];
77        query.extend(self.options.into_params());
78        if stream {
79            query.push(("stream".to_string(), "1".to_string()));
80        }
81
82        Some(query)
83    }
84
85    pub(crate) fn into_form(self, stream: bool) -> Option<Vec<(String, String)>> {
86        let SummarizeInput::Text(text) = self.input else {
87            return None;
88        };
89
90        let mut form = vec![("text".to_string(), text.as_str().to_string())];
91        form.extend(self.options.into_params());
92        if stream {
93            form.push(("stream".to_string(), "1".to_string()));
94        }
95
96        Some(form)
97    }
98}
99
100#[derive(Debug, Clone, PartialEq, Eq)]
101pub enum SummarizeInput {
102    Url(HttpUrl),
103    Text(NonBlankString),
104}
105
106#[derive(Debug, Clone, PartialEq, Eq, Default)]
107pub struct SummarizeOptions {
108    pub summary_type: Option<SummaryType>,
109    pub target_language: Option<NonEmptyString>,
110}
111
112impl SummarizeOptions {
113    fn into_params(self) -> Vec<(String, String)> {
114        let mut params = Vec::new();
115
116        if let Some(summary_type) = self.summary_type {
117            params.push((
118                "summary_type".to_string(),
119                summary_type.as_param().to_string(),
120            ));
121        }
122
123        if let Some(target_language) = self.target_language {
124            params.push((
125                "target_language".to_string(),
126                target_language.as_str().to_string(),
127            ));
128        }
129
130        params
131    }
132}
133
134#[derive(Debug, Clone, Copy, PartialEq, Eq)]
135pub enum SummaryType {
136    Summary,
137    Takeaway,
138}
139
140impl SummaryType {
141    fn as_param(self) -> &'static str {
142        match self {
143            Self::Summary => "summary",
144            Self::Takeaway => "takeaway",
145        }
146    }
147}
148
149#[derive(Debug, Clone, PartialEq)]
150pub struct SummarizeResponse {
151    pub markdown: String,
152    pub text: Option<String>,
153    pub status: Option<String>,
154    pub metadata: Map<String, Value>,
155}
156
157#[derive(Debug, Clone, PartialEq, Eq)]
158pub struct SummaryStreamResponse {
159    pub chunks: Vec<String>,
160    pub text: String,
161}
162
163#[deprecated(note = "use SearchRequest and SessionWeb::search(...) instead")]
164#[doc(hidden)]
165pub type HtmlSearchRequest = SearchRequest;
166
167#[deprecated(note = "use SearchResponse returned by SessionWeb::search(...) instead")]
168#[doc(hidden)]
169pub type HtmlSearchResponse = SearchResponse;
170
171#[deprecated(note = "use SearchResult returned by SessionWeb::search(...) instead")]
172#[doc(hidden)]
173pub type HtmlSearchResult = SearchResult;
174
175#[deprecated(note = "use SummarizeRequest with SessionWeb::summarize(...) instead")]
176#[doc(hidden)]
177#[derive(Debug, Clone, PartialEq, Eq)]
178pub struct SummaryLabsUrlRequest {
179    url: HttpUrl,
180}
181
182#[allow(deprecated)]
183impl SummaryLabsUrlRequest {
184    pub fn new(url: impl AsRef<str>) -> Result<Self, KagiError> {
185        Ok(Self {
186            url: HttpUrl::new("url", url)?,
187        })
188    }
189
190    pub(crate) fn into_summarize_request(self) -> SummarizeRequest {
191        SummarizeRequest {
192            input: SummarizeInput::Url(self.url),
193            options: SummarizeOptions::default(),
194        }
195    }
196}
197
198#[deprecated(note = "use SummarizeRequest with SessionWeb::summarize(...) instead")]
199#[doc(hidden)]
200#[derive(Debug, Clone, PartialEq, Eq)]
201pub struct SummaryLabsTextRequest {
202    text: NonBlankString,
203}
204
205#[allow(deprecated)]
206impl SummaryLabsTextRequest {
207    pub fn new(text: impl Into<String>) -> Result<Self, KagiError> {
208        Ok(Self {
209            text: NonBlankString::new("text", text)?,
210        })
211    }
212
213    pub(crate) fn into_summarize_request(self) -> SummarizeRequest {
214        SummarizeRequest {
215            input: SummarizeInput::Text(self.text),
216            options: SummarizeOptions::default(),
217        }
218    }
219}
220
221#[cfg(test)]
222#[allow(deprecated)]
223mod tests {
224    use super::{SummarizeRequest, SummaryLabsTextRequest, SummaryType};
225
226    #[test]
227    fn summary_labs_text_preserves_whitespace() {
228        let form = SummaryLabsTextRequest::new("  keep exact spacing  ")
229            .expect("text should parse")
230            .into_summarize_request()
231            .into_form(false)
232            .expect("text request should produce form");
233
234        assert_eq!(form[0].0, "text");
235        assert_eq!(form[0].1, "  keep exact spacing  ");
236    }
237
238    #[test]
239    fn summarize_options_are_encoded_into_request_params() {
240        let request = SummarizeRequest::from_url("https://example.com")
241            .expect("url should parse")
242            .with_summary_type(SummaryType::Takeaway)
243            .with_target_language("es")
244            .expect("target language should parse");
245
246        let query = request
247            .into_query(true)
248            .expect("url request should produce query params");
249
250        assert!(query
251            .iter()
252            .any(|(key, value)| key == "summary_type" && value == "takeaway"));
253        assert!(query
254            .iter()
255            .any(|(key, value)| key == "target_language" && value == "es"));
256        assert!(query
257            .iter()
258            .any(|(key, value)| key == "stream" && value == "1"));
259    }
260}