rig_core/embeddings/
embedding.rs1use crate::{
10 completion::Usage,
11 http_client, provider_response,
12 wasm_compat::{WasmCompatSend, WasmCompatSync},
13};
14use serde::{Deserialize, Serialize};
15
16#[derive(Debug, thiserror::Error)]
21#[non_exhaustive]
22pub enum EmbeddingError {
23 #[error("HttpError: {0}")]
25 HttpError(#[from] http_client::Error),
26
27 #[error("JsonError: {0}")]
29 JsonError(#[from] serde_json::Error),
30
31 #[error("UrlError: {0}")]
33 UrlError(#[from] url::ParseError),
34
35 #[cfg(not(target_family = "wasm"))]
36 #[error("DocumentError: {0}")]
38 DocumentError(Box<dyn std::error::Error + Send + Sync + 'static>),
39
40 #[cfg(target_family = "wasm")]
41 #[error("DocumentError: {0}")]
43 DocumentError(Box<dyn std::error::Error + 'static>),
44
45 #[error("ResponseError: {0}")]
47 ResponseError(String),
48
49 #[error("ProviderError: {0}")]
51 ProviderError(String),
52
53 #[error("ProviderResponseError: {0}")]
55 ProviderResponse(provider_response::ProviderResponseError),
56}
57
58crate::provider_response::impl_provider_response_helpers!(EmbeddingError);
59
60pub trait EmbeddingModel: WasmCompatSend + WasmCompatSync {
62 const MAX_DOCUMENTS: usize;
64
65 type Client;
67
68 fn make(client: &Self::Client, model: impl Into<String>, dims: Option<usize>) -> Self;
70
71 fn ndims(&self) -> usize;
73
74 fn embed_texts(
76 &self,
77 texts: impl IntoIterator<Item = String> + WasmCompatSend,
78 ) -> impl std::future::Future<Output = Result<Vec<Embedding>, EmbeddingError>> + WasmCompatSend;
79
80 fn embed_text(
82 &self,
83 text: &str,
84 ) -> impl std::future::Future<Output = Result<Embedding, EmbeddingError>> + WasmCompatSend {
85 async {
86 let mut embeddings = self.embed_texts(vec![text.to_string()]).await?;
87 embeddings.pop().ok_or_else(|| {
88 EmbeddingError::ResponseError(
89 "embedding provider returned an empty response for embed_text".to_string(),
90 )
91 })
92 }
93 }
94
95 fn embed_texts_with_usage(
101 &self,
102 texts: impl IntoIterator<Item = String> + WasmCompatSend,
103 ) -> impl std::future::Future<Output = Result<EmbeddingResponse, EmbeddingError>> + WasmCompatSend
104 {
105 async {
106 let embeddings = self.embed_texts(texts).await?;
107 Ok(EmbeddingResponse {
108 embeddings,
109 usage: Usage::default(),
110 })
111 }
112 }
113
114 fn embed_text_with_usage(
119 &self,
120 text: &str,
121 ) -> impl std::future::Future<Output = Result<EmbeddingResponse, EmbeddingError>> + WasmCompatSend
122 {
123 async {
124 let response = self.embed_texts_with_usage(vec![text.to_string()]).await?;
125 if response.embeddings.is_empty() {
126 return Err(EmbeddingError::ResponseError(
127 "embedding provider returned an empty response for embed_text_with_usage"
128 .to_string(),
129 ));
130 }
131 Ok(response)
132 }
133 }
134}
135
136#[derive(Debug, Clone)]
138pub struct EmbeddingResponse {
139 pub embeddings: Vec<Embedding>,
141 pub usage: Usage,
143}
144
145pub trait ImageEmbeddingModel: Clone + WasmCompatSend + WasmCompatSync {
147 const MAX_DOCUMENTS: usize;
149
150 fn ndims(&self) -> usize;
152
153 fn embed_images(
157 &self,
158 images: impl IntoIterator<Item = Vec<u8>> + WasmCompatSend,
159 ) -> impl std::future::Future<Output = Result<Vec<Embedding>, EmbeddingError>> + Send;
160
161 fn embed_image<'a>(
163 &'a self,
164 bytes: &'a [u8],
165 ) -> impl std::future::Future<Output = Result<Embedding, EmbeddingError>> + WasmCompatSend {
166 async move {
167 let mut embeddings = self.embed_images(vec![bytes.to_owned()]).await?;
168 embeddings.pop().ok_or_else(|| {
169 EmbeddingError::ResponseError(
170 "embedding provider returned an empty response for embed_image".to_string(),
171 )
172 })
173 }
174 }
175}
176
177#[derive(Clone, Default, Deserialize, Serialize, Debug)]
179pub struct Embedding {
180 pub document: String,
182 pub vec: Vec<f64>,
184}
185
186impl PartialEq for Embedding {
187 fn eq(&self, other: &Self) -> bool {
188 self.document == other.document
189 }
190}
191
192impl Eq for Embedding {}
193
194#[cfg(test)]
195mod provider_response_tests {
196 use super::*;
197 use http::StatusCode;
198
199 #[test]
200 fn embedding_error_provider_response_helpers_with_preserved_json_body() {
201 let body = r#"{"error":{"message":"rate limited"}}"#;
202 let error = EmbeddingError::ProviderResponse(provider_response::ProviderResponseError {
203 status: None,
204 body: body.to_string(),
205 });
206
207 assert_eq!(error.provider_response_body(), Some(body));
208 assert_eq!(error.provider_response_status(), None);
209 assert_eq!(
210 error.provider_response_json().expect("valid JSON"),
211 Some(serde_json::json!({ "error": { "message": "rate limited" } }))
212 );
213 }
214
215 #[test]
216 fn embedding_error_provider_error_is_not_a_provider_response() {
217 let error = EmbeddingError::ProviderError("internal diagnostic".to_string());
218
219 assert_eq!(error.provider_response_body(), None);
220 assert_eq!(error.provider_response_status(), None);
221 assert_eq!(error.provider_response_json().expect("no body"), None);
222 }
223
224 #[test]
225 fn embedding_error_provider_response_helpers_with_http_non_success() {
226 let body = r#"{"error":{"message":"bad request"}}"#;
227 let error = EmbeddingError::HttpError(http_client::Error::InvalidStatusCodeWithMessage(
228 StatusCode::BAD_REQUEST,
229 body.to_string(),
230 ));
231
232 assert_eq!(error.provider_response_body(), Some(body));
233 assert_eq!(
234 error.provider_response_status(),
235 Some(StatusCode::BAD_REQUEST)
236 );
237 assert_eq!(
238 error.provider_response_json().expect("valid JSON"),
239 Some(serde_json::json!({ "error": { "message": "bad request" } }))
240 );
241 }
242
243 #[test]
244 fn embedding_error_provider_response_helpers_with_preserved_plain_text_body() {
245 let error = EmbeddingError::ProviderResponse(provider_response::ProviderResponseError {
246 status: None,
247 body: "not json".to_string(),
248 });
249
250 assert_eq!(error.provider_response_body(), Some("not json"));
251 assert!(error.provider_response_json().is_err());
252 }
253
254 #[test]
255 fn embedding_error_provider_response_helpers_with_unrelated_variant() {
256 let error = EmbeddingError::ResponseError("parse failed".to_string());
257
258 assert_eq!(error.provider_response_body(), None);
259 assert_eq!(error.provider_response_status(), None);
260 assert_eq!(error.provider_response_json().expect("no body"), None);
261 }
262}