1use std::num::NonZeroUsize;
31use std::time::Duration;
32
33use haem_frames::embedding::{ItemCode, Kind, input_refusal};
34
35use super::{Answer, Embedded, Embedder, Longer};
36use crate::serve::protocol::{Selection, Version};
37use crate::serve::storage::embed_job::{Made, MadeWindows, Page, Reach, Window};
38use crate::serve::storage::embed_pending::{Pending, Waiting};
39use crate::serve::storage::embedded::{Declaration, ModelName};
40use crate::serve::storage::{Failure, Storage};
41
42pub trait Embed {
44 fn documents(&self, texts: &[String]) -> Result<Embedded, Failure>;
50}
51
52impl Embed for Embedder {
53 fn documents(&self, texts: &[String]) -> Result<Embedded, Failure> {
54 self.embed(Kind::Document, texts)
55 }
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub struct Bounds {
61 pub page_records: NonZeroUsize,
63 pub batch: NonZeroUsize,
65 pub input_bytes: NonZeroUsize,
67 pub call: Duration,
69}
70
71#[derive(Debug, Clone, PartialEq, Eq)]
74pub struct LongerThanModel {
75 pub id: String,
76 pub tokens_total: u64,
78 pub tokens_limit: u64,
80}
81
82#[derive(Debug, Clone, PartialEq)]
86pub struct MadePage {
87 pub page: Page,
88 pub longer_than_model: Vec<LongerThanModel>,
89 pub empty_input: Vec<String>,
91 pub too_long: Vec<String>,
93 pub windows_over_cap: Vec<LongerThanModel>,
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub struct Counts {
101 pub made: usize,
102 pub removed: usize,
103 pub no_text: usize,
104 pub too_long: usize,
105 pub longer_than_model: usize,
106 pub empty_input: usize,
108 pub windowed: usize,
110 pub windows_over_cap: usize,
112 pub ended: bool,
114}
115
116#[derive(Debug, Clone)]
118pub enum Step {
119 Idle,
121 Committed(Counts),
123 Failed(Failure),
125}
126
127pub fn make(
140 pending: &Pending,
141 collection: &str,
142 field: &str,
143 declaration: &Declaration,
144 worker: &dyn Embed,
145 bounds: Bounds,
146) -> Result<MadePage, Failure> {
147 let Sorted {
148 texts,
149 removed,
150 empty_input,
151 too_long,
152 } = sorted(pending, bounds.input_bytes);
153 let cleared: Vec<String> = too_long
157 .iter()
158 .chain(&pending.no_text)
159 .chain(&empty_input)
160 .cloned()
161 .collect();
162 let mut collected = Collected {
163 cleared,
164 ..Collected::default()
165 };
166 for chunk in texts.chunks(bounds.batch.get()) {
167 let sent: Vec<String> = chunk.iter().map(|(_, _, text)| (*text).clone()).collect();
168 let answers = checked(worker.documents(&sent)?, sent.len(), declaration)?;
169 for ((id, made_from, _), answer) in chunk.iter().zip(answers) {
170 collected.take(id, *made_from, answer);
171 }
172 }
173 let page = Page {
174 collection: collection.to_owned(),
175 field: field.to_owned(),
176 model: declaration.model.clone(),
177 made: collected.made,
178 windowed: collected.windowed,
179 removed,
180 cleared: collected.cleared,
181 toward: pending.toward,
182 reach: pending.reach.clone(),
183 };
184 Ok(MadePage {
185 page,
186 longer_than_model: collected.longer_than_model,
187 empty_input,
188 too_long,
189 windows_over_cap: collected.windows_over_cap,
190 })
191}
192
193fn checked(
199 embedded: Embedded,
200 sent: usize,
201 declaration: &Declaration,
202) -> Result<Vec<Answer>, Failure> {
203 if embedded.answers.len() != sent {
204 return Err(Failure::new(
205 "embed_vector_count",
206 format!(
207 "{sent} texts were sent and {} vectors came back",
208 embedded.answers.len()
209 ),
210 ));
211 }
212 let dimensions = declaration.dimensions.get();
213 let wrong = embedded
214 .answers
215 .iter()
216 .flat_map(|answer| match answer {
217 Answer::Vector(vector) => vec![vector.len()],
218 Answer::Windows(windows) => windows.iter().map(|window| window.vector.len()).collect(),
219 Answer::Longer(_) | Answer::OverCap(_) => Vec::new(),
220 })
221 .find(|length| *length != dimensions);
222 if let Some(wrong) = wrong {
223 return Err(Failure::new(
224 "embed_vector_length",
225 format!("a vector of {wrong} members came back; {dimensions} are declared"),
226 ));
227 }
228 let model = ModelName {
229 name: embedded.model.name,
230 revision: embedded.model.revision,
231 manifest_sha256: embedded.model.manifest_sha256,
232 };
233 if model != declaration.model {
234 return Err(Failure::new(
235 "embed_model_mismatch",
236 format!(
237 "the worker runs {}; the field is declared for {}",
238 model.name, declaration.model.name
239 ),
240 ));
241 }
242 Ok(embedded.answers)
243}
244
245#[derive(Default)]
248struct Collected {
249 made: Vec<Made>,
250 windowed: Vec<MadeWindows>,
251 cleared: Vec<String>,
252 longer_than_model: Vec<LongerThanModel>,
253 windows_over_cap: Vec<LongerThanModel>,
254}
255
256impl Collected {
257 fn take(&mut self, id: &str, made_from: Version, answer: Answer) {
260 let named = |longer: Longer| LongerThanModel {
261 id: id.to_owned(),
262 tokens_total: longer.tokens_total,
263 tokens_limit: longer.tokens_limit,
264 };
265 match answer {
266 Answer::Vector(vector) => self.made.push(Made {
267 id: id.to_owned(),
268 made_from,
269 vector,
270 }),
271 Answer::Windows(windows) => self.windowed.push(MadeWindows {
272 id: id.to_owned(),
273 made_from,
274 windows: windows
275 .into_iter()
276 .map(|window| Window {
277 start: window.start,
278 end: window.end,
279 vector: window.vector,
280 })
281 .collect(),
282 }),
283 Answer::Longer(longer) => {
284 self.cleared.push(id.to_owned());
285 self.longer_than_model.push(named(longer));
286 }
287 Answer::OverCap(longer) => {
288 self.cleared.push(id.to_owned());
289 self.windows_over_cap.push(named(longer));
290 }
291 }
292 }
293}
294
295struct Sorted<'page> {
298 texts: Vec<(&'page String, Version, &'page String)>,
299 removed: Vec<String>,
300 empty_input: Vec<String>,
301 too_long: Vec<String>,
302}
303
304fn sorted(pending: &Pending, input_bytes: NonZeroUsize) -> Sorted<'_> {
308 let limit = u64::try_from(input_bytes.get()).unwrap_or(u64::MAX);
309 let mut sorted = Sorted {
310 texts: Vec::new(),
311 removed: Vec::new(),
312 empty_input: pending.empty_input.clone(),
313 too_long: pending.too_long.clone(),
314 };
315 for waiting in &pending.waiting {
316 match waiting {
317 Waiting::Text {
318 id,
319 made_from,
320 text,
321 } => match input_refusal(text, limit) {
322 None => sorted.texts.push((id, *made_from, text)),
323 Some(ItemCode::EmbedInputEmpty) => sorted.empty_input.push(id.clone()),
324 Some(_) => {
325 sorted.too_long.push(id.clone());
326 }
327 },
328 Waiting::Removed { id } => sorted.removed.push(id.clone()),
329 }
330 }
331 sorted
332}
333
334pub fn step(
341 storage: &mut Storage,
342 branch: &str,
343 collection: &str,
344 field: &str,
345 worker: &dyn Embed,
346 bounds: Bounds,
347) -> Result<Step, Failure> {
348 let pending = storage.embed_pending(
349 branch,
350 collection,
351 field,
352 bounds.page_records,
353 bounds.input_bytes,
354 )?;
355 let Some(pending) = pending else {
356 return Ok(Step::Idle);
357 };
358 let at = storage.capture(&Selection {
359 branch: branch.to_owned(),
360 version: None,
361 })?;
362 let (declaration, _companion) = storage.embedded_field(&at, collection, field)?;
363 let made = make(&pending, collection, field, &declaration, worker, bounds);
364 let made = match made {
365 Ok(made) => made,
366 Err(failure) => return Ok(Step::Failed(failure)),
367 };
368 let page = &made.page;
369 storage.embed_commit(branch, page, bounds.page_records)?;
370 Ok(Step::Committed(Counts {
371 made: page.made.len(),
372 removed: page.removed.len(),
373 no_text: pending.no_text.len(),
374 too_long: made.too_long.len(),
375 longer_than_model: made
377 .longer_than_model
378 .len()
379 .saturating_add(made.windows_over_cap.len()),
380 empty_input: made.empty_input.len(),
381 windowed: page.windowed.len(),
382 windows_over_cap: made.windows_over_cap.len(),
383 ended: page.reach == Reach::End,
384 }))
385}
386
387#[cfg(test)]
388#[path = "job_tests.rs"]
389mod tests;