1use crate::provider::{EmbeddingProvider, EmbeddingResult};
4
5#[derive(Debug, Clone)]
7pub struct EmbeddingService<P: EmbeddingProvider> {
8 provider: P,
9 batch_size: usize,
10}
11
12impl<P: EmbeddingProvider> EmbeddingService<P> {
13 pub fn new(provider: P, batch_size: usize) -> Self {
15 Self {
16 provider,
17 batch_size: batch_size.max(1),
18 }
19 }
20
21 pub fn provider(&self) -> &P {
23 &self.provider
24 }
25
26 pub async fn embed_batch(&self, texts: &[String]) -> crate::error::Result<EmbeddingResult> {
28 let mut all = Vec::new();
29 let mut prompt_tokens = 0;
30 let mut total_tokens = 0;
31
32 for chunk in texts.chunks(self.batch_size) {
33 let result = self.provider.embed(chunk).await?;
34 all.extend(result.embeddings);
35 prompt_tokens += result.prompt_tokens;
36 total_tokens += result.total_tokens;
37 }
38
39 Ok(EmbeddingResult {
40 embeddings: all,
41 prompt_tokens,
42 total_tokens,
43 })
44 }
45}
46
47#[cfg(test)]
48mod tests {
49 use super::*;
50 use crate::error::EmbeddingError;
51 use async_trait::async_trait;
52
53 struct MockProvider;
54
55 #[async_trait]
56 impl EmbeddingProvider for MockProvider {
57 async fn embed(&self, texts: &[String]) -> crate::error::Result<EmbeddingResult> {
58 if texts.iter().any(|text| text == "boom") {
59 return Err(EmbeddingError::Transport("mock failure".into()));
60 }
61 Ok(EmbeddingResult {
62 embeddings: texts.iter().map(|text| vec![text.len() as f32]).collect(),
63 prompt_tokens: texts.len() as u64,
64 total_tokens: texts.len() as u64,
65 })
66 }
67
68 fn dimension(&self) -> usize {
69 1
70 }
71
72 fn model_name(&self) -> &str {
73 "mock"
74 }
75 }
76
77 #[tokio::test]
78 async fn batches_preserve_order_and_sum_usage() {
79 let service = EmbeddingService::new(MockProvider, 2);
80 let texts: Vec<String> = ["a", "bb", "ccc", "dddd"]
81 .iter()
82 .map(ToString::to_string)
83 .collect();
84 let result = service.embed_batch(&texts).await.expect("batch embed");
85 assert_eq!(
86 result.embeddings,
87 vec![vec![1.0], vec![2.0], vec![3.0], vec![4.0]]
88 );
89 assert_eq!(result.prompt_tokens, 4);
90 assert_eq!(result.total_tokens, 4);
91 }
92
93 #[tokio::test]
94 async fn empty_input_returns_empty_result() {
95 let service = EmbeddingService::new(MockProvider, 2);
96 let result = service.embed_batch(&[]).await.expect("empty batch");
97 assert!(result.embeddings.is_empty());
98 }
99
100 #[tokio::test]
101 async fn provider_error_propagates() {
102 let service = EmbeddingService::new(MockProvider, 2);
103 let result = service.embed_batch(&["boom".to_string()]).await;
104 assert!(result.is_err());
105 }
106}