1use std::collections::BTreeMap;
6
7use serde::{Deserialize, Deserializer, Serialize};
8use serde_json::{Map, Value};
9
10#[derive(Debug, Clone, PartialEq, Serialize)]
12pub struct Format {
13 #[serde(rename = "type")]
14 pub kind: String,
15 #[serde(flatten)]
16 pub options: Map<String, Value>,
17}
18
19impl Format {
20 pub fn new(kind: &str) -> Self {
21 Self {
22 kind: kind.to_string(),
23 options: Map::new(),
24 }
25 }
26}
27
28impl<'de> Deserialize<'de> for Format {
29 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
30 match Value::deserialize(d)? {
31 Value::String(kind) => Ok(Format::new(&kind)),
32 Value::Object(mut map) => {
33 let kind = match map.remove("type") {
34 Some(Value::String(k)) => k,
35 _ => {
36 return Err(serde::de::Error::custom(
37 "format object needs a string `type`",
38 ));
39 }
40 };
41 Ok(Format { kind, options: map })
42 }
43 _ => Err(serde::de::Error::custom(
44 "format must be a string or an object",
45 )),
46 }
47 }
48}
49
50pub(crate) fn default_formats() -> Vec<Format> {
51 vec![Format::new("markdown")]
52}
53
54pub(crate) fn yes() -> bool {
55 true
56}
57
58#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
59pub struct Location {
60 #[serde(default)]
61 pub country: Option<String>,
62 #[serde(default)]
63 pub languages: Vec<String>,
64}
65
66#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
68#[serde(rename_all = "camelCase")]
69pub struct ScrapeOptions {
70 #[serde(default = "default_formats")]
71 pub formats: Vec<Format>,
72 #[serde(default)]
73 pub headers: BTreeMap<String, String>,
74 #[serde(default)]
75 pub include_tags: Vec<String>,
76 #[serde(default)]
77 pub exclude_tags: Vec<String>,
78 #[serde(default = "yes")]
79 pub only_main_content: bool,
80 #[serde(default)]
82 pub timeout: Option<u64>,
83 #[serde(default)]
85 pub wait_for: u64,
86 #[serde(default)]
87 pub mobile: bool,
88 #[serde(default)]
89 pub actions: Vec<Value>,
90 #[serde(default)]
91 pub location: Option<Location>,
92 #[serde(default)]
93 pub parsers: Option<Value>,
94 #[serde(default = "yes")]
95 pub remove_base64_images: bool,
96 #[serde(default = "yes")]
97 pub block_ads: bool,
98 #[serde(default)]
100 pub proxy: Option<String>,
101 #[serde(default)]
103 pub egress: Option<String>,
104 #[serde(default)]
106 pub skip_tls_verification: Option<bool>,
107 #[serde(default)]
109 pub max_age: Option<u64>,
110 #[serde(default)]
112 pub min_age: Option<u64>,
113 #[serde(default = "yes")]
114 pub store_in_cache: bool,
115
116 #[serde(default)]
119 pub engine: Option<String>,
120 #[serde(default)]
122 pub discover_scripts: bool,
123 #[serde(default)]
124 pub if_none_match: Option<String>,
125 #[serde(default)]
126 pub if_modified_since: Option<String>,
127 #[serde(default)]
129 pub ignore_robots_txt: bool,
130 #[serde(default)]
131 pub store_snapshot: bool,
132 #[serde(default)]
134 pub query_filter: Option<QueryFilter>,
135 #[serde(default, skip_serializing_if = "Option::is_none")]
137 pub chunking: Option<Value>,
138}
139
140#[derive(Debug, Clone, PartialEq, Serialize)]
142#[serde(rename_all = "camelCase")]
143pub struct QueryFilter {
144 pub query: String,
145 #[serde(skip_serializing_if = "Option::is_none")]
147 pub threshold: Option<f32>,
148 #[serde(skip_serializing_if = "Option::is_none")]
150 pub relative: Option<f32>,
151 #[serde(skip_serializing_if = "Option::is_none")]
153 pub top_k: Option<usize>,
154 #[serde(skip_serializing_if = "Option::is_none")]
156 pub max_tokens: Option<usize>,
157 #[serde(skip_serializing_if = "Option::is_none")]
159 pub min_tokens: Option<usize>,
160 pub headings: bool,
162}
163
164impl<'de> Deserialize<'de> for QueryFilter {
165 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
166 #[derive(Deserialize)]
167 #[serde(rename_all = "camelCase")]
168 struct Full {
169 query: String,
170 #[serde(default)]
171 threshold: Option<f32>,
172 #[serde(default)]
173 relative: Option<f32>,
174 #[serde(default)]
175 top_k: Option<usize>,
176 #[serde(default)]
177 max_tokens: Option<usize>,
178 #[serde(default)]
179 min_tokens: Option<usize>,
180 #[serde(default = "yes")]
181 headings: bool,
182 }
183 #[derive(Deserialize)]
184 #[serde(untagged)]
185 enum Repr {
186 Query(String),
187 Full(Full),
188 }
189 Ok(match Repr::deserialize(d)? {
190 Repr::Query(query) => QueryFilter {
191 query,
192 threshold: None,
193 relative: Some(pagesift_extract_defaults::RELATIVE),
194 top_k: None,
195 max_tokens: None,
196 min_tokens: Some(pagesift_extract_defaults::MIN_TOKENS),
197 headings: true,
198 },
199 Repr::Full(f) => QueryFilter {
200 query: f.query,
201 relative: f.relative.or(if f.threshold.is_none() {
202 Some(pagesift_extract_defaults::RELATIVE)
203 } else {
204 None
205 }),
206 threshold: f.threshold,
207 top_k: f.top_k,
208 max_tokens: f.max_tokens,
209 min_tokens: f.min_tokens.or(if f.threshold.is_none() {
210 Some(pagesift_extract_defaults::MIN_TOKENS)
211 } else {
212 None
213 }),
214 headings: f.headings,
215 },
216 })
217 }
218}
219
220mod pagesift_extract_defaults {
221 pub const RELATIVE: f32 = 0.5;
222 pub const MIN_TOKENS: usize = 1000;
223}
224
225impl Default for ScrapeOptions {
226 fn default() -> Self {
227 serde_json::from_value(Value::Object(Map::new())).expect("defaults deserialize")
228 }
229}
230
231impl ScrapeOptions {
232 pub fn wants(&self, kind: &str) -> bool {
233 self.formats.iter().any(|f| f.kind == kind)
234 }
235}
236
237#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
238pub struct ScrapeRequest {
239 pub url: String,
240 #[serde(flatten)]
241 pub options: ScrapeOptions,
242}
243
244#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
245#[serde(rename_all = "camelCase")]
246pub struct BatchScrapeRequest {
247 pub urls: Vec<String>,
248 #[serde(default = "yes")]
249 pub ignore_invalid_urls: bool,
250 #[serde(default)]
251 pub max_concurrency: Option<usize>,
252 #[serde(default)]
253 pub webhook: Option<Value>,
254 #[serde(flatten)]
255 pub options: ScrapeOptions,
256}
257
258fn default_crawl_limit() -> usize {
259 10_000
260}
261
262fn default_sitemap() -> String {
263 "include".into()
264}
265
266#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
267#[serde(rename_all = "camelCase")]
268pub struct CrawlRequest {
269 pub url: String,
270 #[serde(default)]
271 pub include_paths: Vec<String>,
272 #[serde(default)]
273 pub exclude_paths: Vec<String>,
274 #[serde(default)]
275 pub max_discovery_depth: Option<usize>,
276 #[serde(default, skip_serializing_if = "Option::is_none")]
278 pub max_depth: Option<u64>,
279 #[serde(default = "default_crawl_limit")]
280 pub limit: usize,
281 #[serde(default)]
282 pub crawl_entire_domain: bool,
283 #[serde(default)]
284 pub allow_external_links: bool,
285 #[serde(default)]
287 pub allow_subdomains: Option<bool>,
288 #[serde(default)]
289 pub ignore_robots_txt: bool,
290 #[serde(default = "default_sitemap")]
291 pub sitemap: String,
292 #[serde(default)]
293 pub ignore_query_parameters: bool,
294 #[serde(default)]
296 pub delay: Option<f64>,
297 #[serde(default)]
298 pub max_concurrency: Option<usize>,
299 #[serde(default)]
300 pub scrape_options: ScrapeOptions,
301 #[serde(default)]
302 pub webhook: Option<Value>,
303
304 #[serde(default, deserialize_with = "query_or_keywords")]
306 pub query: Option<String>,
307 #[serde(default, deserialize_with = "adaptive_options")]
308 pub adaptive: Option<AdaptiveOptions>,
309 #[serde(default)]
313 pub scope: Option<CrawlScope>,
314 #[serde(default)]
316 pub attachments: bool,
317 #[serde(default)]
319 pub attachment_limit: Option<usize>,
320}
321
322#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
324#[serde(rename_all = "lowercase")]
325pub enum CrawlScope {
326 #[default]
328 Auto,
329 Exact,
331 Parent,
333 Domain,
335}
336
337fn query_or_keywords<'de, D: Deserializer<'de>>(d: D) -> Result<Option<String>, D::Error> {
338 #[derive(Deserialize)]
339 #[serde(untagged)]
340 enum Repr {
341 Query(String),
342 Keywords(Vec<String>),
343 }
344 Ok(match Option::<Repr>::deserialize(d)? {
345 Some(Repr::Query(q)) => Some(q),
346 Some(Repr::Keywords(k)) => Some(k.join(" ")),
347 None => None,
348 }
349 .filter(|q| !q.trim().is_empty()))
350}
351
352fn adaptive_options<'de, D: Deserializer<'de>>(d: D) -> Result<Option<AdaptiveOptions>, D::Error> {
353 #[derive(Deserialize)]
354 #[serde(untagged)]
355 enum Repr {
356 Flag(bool),
357 Options(AdaptiveOptions),
358 }
359 Ok(match Option::<Repr>::deserialize(d)? {
360 Some(Repr::Flag(true)) => Some(AdaptiveOptions::default()),
361 Some(Repr::Flag(false)) | None => None,
362 Some(Repr::Options(o)) => Some(o),
363 })
364}
365
366#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
368#[serde(rename_all = "camelCase", default)]
369pub struct AdaptiveOptions {
370 pub coverage_threshold: f64,
371 pub evidence_per_term: usize,
372 pub patience: usize,
373 pub min_gain: f64,
374}
375
376impl Default for AdaptiveOptions {
377 fn default() -> Self {
378 Self {
379 coverage_threshold: 0.9,
380 evidence_per_term: 3,
381 patience: 5,
382 min_gain: 0.05,
383 }
384 }
385}
386
387fn default_map_limit() -> usize {
388 5_000
389}
390
391#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
392#[serde(rename_all = "camelCase")]
393pub struct MapRequest {
394 pub url: String,
395 #[serde(default)]
396 pub search: Option<String>,
397 #[serde(default = "yes")]
398 pub include_subdomains: bool,
399 #[serde(default = "yes")]
400 pub ignore_query_parameters: bool,
401 #[serde(default = "default_sitemap")]
402 pub sitemap: String,
403 #[serde(default = "default_map_limit")]
404 pub limit: usize,
405 #[serde(default)]
406 pub ignore_robots_txt: bool,
407 #[serde(default)]
409 pub timeout: Option<u64>,
410}
411
412fn default_search_limit() -> usize {
413 10
414}
415
416#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
418#[serde(rename_all = "camelCase")]
419pub struct SearchRequest {
420 pub query: String,
421 #[serde(default = "default_search_limit")]
422 pub limit: usize,
423 #[serde(default)]
424 pub lang: Option<String>,
425 #[serde(default)]
426 pub country: Option<String>,
427 #[serde(default)]
429 pub tbs: Option<String>,
430 #[serde(default)]
432 pub timeout: Option<u64>,
433 #[serde(default)]
434 pub scrape_options: Option<ScrapeOptions>,
435}
436
437#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
439#[serde(rename_all = "camelCase")]
440pub struct ExtractRequest {
441 pub urls: Vec<String>,
442 #[serde(default)]
443 pub prompt: Option<String>,
444 #[serde(default)]
445 pub schema: Option<Value>,
446 #[serde(default)]
447 pub enable_web_search: bool,
448 #[serde(default)]
449 pub ignore_invalid_urls: bool,
450 #[serde(default)]
451 pub scrape_options: Option<ScrapeOptions>,
452}