1use std::collections::BTreeMap;
7
8use serde::{Deserialize, Deserializer, Serialize};
9use serde_json::{Map, Value};
10
11#[derive(Debug, Clone, PartialEq, Serialize)]
13pub struct Format {
14 #[serde(rename = "type")]
15 pub kind: String,
16 #[serde(flatten)]
17 pub options: Map<String, Value>,
18}
19
20impl Format {
21 pub fn new(kind: &str) -> Self {
22 Self {
23 kind: kind.to_string(),
24 options: Map::new(),
25 }
26 }
27}
28
29impl<'de> Deserialize<'de> for Format {
30 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
31 match Value::deserialize(d)? {
32 Value::String(kind) => Ok(Format::new(&kind)),
33 Value::Object(mut map) => {
34 let kind = match map.remove("type") {
35 Some(Value::String(k)) => k,
36 _ => {
37 return Err(serde::de::Error::custom(
38 "format object needs a string `type`",
39 ));
40 }
41 };
42 Ok(Format { kind, options: map })
43 }
44 _ => Err(serde::de::Error::custom(
45 "format must be a string or an object",
46 )),
47 }
48 }
49}
50
51pub(crate) fn default_formats() -> Vec<Format> {
52 vec![Format::new("markdown")]
53}
54
55pub(crate) fn yes() -> bool {
56 true
57}
58
59#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
60pub struct Location {
61 #[serde(default)]
62 pub country: Option<String>,
63 #[serde(default)]
64 pub languages: Vec<String>,
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
69#[serde(rename_all = "lowercase")]
70pub enum ImageAssetMode {
71 Attach,
72 Embed,
73 Rewrite,
74}
75
76#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
78#[serde(rename_all = "camelCase")]
79pub struct ScrapeOptions {
80 #[serde(default = "default_formats")]
81 pub formats: Vec<Format>,
82 #[serde(default)]
83 pub headers: BTreeMap<String, String>,
84 #[serde(default)]
85 pub include_tags: Vec<String>,
86 #[serde(default)]
87 pub exclude_tags: Vec<String>,
88 #[serde(default = "yes")]
89 pub only_main_content: bool,
90 #[serde(default)]
92 pub timeout: Option<u64>,
93 #[serde(default)]
95 pub wait_for: u64,
96 #[serde(default)]
97 pub mobile: bool,
98 #[serde(default)]
99 pub actions: Vec<Value>,
100 #[serde(default)]
101 pub location: Option<Location>,
102 #[serde(default)]
103 pub parsers: Option<Value>,
104 #[serde(default = "yes")]
105 pub remove_base64_images: bool,
106 #[serde(default = "yes")]
107 pub block_ads: bool,
108 #[serde(default)]
110 pub proxy: Option<String>,
111 #[serde(default)]
113 pub egress: Option<String>,
114 #[serde(default)]
116 pub skip_tls_verification: Option<bool>,
117 #[serde(default)]
119 pub max_age: Option<u64>,
120 #[serde(default)]
122 pub min_age: Option<u64>,
123 #[serde(default = "yes")]
124 pub store_in_cache: bool,
125
126 #[serde(default)]
129 pub engine: Option<String>,
130 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
134 pub keep_session: bool,
135 #[serde(default)]
137 pub discover_scripts: bool,
138 #[serde(skip)]
141 pub owner: Option<String>,
142 #[serde(default)]
145 pub explain: bool,
146 #[serde(default)]
149 pub export_images: bool,
150 #[serde(default, skip_serializing_if = "Option::is_none")]
152 pub image_assets: Option<ImageAssetMode>,
153 #[serde(skip)]
155 pub asset_fetch_cache: Option<super::AssetFetchCache>,
156 #[serde(skip)]
158 pub markdown_progress: Option<super::MarkdownProgress>,
159 #[serde(default)]
160 pub if_none_match: Option<String>,
161 #[serde(default)]
162 pub if_modified_since: Option<String>,
163 #[serde(default)]
165 pub ignore_robots_txt: bool,
166 #[serde(default)]
167 pub store_snapshot: bool,
168 #[serde(default)]
170 pub query_filter: Option<QueryFilter>,
171 #[serde(default, skip_serializing_if = "Option::is_none")]
173 pub chunking: Option<Value>,
174 #[serde(default, skip_serializing_if = "Option::is_none")]
177 pub respond_timing: Option<String>,
178}
179
180pub const RESPOND_TIMINGS: &[&str] = &[
182 "html",
183 "visible-content",
184 "mutation-idle",
185 "resource-idle",
186 "media-idle",
187 "network-idle",
188];
189
190#[derive(Debug, Clone, PartialEq, Serialize)]
192#[serde(rename_all = "camelCase")]
193pub struct QueryFilter {
194 pub query: String,
195 #[serde(skip_serializing_if = "Option::is_none")]
197 pub threshold: Option<f32>,
198 #[serde(skip_serializing_if = "Option::is_none")]
200 pub relative: Option<f32>,
201 #[serde(skip_serializing_if = "Option::is_none")]
203 pub top_k: Option<usize>,
204 #[serde(skip_serializing_if = "Option::is_none")]
206 pub max_tokens: Option<usize>,
207 #[serde(skip_serializing_if = "Option::is_none")]
209 pub min_tokens: Option<usize>,
210 pub headings: bool,
212}
213
214impl<'de> Deserialize<'de> for QueryFilter {
215 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
216 #[derive(Deserialize)]
217 #[serde(rename_all = "camelCase")]
218 struct Full {
219 query: String,
220 #[serde(default)]
221 threshold: Option<f32>,
222 #[serde(default)]
223 relative: Option<f32>,
224 #[serde(default)]
225 top_k: Option<usize>,
226 #[serde(default)]
227 max_tokens: Option<usize>,
228 #[serde(default)]
229 min_tokens: Option<usize>,
230 #[serde(default = "yes")]
231 headings: bool,
232 }
233 #[derive(Deserialize)]
234 #[serde(untagged)]
235 enum Repr {
236 Query(String),
237 Full(Full),
238 }
239 Ok(match Repr::deserialize(d)? {
240 Repr::Query(query) => QueryFilter {
241 query,
242 threshold: None,
243 relative: Some(pagesift_extract_defaults::RELATIVE),
244 top_k: None,
245 max_tokens: None,
246 min_tokens: Some(pagesift_extract_defaults::MIN_TOKENS),
247 headings: true,
248 },
249 Repr::Full(f) => QueryFilter {
250 query: f.query,
251 relative: f.relative.or(if f.threshold.is_none() {
252 Some(pagesift_extract_defaults::RELATIVE)
253 } else {
254 None
255 }),
256 threshold: f.threshold,
257 top_k: f.top_k,
258 max_tokens: f.max_tokens,
259 min_tokens: f.min_tokens.or(if f.threshold.is_none() {
260 Some(pagesift_extract_defaults::MIN_TOKENS)
261 } else {
262 None
263 }),
264 headings: f.headings,
265 },
266 })
267 }
268}
269
270mod pagesift_extract_defaults {
271 pub const RELATIVE: f32 = 0.5;
272 pub const MIN_TOKENS: usize = 1000;
273}
274
275impl Default for ScrapeOptions {
276 fn default() -> Self {
277 serde_json::from_value(Value::Object(Map::new())).expect("defaults deserialize")
278 }
279}
280
281impl ScrapeOptions {
282 pub fn wants(&self, kind: &str) -> bool {
283 self.formats.iter().any(|f| f.kind == kind)
284 }
285}
286
287#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
288pub struct ScrapeRequest {
289 pub url: String,
290 #[serde(flatten)]
291 pub options: ScrapeOptions,
292}
293
294#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
295#[serde(rename_all = "camelCase")]
296pub struct BatchScrapeRequest {
297 pub urls: Vec<String>,
298 #[serde(default = "yes")]
299 pub ignore_invalid_urls: bool,
300 #[serde(default)]
301 pub max_concurrency: Option<usize>,
302 #[serde(default)]
303 pub webhook: Option<Value>,
304 #[serde(flatten)]
305 pub options: ScrapeOptions,
306}
307
308impl BatchScrapeRequest {
309 pub const OWN_FIELDS: &'static [&'static str] =
312 &["urls", "ignoreInvalidUrls", "maxConcurrency", "webhook"];
313}
314
315fn default_crawl_limit() -> usize {
316 10_000
317}
318
319fn default_sitemap() -> String {
320 "include".into()
321}
322
323#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
324#[serde(rename_all = "camelCase")]
325pub struct CrawlRequest {
326 pub url: String,
327 #[serde(default)]
328 pub include_paths: Vec<String>,
329 #[serde(default)]
330 pub exclude_paths: Vec<String>,
331 #[serde(default)]
332 pub max_discovery_depth: Option<usize>,
333 #[serde(default, skip_serializing_if = "Option::is_none")]
335 pub max_depth: Option<u64>,
336 #[serde(default = "default_crawl_limit")]
337 pub limit: usize,
338 #[serde(default)]
339 pub crawl_entire_domain: bool,
340 #[serde(default)]
341 pub allow_external_links: bool,
342 #[serde(default)]
344 pub allow_subdomains: Option<bool>,
345 #[serde(default)]
346 pub ignore_robots_txt: bool,
347 #[serde(default = "default_sitemap")]
348 pub sitemap: String,
349 #[serde(default)]
350 pub ignore_query_parameters: bool,
351 #[serde(default)]
353 pub delay: Option<f64>,
354 #[serde(default)]
355 pub max_concurrency: Option<usize>,
356 #[serde(default)]
357 pub scrape_options: ScrapeOptions,
358 #[serde(default)]
359 pub webhook: Option<Value>,
360
361 #[serde(default, deserialize_with = "query_or_keywords")]
363 pub query: Option<String>,
364 #[serde(default, deserialize_with = "adaptive_options")]
365 pub adaptive: Option<AdaptiveOptions>,
366 #[serde(default)]
370 pub scope: Option<CrawlScope>,
371 #[serde(default)]
373 pub attachments: bool,
374 #[serde(default)]
376 pub attachment_limit: Option<usize>,
377}
378
379#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
381#[serde(rename_all = "lowercase")]
382pub enum CrawlScope {
383 #[default]
385 Auto,
386 Exact,
388 Parent,
390 Domain,
392}
393
394fn query_or_keywords<'de, D: Deserializer<'de>>(d: D) -> Result<Option<String>, D::Error> {
395 #[derive(Deserialize)]
396 #[serde(untagged)]
397 enum Repr {
398 Query(String),
399 Keywords(Vec<String>),
400 }
401 Ok(match Option::<Repr>::deserialize(d)? {
402 Some(Repr::Query(q)) => Some(q),
403 Some(Repr::Keywords(k)) => Some(k.join(" ")),
404 None => None,
405 }
406 .filter(|q| !q.trim().is_empty()))
407}
408
409fn adaptive_options<'de, D: Deserializer<'de>>(d: D) -> Result<Option<AdaptiveOptions>, D::Error> {
410 #[derive(Deserialize)]
411 #[serde(untagged)]
412 enum Repr {
413 Flag(bool),
414 Options(AdaptiveOptions),
415 }
416 Ok(match Option::<Repr>::deserialize(d)? {
417 Some(Repr::Flag(true)) => Some(AdaptiveOptions::default()),
418 Some(Repr::Flag(false)) | None => None,
419 Some(Repr::Options(o)) => Some(o),
420 })
421}
422
423#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
426#[serde(rename_all = "camelCase", default)]
427pub struct AdaptiveOptions {
428 pub coverage_threshold: f64,
432 pub evidence_per_term: usize,
433 pub patience: usize,
437 pub min_gain: f64,
440}
441
442impl Default for AdaptiveOptions {
443 fn default() -> Self {
444 Self {
446 coverage_threshold: 0.9,
448 evidence_per_term: 3,
450 patience: 5,
452 min_gain: 0.05,
455 }
456 }
457}
458
459fn default_map_limit() -> usize {
460 5_000
461}
462
463#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
464#[serde(rename_all = "camelCase")]
465pub struct MapRequest {
466 pub url: String,
467 #[serde(default)]
468 pub search: Option<String>,
469 #[serde(default = "yes")]
470 pub include_subdomains: bool,
471 #[serde(default = "yes")]
472 pub ignore_query_parameters: bool,
473 #[serde(default = "default_sitemap")]
474 pub sitemap: String,
475 #[serde(default = "default_map_limit")]
476 pub limit: usize,
477 #[serde(default)]
478 pub ignore_robots_txt: bool,
479 #[serde(default)]
481 pub timeout: Option<u64>,
482}
483
484fn default_search_limit() -> usize {
485 10
486}
487
488#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
490#[serde(rename_all = "camelCase")]
491pub struct SearchRequest {
492 pub query: String,
493 #[serde(default = "default_search_limit")]
494 pub limit: usize,
495 #[serde(default)]
496 pub lang: Option<String>,
497 #[serde(default)]
498 pub country: Option<String>,
499 #[serde(default)]
501 pub tbs: Option<String>,
502 #[serde(default)]
504 pub timeout: Option<u64>,
505 #[serde(default)]
506 pub scrape_options: Option<ScrapeOptions>,
507}
508
509#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
511#[serde(rename_all = "camelCase")]
512pub struct ExtractRequest {
513 pub urls: Vec<String>,
514 #[serde(default)]
515 pub prompt: Option<String>,
516 #[serde(default)]
517 pub schema: Option<Value>,
518 #[serde(default)]
519 pub enable_web_search: bool,
520 #[serde(default)]
521 pub ignore_invalid_urls: bool,
522 #[serde(default)]
523 pub scrape_options: Option<ScrapeOptions>,
524}