1use std::future::Future;
4use std::pin::Pin;
5use std::sync::Arc;
6
7use crate::error::ApiError;
8use crate::types::{
9 AgentFailure, AgentOutcome, AgentRequest, AgentStep, CrawlRequest, CrawlSummary, Document,
10 ExtractRequest, InteractRequest, InteractResponse, InteractStopped, MapLink, MapRequest,
11 ScrapeOptions, SearchRequest, Upload,
12};
13use serde_json::Value;
14
15pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
16
17pub trait PageBudget: Send + Sync {
19 fn acquire(&self) -> BoxFuture<'_, Result<(), ApiError>>;
20}
21
22pub trait Engine: Send + Sync + 'static {
23 fn scrape<'a>(
24 &'a self,
25 url: &'a str,
26 opts: &'a ScrapeOptions,
27 ) -> BoxFuture<'a, Result<Document, ApiError>>;
28
29 fn parse<'a>(
32 &'a self,
33 _file: Upload,
34 _opts: &'a ScrapeOptions,
35 ) -> BoxFuture<'a, Result<Document, ApiError>> {
36 Box::pin(async {
37 Err(ApiError::unsupported(
38 "parse is not available in this build",
39 ))
40 })
41 }
42
43 fn map<'a>(&'a self, _req: &'a MapRequest) -> BoxFuture<'a, Result<Vec<MapLink>, ApiError>> {
44 Box::pin(async { Err(ApiError::unsupported("map is not available in this build")) })
45 }
46
47 fn crawl<'a>(
49 &'a self,
50 _req: &'a CrawlRequest,
51 _sink: Arc<dyn CrawlSink>,
52 ) -> BoxFuture<'a, Result<(), ApiError>> {
53 Box::pin(async {
54 Err(ApiError::unsupported(
55 "crawl is not available in this build",
56 ))
57 })
58 }
59
60 fn search<'a>(&'a self, _req: &'a SearchRequest) -> BoxFuture<'a, Result<Value, ApiError>> {
62 Box::pin(async {
63 Err(ApiError::unsupported(
64 "search is not configured: set [search] in pagesift.toml",
65 ))
66 })
67 }
68
69 fn extract<'a>(&'a self, _req: &'a ExtractRequest) -> BoxFuture<'a, Result<Value, ApiError>> {
71 Box::pin(async {
72 Err(ApiError::unsupported(
73 "extract needs an LLM: set [llm] in pagesift.toml",
74 ))
75 })
76 }
77
78 fn search_budgeted<'a>(
79 &'a self,
80 _req: &'a SearchRequest,
81 _budget: Arc<dyn PageBudget>,
82 ) -> BoxFuture<'a, Result<Value, ApiError>> {
83 Box::pin(async {
84 Err(ApiError::unsupported(
85 "page quotas are not supported by this search engine",
86 ))
87 })
88 }
89
90 fn extract_budgeted<'a>(
91 &'a self,
92 _req: &'a ExtractRequest,
93 _budget: Arc<dyn PageBudget>,
94 ) -> BoxFuture<'a, Result<Value, ApiError>> {
95 Box::pin(async {
96 Err(ApiError::unsupported(
97 "page quotas are not supported by this extract engine",
98 ))
99 })
100 }
101
102 fn agent_check(&self, _req: &AgentRequest) -> Result<(), ApiError> {
104 Err(ApiError::unsupported(
105 "agent needs an LLM: set [llm] in pagesift.toml",
106 ))
107 }
108
109 fn agent<'a>(
111 &'a self,
112 _req: &'a AgentRequest,
113 _sink: Arc<dyn AgentSink>,
114 ) -> BoxFuture<'a, AgentOutcome> {
115 Box::pin(async {
116 AgentOutcome {
117 error: Some(AgentFailure::from(&ApiError::unsupported(
118 "agent needs an LLM: set [llm] in pagesift.toml",
119 ))),
120 ..Default::default()
121 }
122 })
123 }
124
125 fn supported_formats(&self) -> &'static [&'static str] {
127 &["markdown", "html", "rawHtml", "links", "images"]
128 }
129
130 fn supports_actions(&self) -> bool {
132 false
133 }
134
135 fn actions_unavailable(&self) -> &'static str {
137 "actions need browser rendering, which is not available in this build"
138 }
139
140 fn supports_interact(&self) -> bool {
142 false
143 }
144
145 fn scrape_session<'a>(
148 &'a self,
149 _url: &'a str,
150 _opts: &'a ScrapeOptions,
151 _keep: bool,
152 _owner: Option<&'a str>,
153 ) -> BoxFuture<'a, Result<Document, ApiError>> {
154 Box::pin(async {
155 Err(ApiError::unsupported(
156 "keepSession is not available in this build",
157 ))
158 })
159 }
160
161 fn observe<'a>(
163 &'a self,
164 _url: &'a str,
165 _instruction: &'a str,
166 ) -> BoxFuture<'a, Result<crate::types::Observed, ApiError>> {
167 Box::pin(async {
168 Err(ApiError::unsupported(
169 "observe is not available in this build",
170 ))
171 })
172 }
173
174 fn find_similar<'a>(
176 &'a self,
177 _url: &'a str,
178 _selector: &'a str,
179 _opts: &'a ScrapeOptions,
180 ) -> BoxFuture<'a, Result<crate::types::SimilarData, ApiError>> {
181 Box::pin(async {
182 Err(ApiError::unsupported(
183 "find_similar is not available in this build",
184 ))
185 })
186 }
187
188 fn interact<'a>(
189 &'a self,
190 _id: &'a str,
191 _req: &'a InteractRequest,
192 _owner: Option<&'a str>,
193 ) -> BoxFuture<'a, Result<InteractResponse, ApiError>> {
194 Box::pin(async {
195 Err(ApiError::unsupported(
196 "interact is not available in this build",
197 ))
198 })
199 }
200
201 fn stop_interact<'a>(
202 &'a self,
203 _id: &'a str,
204 _owner: Option<&'a str>,
205 ) -> BoxFuture<'a, Result<InteractStopped, ApiError>> {
206 Box::pin(async {
207 Err(ApiError::unsupported(
208 "interact is not available in this build",
209 ))
210 })
211 }
212
213 fn admitted_concurrency(&self) -> Option<usize> {
216 None
217 }
218}
219
220pub trait AgentSink: Send + Sync {
222 fn step<'a>(&'a self, _step: &'a AgentStep, _credits: u64) -> BoxFuture<'a, ()> {
224 Box::pin(async {})
225 }
226 fn cancelled(&self) -> bool {
228 false
229 }
230 fn charge_page(&self) -> BoxFuture<'_, ()> {
232 Box::pin(async {})
233 }
234 fn pages_left(&self) -> Option<u64> {
236 None
237 }
238}
239
240impl AgentSink for () {}
241
242pub trait CrawlSink: Send + Sync {
244 fn page(&self, doc: Document) -> BoxFuture<'_, ()>;
245 fn error<'a>(&'a self, url: &'a str, err: ApiError) -> BoxFuture<'a, ()>;
246 fn total(&self, total: usize) -> BoxFuture<'_, ()>;
248 fn cancelled(&self) -> BoxFuture<'_, bool>;
249
250 fn resume(&self) -> Option<CrawlResume> {
253 None
254 }
255
256 fn tracks_frontier(&self) -> bool {
258 false
259 }
260
261 fn enqueued(&self, _urls: Vec<(String, usize)>) -> BoxFuture<'_, ()> {
263 Box::pin(async {})
264 }
265
266 fn summary(&self, _summary: CrawlSummary) -> BoxFuture<'_, ()> {
268 Box::pin(async {})
269 }
270}
271
272#[derive(Debug, Clone, Default, PartialEq)]
274pub struct CrawlResume {
275 pub enqueued: Vec<String>,
277 pub pending: Vec<(String, usize)>,
279 pub done: usize,
281}