ceylon_runtime/config/
memory_config.rs

1//! Memory backend configuration from TOML files.
2
3use serde::{Deserialize, Serialize};
4
5/// Memory backend type.
6///
7/// Specifies which memory backend implementation to use.
8#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
9#[serde(rename_all = "lowercase")]
10pub enum MemoryBackendType {
11    /// In-memory storage (fast, no persistence)
12    #[default]
13    InMemory,
14    /// SQLite-based storage (persistent, requires 'sqlite' feature)
15    Sqlite,
16    /// Redis-based storage (distributed, requires 'redis' feature)
17    Redis,
18    /// Qdrant vector database (requires 'qdrant' feature)
19    Qdrant,
20    /// ChromaDB vector database (requires 'chromadb' feature)
21    #[serde(alias = "chroma")]
22    ChromaDB,
23    /// Pinecone vector database (requires 'pinecone' feature)
24    Pinecone,
25}
26
27/// Configuration for memory backends.
28///
29/// This struct can be used at the mesh level (shared by all agents) or
30/// at the agent level (per-agent configuration).
31///
32/// # Examples
33///
34/// ## InMemory (default)
35/// ```toml
36/// [memory]
37/// backend = "inmemory"
38/// max_entries = 500
39/// ttl_seconds = 3600
40/// ```
41///
42/// ## SQLite (persistent)
43/// ```toml
44/// [memory]
45/// backend = "sqlite"
46/// path = "./data/knowledge.db"
47/// ```
48///
49/// ## Redis (distributed)
50/// ```toml
51/// [memory]
52/// backend = "redis"
53/// url = "redis://localhost:6379"
54/// prefix = "myapp"
55/// ttl_seconds = 7200
56/// ```
57///
58/// ## Qdrant (vector database)
59/// ```toml
60/// [memory]
61/// backend = "qdrant"
62/// grpc_url = "http://localhost:6334"
63/// collection = "agent_memory"
64/// dimensions = 384
65/// embedding_model = "ollama::mxbai-embed-large"
66/// ```
67///
68/// ## ChromaDB (vector database)
69/// ```toml
70/// [memory]
71/// backend = "chromadb"
72/// url = "http://localhost:8000"
73/// collection = "agent_memory"
74/// use_server_embeddings = true
75/// ```
76///
77/// ## Pinecone (cloud vector database)
78/// ```toml
79/// [memory]
80/// backend = "pinecone"
81/// api_key = "${PINECONE_API_KEY}"
82/// collection = "agent-memory"
83/// dimensions = 1536
84/// ```
85#[derive(Debug, Clone, Serialize, Deserialize, Default)]
86pub struct MemoryConfig {
87    /// Backend type: "inmemory", "sqlite", "redis", "qdrant", "chromadb", or "pinecone"
88    #[serde(default)]
89    pub backend: MemoryBackendType,
90
91    /// Path for SQLite database file.
92    /// Use ":memory:" for in-memory SQLite.
93    /// Only used when backend = "sqlite".
94    #[serde(default)]
95    pub path: Option<String>,
96
97    /// Connection URL for Redis or ChromaDB.
98    /// Example: "redis://localhost:6379" or "http://localhost:8000"
99    /// Used when backend = "redis" or "chromadb".
100    #[serde(default)]
101    pub url: Option<String>,
102
103    /// GRPC URL for Qdrant.
104    /// Example: "http://localhost:6334"
105    /// Only used when backend = "qdrant".
106    #[serde(default)]
107    pub grpc_url: Option<String>,
108
109    /// Collection/index name for vector databases.
110    /// Used by Qdrant, ChromaDB, and Pinecone.
111    #[serde(default)]
112    pub collection: Option<String>,
113
114    /// Vector dimensions for embeddings.
115    /// Required for Qdrant and Pinecone when creating new collections.
116    #[serde(default)]
117    pub dimensions: Option<usize>,
118
119    /// API key for cloud services (Pinecone, etc.).
120    /// Can use environment variable syntax: "${PINECONE_API_KEY}"
121    #[serde(default)]
122    pub api_key: Option<String>,
123
124    /// Embedding model identifier (e.g., "ollama::mxbai-embed-large").
125    /// Used for generating embeddings if not using server-side embeddings.
126    #[serde(default)]
127    pub embedding_model: Option<String>,
128
129    /// Whether to use the vector DB's server-side embedding feature.
130    /// ChromaDB supports this with configured embedding functions.
131    #[serde(default)]
132    pub use_server_embeddings: Option<bool>,
133
134    /// Key prefix for Redis namespace isolation.
135    /// Only used when backend = "redis".
136    #[serde(default)]
137    pub prefix: Option<String>,
138
139    /// Maximum number of entries for InMemory backend.
140    /// When exceeded, oldest entries are evicted.
141    /// Only used when backend = "inmemory".
142    #[serde(default)]
143    pub max_entries: Option<usize>,
144
145    /// Default TTL (time-to-live) in seconds for entries.
146    /// Entries will be automatically removed after this time.
147    /// Supported by all backends.
148    #[serde(default)]
149    pub ttl_seconds: Option<i64>,
150}
151
152impl MemoryConfig {
153    /// Create a new default memory config (InMemory backend).
154    pub fn new() -> Self {
155        Self::default()
156    }
157
158    /// Create an InMemory backend configuration.
159    pub fn in_memory() -> Self {
160        Self {
161            backend: MemoryBackendType::InMemory,
162            ..Default::default()
163        }
164    }
165
166    /// Create a SQLite backend configuration.
167    pub fn sqlite(path: impl Into<String>) -> Self {
168        Self {
169            backend: MemoryBackendType::Sqlite,
170            path: Some(path.into()),
171            ..Default::default()
172        }
173    }
174
175    /// Create a Redis backend configuration.
176    pub fn redis(url: impl Into<String>) -> Self {
177        Self {
178            backend: MemoryBackendType::Redis,
179            url: Some(url.into()),
180            ..Default::default()
181        }
182    }
183
184    /// Create a Qdrant backend configuration.
185    ///
186    /// # Arguments
187    /// * `grpc_url` - Qdrant gRPC URL (e.g., "http://localhost:6334")
188    /// * `collection` - Collection name
189    /// * `dimensions` - Vector dimensions
190    pub fn qdrant(
191        grpc_url: impl Into<String>,
192        collection: impl Into<String>,
193        dimensions: usize,
194    ) -> Self {
195        Self {
196            backend: MemoryBackendType::Qdrant,
197            grpc_url: Some(grpc_url.into()),
198            collection: Some(collection.into()),
199            dimensions: Some(dimensions),
200            ..Default::default()
201        }
202    }
203
204    /// Create a ChromaDB backend configuration.
205    ///
206    /// # Arguments
207    /// * `url` - ChromaDB URL (e.g., "http://localhost:8000")
208    /// * `collection` - Collection name
209    pub fn chromadb(url: impl Into<String>, collection: impl Into<String>) -> Self {
210        Self {
211            backend: MemoryBackendType::ChromaDB,
212            url: Some(url.into()),
213            collection: Some(collection.into()),
214            ..Default::default()
215        }
216    }
217
218    /// Create a Pinecone backend configuration.
219    ///
220    /// # Arguments
221    /// * `api_key` - Pinecone API key
222    /// * `collection` - Index name
223    /// * `dimensions` - Vector dimensions
224    pub fn pinecone(
225        api_key: impl Into<String>,
226        collection: impl Into<String>,
227        dimensions: usize,
228    ) -> Self {
229        Self {
230            backend: MemoryBackendType::Pinecone,
231            api_key: Some(api_key.into()),
232            collection: Some(collection.into()),
233            dimensions: Some(dimensions),
234            ..Default::default()
235        }
236    }
237
238    /// Set max entries limit.
239    pub fn with_max_entries(mut self, max: usize) -> Self {
240        self.max_entries = Some(max);
241        self
242    }
243
244    /// Set default TTL in seconds.
245    pub fn with_ttl_seconds(mut self, seconds: i64) -> Self {
246        self.ttl_seconds = Some(seconds);
247        self
248    }
249
250    /// Set Redis key prefix.
251    pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
252        self.prefix = Some(prefix.into());
253        self
254    }
255
256    /// Set collection/index name.
257    pub fn with_collection(mut self, collection: impl Into<String>) -> Self {
258        self.collection = Some(collection.into());
259        self
260    }
261
262    /// Set vector dimensions.
263    pub fn with_dimensions(mut self, dimensions: usize) -> Self {
264        self.dimensions = Some(dimensions);
265        self
266    }
267
268    /// Set embedding model.
269    pub fn with_embedding_model(mut self, model: impl Into<String>) -> Self {
270        self.embedding_model = Some(model.into());
271        self
272    }
273
274    /// Enable or disable server-side embeddings.
275    pub fn with_server_embeddings(mut self, enabled: bool) -> Self {
276        self.use_server_embeddings = Some(enabled);
277        self
278    }
279
280    /// Set API key for cloud services.
281    pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
282        self.api_key = Some(api_key.into());
283        self
284    }
285}
286
287#[cfg(test)]
288mod tests {
289    use super::*;
290
291    #[test]
292    fn test_parse_inmemory_config() {
293        let toml = r#"
294            backend = "inmemory"
295            max_entries = 500
296            ttl_seconds = 3600
297        "#;
298
299        let config: MemoryConfig = toml::from_str(toml).unwrap();
300        assert_eq!(config.backend, MemoryBackendType::InMemory);
301        assert_eq!(config.max_entries, Some(500));
302        assert_eq!(config.ttl_seconds, Some(3600));
303    }
304
305    #[test]
306    fn test_parse_sqlite_config() {
307        let toml = r#"
308            backend = "sqlite"
309            path = "./data/knowledge.db"
310        "#;
311
312        let config: MemoryConfig = toml::from_str(toml).unwrap();
313        assert_eq!(config.backend, MemoryBackendType::Sqlite);
314        assert_eq!(config.path, Some("./data/knowledge.db".to_string()));
315    }
316
317    #[test]
318    fn test_parse_redis_config() {
319        let toml = r#"
320            backend = "redis"
321            url = "redis://localhost:6379"
322            prefix = "myapp"
323            ttl_seconds = 7200
324        "#;
325
326        let config: MemoryConfig = toml::from_str(toml).unwrap();
327        assert_eq!(config.backend, MemoryBackendType::Redis);
328        assert_eq!(config.url, Some("redis://localhost:6379".to_string()));
329        assert_eq!(config.prefix, Some("myapp".to_string()));
330        assert_eq!(config.ttl_seconds, Some(7200));
331    }
332
333    #[test]
334    fn test_default_is_inmemory() {
335        let toml = "";
336        let config: MemoryConfig = toml::from_str(toml).unwrap();
337        assert_eq!(config.backend, MemoryBackendType::InMemory);
338    }
339
340    #[test]
341    fn test_builder_methods() {
342        let config = MemoryConfig::in_memory()
343            .with_max_entries(1000)
344            .with_ttl_seconds(3600);
345
346        assert_eq!(config.backend, MemoryBackendType::InMemory);
347        assert_eq!(config.max_entries, Some(1000));
348        assert_eq!(config.ttl_seconds, Some(3600));
349
350        let config = MemoryConfig::redis("redis://localhost:6379")
351            .with_prefix("test")
352            .with_ttl_seconds(7200);
353
354        assert_eq!(config.backend, MemoryBackendType::Redis);
355        assert_eq!(config.url, Some("redis://localhost:6379".to_string()));
356        assert_eq!(config.prefix, Some("test".to_string()));
357    }
358}