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}