cratefield_core/ports/embedder.rs
1//! The `Embedder` port (issue #561, ADR 0024): text into embedding
2//! vectors, asked for by shape, never by vendor. A module holds
3//! `Arc<dyn Embedder>` and never learns which provider turned its text
4//! into numbers; the vectors feed the [`VectorIndex`](crate::VectorIndex)
5//! port, the sibling of this one — which is why [`Embeddings`] reports the
6//! answering model: vectors from different models are not comparable. No
7//! outcome enum, as on [`TextModel`](crate::TextModel): the unwired answer
8//! is [`EmbedError::NotConfigured`], so a module that cannot degrade
9//! without its embedder fails loudly.
10
11use async_trait::async_trait;
12
13/// One answered embedding request: one vector per input text, **in input
14/// order**, plus the model that answered and the input tokens it billed.
15#[derive(Debug, Clone, PartialEq)]
16pub struct Embeddings {
17 pub vectors: Vec<Vec<f32>>,
18 /// A provider identifier for the log line and the bill, not a wire
19 /// value a module switches on.
20 pub model: String,
21 pub input_tokens: u64,
22}
23
24impl Embeddings {
25 /// An answer by `model`; vectors and usage follow with
26 /// [`Embeddings::vector`] / [`Embeddings::usage`].
27 #[must_use]
28 pub fn new(model: impl Into<String>) -> Self {
29 Self {
30 vectors: Vec::new(),
31 model: model.into(),
32 input_tokens: 0,
33 }
34 }
35
36 /// Appends one vector, in input order.
37 #[must_use]
38 pub fn vector(mut self, vector: Vec<f32>) -> Self {
39 self.vectors.push(vector);
40 self
41 }
42
43 /// The token count the provider billed for the inputs.
44 #[must_use]
45 pub fn usage(mut self, input_tokens: u64) -> Self {
46 self.input_tokens = input_tokens;
47 self
48 }
49}
50
51/// Embedding failures. [`EmbedError::Provider`] carries the provider's own
52/// words, so it is scrubbed in `Display` (issue #235); `Debug` stays raw.
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub enum EmbedError {
55 /// No embedder is wired — the venture did not provide one.
56 NotConfigured,
57 /// The request was malformed before any provider saw it.
58 InvalidInput(String),
59 /// The provider refused the request or the call failed.
60 Provider(String),
61}
62
63impl std::fmt::Display for EmbedError {
64 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65 let scrub = crate::logging::scrub_text;
66 match self {
67 Self::NotConfigured => f.write_str("no embedder is wired"),
68 Self::InvalidInput(message) => {
69 write!(f, "embed request rejected: {}", scrub(message))
70 }
71 Self::Provider(message) => write!(f, "embed failed: {}", scrub(message)),
72 }
73 }
74}
75
76impl std::error::Error for EmbedError {}
77
78/// Embeds texts, over whichever provider the venture wired: exactly one
79/// vector per input text, in input order, so a caller can zip them.
80#[async_trait]
81pub trait Embedder: Send + Sync {
82 /// Embeds every text in `texts`.
83 ///
84 /// # Errors
85 /// [`EmbedError::NotConfigured`] when no embedder is wired;
86 /// [`EmbedError::InvalidInput`] when the request is malformed;
87 /// [`EmbedError::Provider`] when the provider refused or failed.
88 async fn embed(&self, texts: &[String]) -> Result<Embeddings, EmbedError>;
89}