haematite-cli 0.21.3

The haematite command: serve a store, its door and Studio, answer MCP, query with jq, and host the store's assistant seat
Documentation
//! V8b (ruled 5 Oct 09:34): a question in words made into the one vector a
//! search compares, by a worker that takes questions only.
//!
//! The vector is checked against the field's declaration before any search
//! reads it: exactly one vector, made by the declared model, of the declared
//! length. Each check fails by the name `make` gives the same failing of a
//! document worker, so a caller reads one vocabulary for both.

use haem_frames::embedding::Kind;

use super::{Answer, Embedded, Embedder};
use crate::serve::storage::Failure;
use crate::serve::storage::embedded::{Declaration, ModelName};

/// A question in, its vector out: what a search needs of a worker.
pub trait Ask {
    /// The vectors of `text` embedded as a question, and the model that
    /// made them.
    ///
    /// # Errors
    /// Whatever the worker answers.
    fn question(&self, text: &str) -> Result<Embedded, Failure>;
}

impl Ask for Embedder {
    fn question(&self, text: &str) -> Result<Embedded, Failure> {
        self.embed(Kind::Query, &[text.to_owned()])
    }
}

/// The vector of `text` as a question for a field declared as
/// `declaration`.
///
/// # Errors
/// The worker's failure; `embed_vector_count` unless exactly one vector came
/// back; `embed_model_mismatch` for another model than the declared one;
/// `text_longer_than_model` for a question of more tokens than the model
/// reads, never searched by a part of it (EMB1); `embed_vector_length` for a
/// vector of another length than declared.
pub fn question_for(
    asker: &dyn Ask,
    text: &str,
    declaration: &Declaration,
) -> Result<Vec<f32>, Failure> {
    let embedded = asker.question(text)?;
    let count = embedded.answers.len();
    let model = ModelName {
        name: embedded.model.name,
        revision: embedded.model.revision,
        manifest_sha256: embedded.model.manifest_sha256,
    };
    let mut answers = embedded.answers.into_iter();
    let (Some(answer), None) = (answers.next(), answers.next()) else {
        return Err(Failure::new(
            "embed_vector_count",
            format!("one question was sent and {count} vectors came back"),
        ));
    };
    if model != declaration.model {
        return Err(Failure::new(
            "embed_model_mismatch",
            format!(
                "the question worker runs {}; the field is declared for {}",
                model.name, declaration.model.name
            ),
        ));
    }
    let vector = match answer {
        Answer::Vector(vector) => vector,
        // A question is never windowed (EMB-E2 Out); a worker that windows
        // it is answering as though it were a document.
        Answer::Windows(_) => {
            return Err(Failure::new(
                "embed_vector_count",
                "the question worker answered windows for one question",
            ));
        }
        Answer::Longer(longer) | Answer::OverCap(longer) => {
            return Err(Failure::new(
                "text_longer_than_model",
                format!(
                    "the question is {} tokens; the model's limit is {}",
                    longer.tokens_total, longer.tokens_limit
                ),
            ));
        }
    };
    let dimensions = declaration.dimensions.get();
    if vector.len() != dimensions {
        return Err(Failure::new(
            "embed_vector_length",
            format!(
                "a question vector of {} members came back; {dimensions} are declared",
                vector.len()
            ),
        ));
    }
    Ok(vector)
}

#[cfg(test)]
#[path = "question_tests.rs"]
mod tests;