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 Engine: Send + Sync + 'static {
18 fn scrape<'a>(
19 &'a self,
20 url: &'a str,
21 opts: &'a ScrapeOptions,
22 ) -> BoxFuture<'a, Result<Document, ApiError>>;
23
24 fn parse<'a>(
27 &'a self,
28 _file: Upload,
29 _opts: &'a ScrapeOptions,
30 ) -> BoxFuture<'a, Result<Document, ApiError>> {
31 Box::pin(async {
32 Err(ApiError::unsupported(
33 "parse is not available in this build",
34 ))
35 })
36 }
37
38 fn map<'a>(&'a self, _req: &'a MapRequest) -> BoxFuture<'a, Result<Vec<MapLink>, ApiError>> {
39 Box::pin(async { Err(ApiError::unsupported("map is not available in this build")) })
40 }
41
42 fn crawl<'a>(
44 &'a self,
45 _req: &'a CrawlRequest,
46 _sink: Arc<dyn CrawlSink>,
47 ) -> BoxFuture<'a, Result<(), ApiError>> {
48 Box::pin(async {
49 Err(ApiError::unsupported(
50 "crawl is not available in this build",
51 ))
52 })
53 }
54
55 fn search<'a>(&'a self, _req: &'a SearchRequest) -> BoxFuture<'a, Result<Value, ApiError>> {
57 Box::pin(async {
58 Err(ApiError::unsupported(
59 "search is not configured: set [search] in pagesift.toml",
60 ))
61 })
62 }
63
64 fn extract<'a>(&'a self, _req: &'a ExtractRequest) -> BoxFuture<'a, Result<Value, ApiError>> {
66 Box::pin(async {
67 Err(ApiError::unsupported(
68 "extract needs an LLM: set [llm] in pagesift.toml",
69 ))
70 })
71 }
72
73 fn agent_check(&self, _req: &AgentRequest) -> Result<(), ApiError> {
75 Err(ApiError::unsupported(
76 "agent needs an LLM: set [llm] in pagesift.toml",
77 ))
78 }
79
80 fn agent<'a>(
82 &'a self,
83 _req: &'a AgentRequest,
84 _sink: Arc<dyn AgentSink>,
85 ) -> BoxFuture<'a, AgentOutcome> {
86 Box::pin(async {
87 AgentOutcome {
88 error: Some(AgentFailure::from(&ApiError::unsupported(
89 "agent needs an LLM: set [llm] in pagesift.toml",
90 ))),
91 ..Default::default()
92 }
93 })
94 }
95
96 fn supported_formats(&self) -> &'static [&'static str] {
98 &["markdown", "html", "rawHtml", "links", "images"]
99 }
100
101 fn supports_actions(&self) -> bool {
103 false
104 }
105
106 fn actions_unavailable(&self) -> &'static str {
108 "actions need browser rendering, which is not available in this build"
109 }
110
111 fn supports_interact(&self) -> bool {
113 false
114 }
115
116 fn scrape_session<'a>(
119 &'a self,
120 _url: &'a str,
121 _opts: &'a ScrapeOptions,
122 _keep: bool,
123 _owner: Option<&'a str>,
124 ) -> BoxFuture<'a, Result<Document, ApiError>> {
125 Box::pin(async {
126 Err(ApiError::unsupported(
127 "keepSession is not available in this build",
128 ))
129 })
130 }
131
132 fn observe<'a>(
134 &'a self,
135 _url: &'a str,
136 _instruction: &'a str,
137 ) -> BoxFuture<'a, Result<crate::types::Observed, ApiError>> {
138 Box::pin(async {
139 Err(ApiError::unsupported(
140 "observe is not available in this build",
141 ))
142 })
143 }
144
145 fn find_similar<'a>(
147 &'a self,
148 _url: &'a str,
149 _selector: &'a str,
150 _opts: &'a ScrapeOptions,
151 ) -> BoxFuture<'a, Result<crate::types::SimilarData, ApiError>> {
152 Box::pin(async {
153 Err(ApiError::unsupported(
154 "find_similar is not available in this build",
155 ))
156 })
157 }
158
159 fn interact<'a>(
160 &'a self,
161 _id: &'a str,
162 _req: &'a InteractRequest,
163 _owner: Option<&'a str>,
164 ) -> BoxFuture<'a, Result<InteractResponse, ApiError>> {
165 Box::pin(async {
166 Err(ApiError::unsupported(
167 "interact is not available in this build",
168 ))
169 })
170 }
171
172 fn stop_interact<'a>(
173 &'a self,
174 _id: &'a str,
175 _owner: Option<&'a str>,
176 ) -> BoxFuture<'a, Result<InteractStopped, ApiError>> {
177 Box::pin(async {
178 Err(ApiError::unsupported(
179 "interact is not available in this build",
180 ))
181 })
182 }
183
184 fn admitted_concurrency(&self) -> Option<usize> {
187 None
188 }
189}
190
191pub trait AgentSink: Send + Sync {
193 fn step<'a>(&'a self, _step: &'a AgentStep, _credits: u64) -> BoxFuture<'a, ()> {
195 Box::pin(async {})
196 }
197 fn cancelled(&self) -> bool {
199 false
200 }
201 fn charge_page(&self) -> BoxFuture<'_, ()> {
203 Box::pin(async {})
204 }
205 fn pages_left(&self) -> Option<u64> {
207 None
208 }
209}
210
211impl AgentSink for () {}
212
213pub trait CrawlSink: Send + Sync {
215 fn page(&self, doc: Document) -> BoxFuture<'_, ()>;
216 fn error<'a>(&'a self, url: &'a str, err: ApiError) -> BoxFuture<'a, ()>;
217 fn total(&self, total: usize) -> BoxFuture<'_, ()>;
219 fn cancelled(&self) -> BoxFuture<'_, bool>;
220
221 fn resume(&self) -> Option<CrawlResume> {
224 None
225 }
226
227 fn tracks_frontier(&self) -> bool {
229 false
230 }
231
232 fn enqueued(&self, _urls: Vec<(String, usize)>) -> BoxFuture<'_, ()> {
234 Box::pin(async {})
235 }
236
237 fn summary(&self, _summary: CrawlSummary) -> BoxFuture<'_, ()> {
239 Box::pin(async {})
240 }
241}
242
243#[derive(Debug, Clone, Default, PartialEq)]
245pub struct CrawlResume {
246 pub enqueued: Vec<String>,
248 pub pending: Vec<(String, usize)>,
250 pub done: usize,
252}