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, PartialEq, Serialize, Deserialize)]
69#[serde(rename_all = "camelCase")]
70pub struct ScrapeOptions {
71 #[serde(default = "default_formats")]
72 pub formats: Vec<Format>,
73 #[serde(default)]
74 pub headers: BTreeMap<String, String>,
75 #[serde(default)]
76 pub include_tags: Vec<String>,
77 #[serde(default)]
78 pub exclude_tags: Vec<String>,
79 #[serde(default = "yes")]
80 pub only_main_content: bool,
81 #[serde(default)]
83 pub timeout: Option<u64>,
84 #[serde(default)]
86 pub wait_for: u64,
87 #[serde(default)]
88 pub mobile: bool,
89 #[serde(default)]
90 pub actions: Vec<Value>,
91 #[serde(default)]
92 pub location: Option<Location>,
93 #[serde(default)]
94 pub parsers: Option<Value>,
95 #[serde(default = "yes")]
96 pub remove_base64_images: bool,
97 #[serde(default = "yes")]
98 pub block_ads: bool,
99 #[serde(default)]
101 pub proxy: Option<String>,
102 #[serde(default)]
104 pub egress: Option<String>,
105 #[serde(default)]
107 pub skip_tls_verification: Option<bool>,
108 #[serde(default)]
110 pub max_age: Option<u64>,
111 #[serde(default)]
113 pub min_age: Option<u64>,
114 #[serde(default = "yes")]
115 pub store_in_cache: bool,
116
117 #[serde(default)]
120 pub engine: Option<String>,
121 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
125 pub keep_session: bool,
126 #[serde(default)]
128 pub discover_scripts: bool,
129 #[serde(skip)]
132 pub owner: Option<String>,
133 #[serde(default)]
136 pub explain: bool,
137 #[serde(default)]
140 pub export_images: bool,
141 #[serde(default)]
142 pub if_none_match: Option<String>,
143 #[serde(default)]
144 pub if_modified_since: Option<String>,
145 #[serde(default)]
147 pub ignore_robots_txt: bool,
148 #[serde(default)]
149 pub store_snapshot: bool,
150 #[serde(default)]
152 pub query_filter: Option<QueryFilter>,
153 #[serde(default, skip_serializing_if = "Option::is_none")]
155 pub chunking: Option<Value>,
156 #[serde(default, skip_serializing_if = "Option::is_none")]
159 pub respond_timing: Option<String>,
160}
161
162pub const RESPOND_TIMINGS: &[&str] = &[
164 "html",
165 "visible-content",
166 "mutation-idle",
167 "resource-idle",
168 "media-idle",
169 "network-idle",
170];
171
172#[derive(Debug, Clone, PartialEq, Serialize)]
174#[serde(rename_all = "camelCase")]
175pub struct QueryFilter {
176 pub query: String,
177 #[serde(skip_serializing_if = "Option::is_none")]
179 pub threshold: Option<f32>,
180 #[serde(skip_serializing_if = "Option::is_none")]
182 pub relative: Option<f32>,
183 #[serde(skip_serializing_if = "Option::is_none")]
185 pub top_k: Option<usize>,
186 #[serde(skip_serializing_if = "Option::is_none")]
188 pub max_tokens: Option<usize>,
189 #[serde(skip_serializing_if = "Option::is_none")]
191 pub min_tokens: Option<usize>,
192 pub headings: bool,
194}
195
196impl<'de> Deserialize<'de> for QueryFilter {
197 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
198 #[derive(Deserialize)]
199 #[serde(rename_all = "camelCase")]
200 struct Full {
201 query: String,
202 #[serde(default)]
203 threshold: Option<f32>,
204 #[serde(default)]
205 relative: Option<f32>,
206 #[serde(default)]
207 top_k: Option<usize>,
208 #[serde(default)]
209 max_tokens: Option<usize>,
210 #[serde(default)]
211 min_tokens: Option<usize>,
212 #[serde(default = "yes")]
213 headings: bool,
214 }
215 #[derive(Deserialize)]
216 #[serde(untagged)]
217 enum Repr {
218 Query(String),
219 Full(Full),
220 }
221 Ok(match Repr::deserialize(d)? {
222 Repr::Query(query) => QueryFilter {
223 query,
224 threshold: None,
225 relative: Some(pagesift_extract_defaults::RELATIVE),
226 top_k: None,
227 max_tokens: None,
228 min_tokens: Some(pagesift_extract_defaults::MIN_TOKENS),
229 headings: true,
230 },
231 Repr::Full(f) => QueryFilter {
232 query: f.query,
233 relative: f.relative.or(if f.threshold.is_none() {
234 Some(pagesift_extract_defaults::RELATIVE)
235 } else {
236 None
237 }),
238 threshold: f.threshold,
239 top_k: f.top_k,
240 max_tokens: f.max_tokens,
241 min_tokens: f.min_tokens.or(if f.threshold.is_none() {
242 Some(pagesift_extract_defaults::MIN_TOKENS)
243 } else {
244 None
245 }),
246 headings: f.headings,
247 },
248 })
249 }
250}
251
252mod pagesift_extract_defaults {
253 pub const RELATIVE: f32 = 0.5;
254 pub const MIN_TOKENS: usize = 1000;
255}
256
257impl Default for ScrapeOptions {
258 fn default() -> Self {
259 serde_json::from_value(Value::Object(Map::new())).expect("defaults deserialize")
260 }
261}
262
263impl ScrapeOptions {
264 pub fn wants(&self, kind: &str) -> bool {
265 self.formats.iter().any(|f| f.kind == kind)
266 }
267}
268
269#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
270pub struct ScrapeRequest {
271 pub url: String,
272 #[serde(flatten)]
273 pub options: ScrapeOptions,
274}
275
276#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
277#[serde(rename_all = "camelCase")]
278pub struct BatchScrapeRequest {
279 pub urls: Vec<String>,
280 #[serde(default = "yes")]
281 pub ignore_invalid_urls: bool,
282 #[serde(default)]
283 pub max_concurrency: Option<usize>,
284 #[serde(default)]
285 pub webhook: Option<Value>,
286 #[serde(flatten)]
287 pub options: ScrapeOptions,
288}
289
290impl BatchScrapeRequest {
291 pub const OWN_FIELDS: &'static [&'static str] =
294 &["urls", "ignoreInvalidUrls", "maxConcurrency", "webhook"];
295}
296
297fn default_crawl_limit() -> usize {
298 10_000
299}
300
301fn default_sitemap() -> String {
302 "include".into()
303}
304
305#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
306#[serde(rename_all = "camelCase")]
307pub struct CrawlRequest {
308 pub url: String,
309 #[serde(default)]
310 pub include_paths: Vec<String>,
311 #[serde(default)]
312 pub exclude_paths: Vec<String>,
313 #[serde(default)]
314 pub max_discovery_depth: Option<usize>,
315 #[serde(default, skip_serializing_if = "Option::is_none")]
317 pub max_depth: Option<u64>,
318 #[serde(default = "default_crawl_limit")]
319 pub limit: usize,
320 #[serde(default)]
321 pub crawl_entire_domain: bool,
322 #[serde(default)]
323 pub allow_external_links: bool,
324 #[serde(default)]
326 pub allow_subdomains: Option<bool>,
327 #[serde(default)]
328 pub ignore_robots_txt: bool,
329 #[serde(default = "default_sitemap")]
330 pub sitemap: String,
331 #[serde(default)]
332 pub ignore_query_parameters: bool,
333 #[serde(default)]
335 pub delay: Option<f64>,
336 #[serde(default)]
337 pub max_concurrency: Option<usize>,
338 #[serde(default)]
339 pub scrape_options: ScrapeOptions,
340 #[serde(default)]
341 pub webhook: Option<Value>,
342
343 #[serde(default, deserialize_with = "query_or_keywords")]
345 pub query: Option<String>,
346 #[serde(default, deserialize_with = "adaptive_options")]
347 pub adaptive: Option<AdaptiveOptions>,
348 #[serde(default)]
352 pub scope: Option<CrawlScope>,
353 #[serde(default)]
355 pub attachments: bool,
356 #[serde(default)]
358 pub attachment_limit: Option<usize>,
359}
360
361#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
363#[serde(rename_all = "lowercase")]
364pub enum CrawlScope {
365 #[default]
367 Auto,
368 Exact,
370 Parent,
372 Domain,
374}
375
376fn query_or_keywords<'de, D: Deserializer<'de>>(d: D) -> Result<Option<String>, D::Error> {
377 #[derive(Deserialize)]
378 #[serde(untagged)]
379 enum Repr {
380 Query(String),
381 Keywords(Vec<String>),
382 }
383 Ok(match Option::<Repr>::deserialize(d)? {
384 Some(Repr::Query(q)) => Some(q),
385 Some(Repr::Keywords(k)) => Some(k.join(" ")),
386 None => None,
387 }
388 .filter(|q| !q.trim().is_empty()))
389}
390
391fn adaptive_options<'de, D: Deserializer<'de>>(d: D) -> Result<Option<AdaptiveOptions>, D::Error> {
392 #[derive(Deserialize)]
393 #[serde(untagged)]
394 enum Repr {
395 Flag(bool),
396 Options(AdaptiveOptions),
397 }
398 Ok(match Option::<Repr>::deserialize(d)? {
399 Some(Repr::Flag(true)) => Some(AdaptiveOptions::default()),
400 Some(Repr::Flag(false)) | None => None,
401 Some(Repr::Options(o)) => Some(o),
402 })
403}
404
405#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
408#[serde(rename_all = "camelCase", default)]
409pub struct AdaptiveOptions {
410 pub coverage_threshold: f64,
414 pub evidence_per_term: usize,
415 pub patience: usize,
419 pub min_gain: f64,
422}
423
424impl Default for AdaptiveOptions {
425 fn default() -> Self {
426 Self {
428 coverage_threshold: 0.9,
430 evidence_per_term: 3,
432 patience: 5,
434 min_gain: 0.05,
437 }
438 }
439}
440
441fn default_map_limit() -> usize {
442 5_000
443}
444
445#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
446#[serde(rename_all = "camelCase")]
447pub struct MapRequest {
448 pub url: String,
449 #[serde(default)]
450 pub search: Option<String>,
451 #[serde(default = "yes")]
452 pub include_subdomains: bool,
453 #[serde(default = "yes")]
454 pub ignore_query_parameters: bool,
455 #[serde(default = "default_sitemap")]
456 pub sitemap: String,
457 #[serde(default = "default_map_limit")]
458 pub limit: usize,
459 #[serde(default)]
460 pub ignore_robots_txt: bool,
461 #[serde(default)]
463 pub timeout: Option<u64>,
464}
465
466fn default_search_limit() -> usize {
467 10
468}
469
470#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
472#[serde(rename_all = "camelCase")]
473pub struct SearchRequest {
474 pub query: String,
475 #[serde(default = "default_search_limit")]
476 pub limit: usize,
477 #[serde(default)]
478 pub lang: Option<String>,
479 #[serde(default)]
480 pub country: Option<String>,
481 #[serde(default)]
483 pub tbs: Option<String>,
484 #[serde(default)]
486 pub timeout: Option<u64>,
487 #[serde(default)]
488 pub scrape_options: Option<ScrapeOptions>,
489}
490
491#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
493#[serde(rename_all = "camelCase")]
494pub struct ExtractRequest {
495 pub urls: Vec<String>,
496 #[serde(default)]
497 pub prompt: Option<String>,
498 #[serde(default)]
499 pub schema: Option<Value>,
500 #[serde(default)]
501 pub enable_web_search: bool,
502 #[serde(default)]
503 pub ignore_invalid_urls: bool,
504 #[serde(default)]
505 pub scrape_options: Option<ScrapeOptions>,
506}