agcodex_core/context_engine/
embeddings.rs

1//! Embeddings abstraction for the Context Engine.
2//!
3//! This module provides a bridge between the Context Engine and the independent
4//! embeddings system. When embeddings are disabled, operations return NotImplemented
5//! errors with zero overhead.
6
7use crate::config::Config;
8use crate::embeddings::EmbeddingsConfig;
9use crate::embeddings::EmbeddingsManager;
10use thiserror::Error;
11
12pub type EmbeddingVector = Vec<f32>;
13
14#[derive(Debug, Error)]
15pub enum EmbeddingError {
16    #[error("not implemented")]
17    NotImplemented,
18
19    #[error(transparent)]
20    Io(#[from] std::io::Error),
21
22    #[error("provider error: {0}")]
23    Provider(String),
24
25    #[error("embeddings error: {0}")]
26    EmbeddingsError(#[from] crate::embeddings::EmbeddingError),
27}
28
29#[allow(async_fn_in_trait)]
30pub trait EmbeddingModel {
31    fn dimensions(&self) -> usize {
32        1536 // common default placeholder
33    }
34
35    async fn embed(&self, _text: &str) -> Result<EmbeddingVector, EmbeddingError> {
36        Err(EmbeddingError::NotImplemented)
37    }
38
39    async fn embed_batch(&self, _texts: &[String]) -> Result<Vec<EmbeddingVector>, EmbeddingError> {
40        Err(EmbeddingError::NotImplemented)
41    }
42}
43
44/// Bridge between the Context Engine and the EmbeddingsManager.
45///
46/// This struct provides zero overhead when embeddings are disabled and seamless
47/// integration when they are enabled.
48///
49/// # Usage Examples
50///
51/// ```rust,ignore
52/// // Create a disabled bridge (zero overhead)
53/// let bridge = EmbeddingModelBridge::disabled();
54///
55/// // Create from embeddings config
56/// let embeddings_config = Some(EmbeddingsConfig::default());
57/// let bridge = EmbeddingModelBridge::from_embeddings_config(embeddings_config);
58///
59/// // Use the bridge (returns NotImplemented when disabled)
60/// match bridge.embed("some text").await {
61///     Ok(vector) => println!("Embedded: {} dimensions", vector.len()),
62///     Err(EmbeddingError::NotImplemented) => println!("Embeddings disabled"),
63///     Err(e) => println!("Error: {}", e),
64/// }
65/// ```
66pub struct EmbeddingModelBridge {
67    /// The embeddings manager (None when disabled)
68    manager: Option<EmbeddingsManager>,
69}
70
71impl EmbeddingModelBridge {
72    /// Create a new bridge from the embeddings manager
73    pub const fn new(manager: Option<EmbeddingsManager>) -> Self {
74        Self { manager }
75    }
76
77    /// Create a bridge from embeddings configuration
78    ///
79    /// This is the preferred way to create the bridge, as it handles
80    /// the case where embeddings are disabled.
81    pub fn from_embeddings_config(config: Option<EmbeddingsConfig>) -> Self {
82        let manager = EmbeddingsManager::new(config);
83        Self::new(Some(manager))
84    }
85
86    /// Create a bridge from the main Config (future extension point)
87    ///
88    /// For now, this creates a disabled bridge since the main Config
89    /// doesn't yet have an embeddings field. When embeddings are added
90    /// to the main Config, this method will extract the embeddings config.
91    pub const fn from_config(_config: &Config) -> Self {
92        // TODO: When Config gets an embeddings field, use:
93        // let embeddings_config = config.embeddings.clone();
94        // Self::from_embeddings_config(embeddings_config)
95
96        // For now, create a disabled bridge
97        Self::new(None)
98    }
99
100    /// Create a disabled bridge (zero overhead)
101    pub const fn disabled() -> Self {
102        Self::new(None)
103    }
104
105    /// Check if embeddings are enabled
106    pub fn is_enabled(&self) -> bool {
107        self.manager
108            .as_ref()
109            .map(|m| m.is_enabled())
110            .unwrap_or(false)
111    }
112}
113
114impl EmbeddingModel for EmbeddingModelBridge {
115    fn dimensions(&self) -> usize {
116        if let Some(manager) = &self.manager {
117            manager.current_dimensions().unwrap_or(1536)
118        } else {
119            1536 // Default when disabled
120        }
121    }
122
123    async fn embed(&self, text: &str) -> Result<EmbeddingVector, EmbeddingError> {
124        match &self.manager {
125            Some(manager) => {
126                if !manager.is_enabled() {
127                    return Err(EmbeddingError::NotImplemented);
128                }
129
130                let result = manager.embed(text).await?;
131                match result {
132                    Some(vector) => Ok(vector),
133                    None => Err(EmbeddingError::NotImplemented),
134                }
135            }
136            None => Err(EmbeddingError::NotImplemented),
137        }
138    }
139
140    async fn embed_batch(&self, texts: &[String]) -> Result<Vec<EmbeddingVector>, EmbeddingError> {
141        match &self.manager {
142            Some(manager) => {
143                if !manager.is_enabled() {
144                    return Err(EmbeddingError::NotImplemented);
145                }
146
147                let result = manager.embed_batch(texts).await?;
148                match result {
149                    Some(vectors) => Ok(vectors),
150                    None => Err(EmbeddingError::NotImplemented),
151                }
152            }
153            None => Err(EmbeddingError::NotImplemented),
154        }
155    }
156}
157
158/// Default implementation that always returns NotImplemented
159///
160/// This is useful for testing and as a fallback when no embeddings are configured.
161pub struct NoOpEmbeddingModel;
162
163impl EmbeddingModel for NoOpEmbeddingModel {
164    fn dimensions(&self) -> usize {
165        1536
166    }
167
168    async fn embed(&self, _text: &str) -> Result<EmbeddingVector, EmbeddingError> {
169        Err(EmbeddingError::NotImplemented)
170    }
171
172    async fn embed_batch(&self, _texts: &[String]) -> Result<Vec<EmbeddingVector>, EmbeddingError> {
173        Err(EmbeddingError::NotImplemented)
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    #[test]
182    fn test_disabled_bridge_has_zero_overhead() {
183        let bridge = EmbeddingModelBridge::disabled();
184        assert!(!bridge.is_enabled());
185        assert_eq!(bridge.dimensions(), 1536);
186    }
187
188    #[tokio::test]
189    async fn test_disabled_bridge_returns_not_implemented() {
190        let bridge = EmbeddingModelBridge::disabled();
191
192        let result = bridge.embed("test").await;
193        assert!(matches!(result, Err(EmbeddingError::NotImplemented)));
194
195        let batch_result = bridge.embed_batch(&["test".to_string()]).await;
196        assert!(matches!(batch_result, Err(EmbeddingError::NotImplemented)));
197    }
198
199    // NOTE: Config construction test is skipped for now since Config has many fields
200    // and doesn't yet have embeddings configuration. When embeddings are added to Config,
201    // we can add a proper test for from_config()
202
203    #[test]
204    fn test_noop_embedding_model() {
205        let model = NoOpEmbeddingModel;
206        assert_eq!(model.dimensions(), 1536);
207    }
208
209    #[tokio::test]
210    async fn test_noop_embedding_model_returns_not_implemented() {
211        let model = NoOpEmbeddingModel;
212
213        let result = model.embed("test").await;
214        assert!(matches!(result, Err(EmbeddingError::NotImplemented)));
215
216        let batch_result = model.embed_batch(&["test".to_string()]).await;
217        assert!(matches!(batch_result, Err(EmbeddingError::NotImplemented)));
218    }
219}