do_memory_core/embeddings/
local.rs1use super::config::LocalConfig;
7use super::provider::EmbeddingProvider;
8use anyhow::{Context, Result};
9use async_trait::async_trait;
10use std::sync::Arc;
11use tokio::sync::RwLock;
12
13pub struct LocalEmbeddingProvider {
41 config: LocalConfig,
43 model: Arc<RwLock<Option<Box<dyn LocalEmbeddingModel>>>>,
45 cache_dir: std::path::PathBuf,
47}
48
49impl LocalEmbeddingProvider {
50 pub async fn new(config: LocalConfig) -> Result<Self> {
58 let cache_dir = Self::get_cache_dir().await?;
59
60 let provider = Self {
61 config,
62 model: Arc::new(RwLock::new(None)),
63 cache_dir,
64 };
65
66 provider.load_model().await?;
68
69 Ok(provider)
70 }
71
72 async fn load_model(&self) -> Result<()> {
74 tracing::info!("Loading local embedding model: {}", self.config.model_name);
75
76 #[cfg(feature = "local-embeddings")]
77 {
78 match self.try_load_real_model().await {
80 Ok(real_model) => {
81 let fallback_model = Box::new(RealEmbeddingModelWithFallback::new(
82 self.config.model_name.clone(),
83 self.config.embedding_dimension,
84 Some(real_model),
85 ));
86
87 let mut model_guard = self.model.write().await;
88 *model_guard = Some(fallback_model);
89
90 tracing::info!("Local embedding model loaded with real ONNX backend");
91 }
92 Err(e) => {
93 tracing::warn!("Failed to load real embedding model: {}", e);
94 tracing::warn!(
95 "Falling back to mock embeddings - semantic search will not work correctly"
96 );
97
98 let mock_fallback = Box::new(RealEmbeddingModelWithFallback::new(
99 self.config.model_name.clone(),
100 self.config.embedding_dimension,
101 None,
102 ));
103
104 let mut model_guard = self.model.write().await;
105 *model_guard = Some(mock_fallback);
106
107 tracing::info!("Local embedding model loaded with mock fallback");
108 }
109 }
110 }
111
112 #[cfg(not(feature = "local-embeddings"))]
113 {
114 tracing::warn!(
115 "PRODUCTION WARNING: Using mock embeddings - semantic search will not work correctly"
116 );
117 tracing::warn!(
118 "To enable real embeddings, add 'local-embeddings' feature and ensure ONNX models are available"
119 );
120
121 let mock_fallback = Box::new(super::mock_model::MockLocalModel::new(
122 self.config.model_name.clone(),
123 self.config.embedding_dimension,
124 ));
125
126 let mut model_guard = self.model.write().await;
127 *model_guard = Some(mock_fallback);
128
129 tracing::info!("Local embedding model loaded with mock implementation");
130 }
131
132 Ok(())
133 }
134
135 #[cfg(feature = "local-embeddings")]
137 async fn try_load_real_model(&self) -> Result<RealEmbeddingModel> {
138 RealEmbeddingModel::try_load_from_cache(&self.config, &self.cache_dir).await
139 }
140
141 #[cfg(target_arch = "wasm32")]
145 async fn get_cache_dir() -> Result<std::path::PathBuf> {
146 Ok(std::path::PathBuf::from("/tmp/memory-core-embeddings"))
148 }
149
150 #[cfg(not(target_arch = "wasm32"))]
152 async fn get_cache_dir() -> Result<std::path::PathBuf> {
153 let home = std::env::var("HOME")
154 .or_else(|_| std::env::var("USERPROFILE"))
155 .context("Could not determine home directory")?;
156
157 let cache_dir = std::path::Path::new(&home)
158 .join(".cache")
159 .join("memory-core")
160 .join("embeddings");
161
162 tokio::fs::create_dir_all(&cache_dir)
163 .await
164 .context("Failed to create cache directory")?;
165
166 Ok(cache_dir)
167 }
168
169 pub async fn is_loaded(&self) -> bool {
171 let model_guard = self.model.read().await;
172 model_guard.is_some()
173 }
174
175 #[must_use]
177 pub fn model_info(&self) -> serde_json::Value {
178 serde_json::json!({
179 "name": self.config.model_name,
180 "dimension": self.config.embedding_dimension,
181 "type": "local",
182 "cache_dir": self.cache_dir,
183 })
184 }
185}
186
187#[async_trait]
188impl EmbeddingProvider for LocalEmbeddingProvider {
189 async fn embed_text(&self, text: &str) -> Result<Vec<f32>> {
190 let model_guard = self.model.read().await;
191 let model = model_guard.as_ref().context("Model not loaded")?;
192
193 model.embed(text).await
194 }
195
196 async fn embed_batch(&self, texts: &[String]) -> Result<Vec<Vec<f32>>> {
197 let model_guard = self.model.read().await;
198 let model = model_guard.as_ref().context("Model not loaded")?;
199
200 model.embed_batch(texts).await
201 }
202
203 fn embedding_dimension(&self) -> usize {
204 self.config.embedding_dimension
205 }
206
207 fn model_name(&self) -> &str {
208 &self.config.model_name
209 }
210
211 async fn is_available(&self) -> bool {
212 self.is_loaded().await
213 }
214
215 async fn warmup(&self) -> Result<()> {
216 let _embedding = self.embed_text("warmup test").await?;
218 Ok(())
219 }
220
221 fn metadata(&self) -> serde_json::Value {
222 serde_json::json!({
223 "model": self.model_name(),
224 "dimension": self.embedding_dimension(),
225 "type": "local",
226 "provider": "sentence-transformers",
227 "cache_dir": self.cache_dir
228 })
229 }
230}
231
232#[async_trait]
234#[allow(dead_code)] pub trait LocalEmbeddingModel: Send + Sync {
236 async fn embed(&self, text: &str) -> Result<Vec<f32>>;
238
239 async fn embed_batch(&self, texts: &[String]) -> Result<Vec<Vec<f32>>>;
241
242 fn name(&self) -> &str;
244
245 fn dimension(&self) -> usize;
247}
248
249#[cfg(feature = "local-embeddings")]
251#[allow(unused)]
252pub use crate::embeddings::real_model::RealEmbeddingModel;
253
254#[cfg(feature = "local-embeddings")]
256#[allow(unused)]
257pub use crate::embeddings::mock_model::{MockLocalModel, RealEmbeddingModelWithFallback};
258
259#[allow(unused)]
261pub use crate::embeddings::utils::{
262 LocalModelUseCase, get_recommended_model, list_available_models,
263};
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268
269 #[tokio::test]
270 async fn test_local_provider_creation() {
271 let config = LocalConfig::new("test-model", 384);
272
273 let provider = LocalEmbeddingProvider::new(config).await.unwrap();
274 assert!(provider.is_loaded().await);
275 assert_eq!(provider.embedding_dimension(), 384);
276 assert_eq!(provider.model_name(), "test-model");
277 }
278
279 #[tokio::test]
280 async fn test_embed_text() {
281 let config = LocalConfig::new("test-model", 384);
282
283 let provider = LocalEmbeddingProvider::new(config).await.unwrap();
284
285 let embedding = provider.embed_text("Hello world").await.unwrap();
286 assert_eq!(embedding.len(), 384);
287
288 let embedding2 = provider.embed_text("Hello world").await.unwrap();
290 assert_eq!(embedding, embedding2);
291
292 let embedding3 = provider.embed_text("Different text").await.unwrap();
294 assert_ne!(embedding, embedding3);
295 }
296
297 #[tokio::test]
298 async fn test_embed_batch() {
299 let config = LocalConfig::new("test-model", 384);
300
301 let provider = LocalEmbeddingProvider::new(config).await.unwrap();
302
303 let texts = vec![
304 "First text".to_string(),
305 "Second text".to_string(),
306 "Third text".to_string(),
307 ];
308
309 let embeddings = provider.embed_batch(&texts).await.unwrap();
310 assert_eq!(embeddings.len(), 3);
311
312 for embedding in embeddings {
313 assert_eq!(embedding.len(), 384);
314 }
315 }
316
317 #[tokio::test]
318 async fn test_similarity_calculation() {
319 let config = LocalConfig::new("test-model", 384);
320
321 let provider = LocalEmbeddingProvider::new(config).await.unwrap();
322
323 let similarity = provider
325 .similarity("Hello world", "Hello world")
326 .await
327 .unwrap();
328 assert!((similarity - 1.0).abs() < 0.001);
329
330 let similarity = provider
332 .similarity("Hello world", "Goodbye universe")
333 .await
334 .unwrap();
335 assert!(similarity < 1.0);
336 }
337
338 #[tokio::test]
339 #[ignore = "Requires local-embeddings feature with ONNX models - blocked by ort crate Send trait issue"]
340 #[cfg(feature = "local-embeddings")]
341 async fn test_real_embedding_generation() {
342 let temp_dir = tempfile::TempDir::new().unwrap();
347 let cache_path = temp_dir.path().join("models");
348
349 if cache_path.exists() || std::env::var("CI").is_ok() {
352 tracing::info!("Skipping real embedding test - no model files available");
353 return;
354 }
355
356 let config = LocalConfig::new("sentence-transformers/all-MiniLM-L6-v2", 384);
357
358 let provider = LocalEmbeddingProvider::new(config).await.unwrap();
359
360 let embedding1 = provider
362 .embed_text("machine learning algorithms")
363 .await
364 .unwrap();
365 let embedding2 = provider
366 .embed_text("artificial intelligence models")
367 .await
368 .unwrap();
369 let embedding3 = provider
370 .embed_text("cooking recipes for pasta")
371 .await
372 .unwrap();
373
374 assert_eq!(embedding1.len(), 384);
375 assert_eq!(embedding2.len(), 384);
376 assert_eq!(embedding3.len(), 384);
377
378 let similarity_ai_ml = provider
380 .similarity("machine learning", "artificial intelligence")
381 .await
382 .unwrap();
383 let similarity_cooking = provider
384 .similarity("machine learning", "cooking recipes")
385 .await
386 .unwrap();
387
388 assert!(
390 similarity_ai_ml > similarity_cooking,
391 "AI/ML similarity ({similarity_ai_ml}) should be higher than ML/cooking ({similarity_cooking})"
392 );
393
394 assert!(similarity_ai_ml > 0.0);
396 assert!(similarity_cooking > 0.0);
397 }
398
399 #[tokio::test]
400 async fn test_embedding_vector_properties() {
401 let config = LocalConfig::new("test-model", 384);
402
403 let provider = LocalEmbeddingProvider::new(config).await.unwrap();
404
405 let embedding = provider.embed_text("test text").await.unwrap();
406
407 let norm: f32 = embedding.iter().map(|x| x * x).sum::<f32>().sqrt();
409 assert!((norm - 1.0).abs() < 0.001, "Embedding should be normalized");
410
411 for &value in &embedding {
413 assert!(
414 (-1.0..=1.0).contains(&value),
415 "Embedding values should be in [-1, 1]"
416 );
417 }
418 }
419
420 #[tokio::test]
421 async fn test_model_metadata() {
422 let config = LocalConfig::new("sentence-transformers/test-model", 768);
423
424 let provider = LocalEmbeddingProvider::new(config).await.unwrap();
425
426 let metadata = provider.metadata();
427 assert_eq!(metadata["model"], "sentence-transformers/test-model");
428 assert_eq!(metadata["dimension"], 768);
429 assert_eq!(metadata["type"], "local");
430
431 let model_info = provider.model_info();
432 assert_eq!(model_info["name"], "sentence-transformers/test-model");
433 assert_eq!(model_info["dimension"], 768);
434 assert_eq!(model_info["type"], "local");
435 }
436
437 #[tokio::test]
438 async fn test_error_handling() {
439 let config = LocalConfig::new("nonexistent-model", 384);
440
441 let result = LocalEmbeddingProvider::new(config).await;
443
444 match result {
445 Ok(provider) => {
446 assert!(provider.is_loaded().await);
448 let embedding = provider.embed_text("test").await.unwrap();
449 assert_eq!(embedding.len(), 384);
450 }
451 Err(e) => {
452 assert!(e.to_string().contains("model") || e.to_string().contains("load"));
454 }
455 }
456 }
457
458 #[tokio::test]
459 async fn test_warmup_functionality() {
460 let config = LocalConfig::new("test-model", 384);
461
462 let provider = LocalEmbeddingProvider::new(config).await.unwrap();
463
464 let result = provider.warmup().await;
466 assert!(result.is_ok(), "Warmup should succeed");
467 }
468
469 #[test]
470 fn test_utils_list_models() {
471 let models = list_available_models();
472 assert!(!models.is_empty());
473
474 for model in models {
475 assert!(!model.model_name.is_empty());
476 assert!(model.embedding_dimension > 0);
477 }
478 }
479
480 #[test]
481 fn test_utils_recommended_models() {
482 let fast_model = get_recommended_model(LocalModelUseCase::Fast);
483 assert_eq!(fast_model.embedding_dimension, 384);
484
485 let quality_model = get_recommended_model(LocalModelUseCase::Quality);
486 assert_eq!(quality_model.embedding_dimension, 768);
487
488 let multilingual_model = get_recommended_model(LocalModelUseCase::Multilingual);
489 assert_eq!(multilingual_model.embedding_dimension, 384);
490 }
491
492 #[tokio::test]
493 async fn test_production_warning_behavior() {
494 let config = LocalConfig::new("test-model", 384);
495
496 let provider = LocalEmbeddingProvider::new(config).await.unwrap();
498
499 let embedding1 = provider.embed_text("test").await.unwrap();
501 let embedding2 = provider.embed_text("test").await.unwrap();
502
503 assert_eq!(embedding1, embedding2);
505 assert_eq!(embedding1.len(), 384);
506 }
507}