use std::num::NonZeroUsize;
use std::time::Duration;
use haem_frames::embedding::{ItemCode, Kind, input_refusal};
use super::{Answer, Embedded, Embedder, Longer};
use crate::serve::protocol::{Selection, Version};
use crate::serve::storage::embed_job::{Made, MadeWindows, Page, Reach, Window};
use crate::serve::storage::embed_pending::{Pending, Waiting};
use crate::serve::storage::embedded::{Declaration, ModelName};
use crate::serve::storage::{Failure, Storage};
pub trait Embed {
fn documents(&self, texts: &[String]) -> Result<Embedded, Failure>;
}
impl Embed for Embedder {
fn documents(&self, texts: &[String]) -> Result<Embedded, Failure> {
self.embed(Kind::Document, texts)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Bounds {
pub page_records: NonZeroUsize,
pub batch: NonZeroUsize,
pub input_bytes: NonZeroUsize,
pub call: Duration,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LongerThanModel {
pub id: String,
pub tokens_total: u64,
pub tokens_limit: u64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct MadePage {
pub page: Page,
pub longer_than_model: Vec<LongerThanModel>,
pub empty_input: Vec<String>,
pub too_long: Vec<String>,
pub windows_over_cap: Vec<LongerThanModel>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Counts {
pub made: usize,
pub removed: usize,
pub no_text: usize,
pub too_long: usize,
pub longer_than_model: usize,
pub empty_input: usize,
pub windowed: usize,
pub windows_over_cap: usize,
pub ended: bool,
}
#[derive(Debug, Clone)]
pub enum Step {
Idle,
Committed(Counts),
Failed(Failure),
}
pub fn make(
pending: &Pending,
collection: &str,
field: &str,
declaration: &Declaration,
worker: &dyn Embed,
bounds: Bounds,
) -> Result<MadePage, Failure> {
let Sorted {
texts,
removed,
empty_input,
too_long,
} = sorted(pending, bounds.input_bytes);
let cleared: Vec<String> = too_long
.iter()
.chain(&pending.no_text)
.chain(&empty_input)
.cloned()
.collect();
let mut collected = Collected {
cleared,
..Collected::default()
};
for chunk in texts.chunks(bounds.batch.get()) {
let sent: Vec<String> = chunk.iter().map(|(_, _, text)| (*text).clone()).collect();
let answers = checked(worker.documents(&sent)?, sent.len(), declaration)?;
for ((id, made_from, _), answer) in chunk.iter().zip(answers) {
collected.take(id, *made_from, answer);
}
}
let page = Page {
collection: collection.to_owned(),
field: field.to_owned(),
model: declaration.model.clone(),
made: collected.made,
windowed: collected.windowed,
removed,
cleared: collected.cleared,
toward: pending.toward,
reach: pending.reach.clone(),
};
Ok(MadePage {
page,
longer_than_model: collected.longer_than_model,
empty_input,
too_long,
windows_over_cap: collected.windows_over_cap,
})
}
fn checked(
embedded: Embedded,
sent: usize,
declaration: &Declaration,
) -> Result<Vec<Answer>, Failure> {
if embedded.answers.len() != sent {
return Err(Failure::new(
"embed_vector_count",
format!(
"{sent} texts were sent and {} vectors came back",
embedded.answers.len()
),
));
}
let dimensions = declaration.dimensions.get();
let wrong = embedded
.answers
.iter()
.flat_map(|answer| match answer {
Answer::Vector(vector) => vec![vector.len()],
Answer::Windows(windows) => windows.iter().map(|window| window.vector.len()).collect(),
Answer::Longer(_) | Answer::OverCap(_) => Vec::new(),
})
.find(|length| *length != dimensions);
if let Some(wrong) = wrong {
return Err(Failure::new(
"embed_vector_length",
format!("a vector of {wrong} members came back; {dimensions} are declared"),
));
}
let model = ModelName {
name: embedded.model.name,
revision: embedded.model.revision,
manifest_sha256: embedded.model.manifest_sha256,
};
if model != declaration.model {
return Err(Failure::new(
"embed_model_mismatch",
format!(
"the worker runs {}; the field is declared for {}",
model.name, declaration.model.name
),
));
}
Ok(embedded.answers)
}
#[derive(Default)]
struct Collected {
made: Vec<Made>,
windowed: Vec<MadeWindows>,
cleared: Vec<String>,
longer_than_model: Vec<LongerThanModel>,
windows_over_cap: Vec<LongerThanModel>,
}
impl Collected {
fn take(&mut self, id: &str, made_from: Version, answer: Answer) {
let named = |longer: Longer| LongerThanModel {
id: id.to_owned(),
tokens_total: longer.tokens_total,
tokens_limit: longer.tokens_limit,
};
match answer {
Answer::Vector(vector) => self.made.push(Made {
id: id.to_owned(),
made_from,
vector,
}),
Answer::Windows(windows) => self.windowed.push(MadeWindows {
id: id.to_owned(),
made_from,
windows: windows
.into_iter()
.map(|window| Window {
start: window.start,
end: window.end,
vector: window.vector,
})
.collect(),
}),
Answer::Longer(longer) => {
self.cleared.push(id.to_owned());
self.longer_than_model.push(named(longer));
}
Answer::OverCap(longer) => {
self.cleared.push(id.to_owned());
self.windows_over_cap.push(named(longer));
}
}
}
}
struct Sorted<'page> {
texts: Vec<(&'page String, Version, &'page String)>,
removed: Vec<String>,
empty_input: Vec<String>,
too_long: Vec<String>,
}
fn sorted(pending: &Pending, input_bytes: NonZeroUsize) -> Sorted<'_> {
let limit = u64::try_from(input_bytes.get()).unwrap_or(u64::MAX);
let mut sorted = Sorted {
texts: Vec::new(),
removed: Vec::new(),
empty_input: pending.empty_input.clone(),
too_long: pending.too_long.clone(),
};
for waiting in &pending.waiting {
match waiting {
Waiting::Text {
id,
made_from,
text,
} => match input_refusal(text, limit) {
None => sorted.texts.push((id, *made_from, text)),
Some(ItemCode::EmbedInputEmpty) => sorted.empty_input.push(id.clone()),
Some(_) => {
sorted.too_long.push(id.clone());
}
},
Waiting::Removed { id } => sorted.removed.push(id.clone()),
}
}
sorted
}
pub fn step(
storage: &mut Storage,
branch: &str,
collection: &str,
field: &str,
worker: &dyn Embed,
bounds: Bounds,
) -> Result<Step, Failure> {
let pending = storage.embed_pending(
branch,
collection,
field,
bounds.page_records,
bounds.input_bytes,
)?;
let Some(pending) = pending else {
return Ok(Step::Idle);
};
let at = storage.capture(&Selection {
branch: branch.to_owned(),
version: None,
})?;
let (declaration, _companion) = storage.embedded_field(&at, collection, field)?;
let made = make(&pending, collection, field, &declaration, worker, bounds);
let made = match made {
Ok(made) => made,
Err(failure) => return Ok(Step::Failed(failure)),
};
let page = &made.page;
storage.embed_commit(branch, page, bounds.page_records)?;
Ok(Step::Committed(Counts {
made: page.made.len(),
removed: page.removed.len(),
no_text: pending.no_text.len(),
too_long: made.too_long.len(),
longer_than_model: made
.longer_than_model
.len()
.saturating_add(made.windows_over_cap.len()),
empty_input: made.empty_input.len(),
windowed: page.windowed.len(),
windows_over_cap: made.windows_over_cap.len(),
ended: page.reach == Reach::End,
}))
}
#[cfg(test)]
#[path = "job_tests.rs"]
mod tests;