Skip to main content

uira_memory/
config.rs

1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Serialize, Deserialize)]
4pub struct MemoryConfig {
5    #[serde(default = "default_enabled")]
6    pub enabled: bool,
7
8    #[serde(default = "default_storage_path")]
9    pub storage_path: String,
10
11    #[serde(default = "default_embedding_model")]
12    pub embedding_model: String,
13
14    #[serde(default = "default_embedding_dimension")]
15    pub embedding_dimension: usize,
16
17    #[serde(default = "default_embedding_api_key_env")]
18    pub embedding_api_key_env: String,
19
20    #[serde(default = "default_embedding_provider")]
21    pub embedding_provider: String,
22
23    #[serde(default = "default_embedding_api_base")]
24    pub embedding_api_base: String,
25
26    #[serde(default = "default_auto_recall")]
27    pub auto_recall: bool,
28
29    #[serde(default = "default_auto_capture")]
30    pub auto_capture: bool,
31
32    #[serde(default = "default_max_recall_results")]
33    pub max_recall_results: usize,
34
35
36    #[serde(default = "default_recall_min_query_length")]
37    pub recall_min_query_length: usize,
38
39    #[serde(default = "default_recall_cooldown_turns")]
40    pub recall_cooldown_turns: usize,
41
42    #[serde(default = "default_profile_frequency")]
43    pub profile_frequency: usize,
44
45    #[serde(default = "default_capture_mode")]
46    pub capture_mode: String,
47
48    #[serde(default = "default_container_tag")]
49    pub container_tag: String,
50
51    #[serde(default = "default_vector_weight")]
52    pub vector_weight: f32,
53
54    #[serde(default = "default_fts_weight")]
55    pub fts_weight: f32,
56
57    #[serde(default = "default_temporal_decay_lambda")]
58    pub temporal_decay_lambda: f64,
59
60    #[serde(default = "default_mmr_lambda")]
61    pub mmr_lambda: f64,
62
63    #[serde(default = "default_min_capture_length")]
64    pub min_capture_length: usize,
65
66    #[serde(default = "default_chunk_size")]
67    pub chunk_size: usize,
68
69    #[serde(default = "default_chunk_overlap")]
70    pub chunk_overlap: usize,
71
72    #[serde(default)]
73    pub retention_days: Option<u32>,
74
75    #[serde(default)]
76    pub max_memories: Option<usize>,
77}
78
79impl Default for MemoryConfig {
80    fn default() -> Self {
81        Self {
82            enabled: default_enabled(),
83            storage_path: default_storage_path(),
84            embedding_model: default_embedding_model(),
85            embedding_dimension: default_embedding_dimension(),
86            embedding_api_key_env: default_embedding_api_key_env(),
87            embedding_api_base: default_embedding_api_base(),
88            embedding_provider: default_embedding_provider(),
89            auto_recall: default_auto_recall(),
90            auto_capture: default_auto_capture(),
91            max_recall_results: default_max_recall_results(),
92            recall_min_query_length: default_recall_min_query_length(),
93            recall_cooldown_turns: default_recall_cooldown_turns(),
94            profile_frequency: default_profile_frequency(),
95            capture_mode: default_capture_mode(),
96            container_tag: default_container_tag(),
97            vector_weight: default_vector_weight(),
98            fts_weight: default_fts_weight(),
99            temporal_decay_lambda: default_temporal_decay_lambda(),
100            mmr_lambda: default_mmr_lambda(),
101            min_capture_length: default_min_capture_length(),
102            chunk_size: default_chunk_size(),
103            chunk_overlap: default_chunk_overlap(),
104            retention_days: None,
105            max_memories: None,
106        }
107    }
108}
109
110fn default_enabled() -> bool {
111    false
112}
113
114fn default_storage_path() -> String {
115    let home = dirs::home_dir()
116        .map(|h| h.to_string_lossy().to_string())
117        .unwrap_or_else(|| "~".to_string());
118    format!("{home}/.uira/memory.db")
119}
120
121fn default_embedding_model() -> String {
122    "text-embedding-3-small".to_string()
123}
124
125fn default_embedding_dimension() -> usize {
126    1536
127}
128
129fn default_embedding_api_key_env() -> String {
130    "OPENAI_API_KEY".to_string()
131}
132
133fn default_embedding_api_base() -> String {
134    "https://api.openai.com/v1".to_string()
135}
136
137fn default_embedding_provider() -> String {
138    "openai".to_string()
139}
140
141fn default_auto_recall() -> bool {
142    true
143}
144
145fn default_auto_capture() -> bool {
146    true
147}
148
149fn default_max_recall_results() -> usize {
150    5
151}
152
153fn default_profile_frequency() -> usize {
154    5
155}
156
157fn default_recall_min_query_length() -> usize {
158    10
159}
160
161fn default_recall_cooldown_turns() -> usize {
162    1
163}
164
165fn default_capture_mode() -> String {
166    "all".to_string()
167}
168
169fn default_container_tag() -> String {
170    if let Ok(cwd) = std::env::current_dir() {
171        if let Some(dir_name) = cwd.file_name().and_then(|n| n.to_str()) {
172            let sanitized: String = dir_name
173                .to_lowercase()
174                .chars()
175                .map(|c| if c.is_alphanumeric() { c } else { '-' })
176                .collect();
177            return format!("project-{sanitized}");
178        }
179    }
180    // Fallback to hostname
181    let hostname = hostname::get()
182        .map(|h| h.to_string_lossy().to_string())
183        .unwrap_or_else(|_| "unknown".to_string());
184    format!("uira_{hostname}")
185}
186
187fn default_vector_weight() -> f32 {
188    0.7
189}
190
191fn default_fts_weight() -> f32 {
192    0.3
193}
194
195fn default_temporal_decay_lambda() -> f64 {
196    0.001
197}
198
199fn default_mmr_lambda() -> f64 {
200    0.7
201}
202
203fn default_min_capture_length() -> usize {
204    20
205}
206
207fn default_chunk_size() -> usize {
208    512
209}
210
211fn default_chunk_overlap() -> usize {
212    50
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218
219    #[test]
220    fn defaults_are_sensible() {
221        let config = MemoryConfig::default();
222        assert!(!config.enabled);
223        assert!(config.storage_path.contains("memory.db"));
224        assert_eq!(config.embedding_model, "text-embedding-3-small");
225        assert_eq!(config.embedding_dimension, 1536);
226        assert!(config.auto_recall);
227        assert!(config.auto_capture);
228        assert_eq!(config.max_recall_results, 5);
229        assert_eq!(config.profile_frequency, 5);
230        assert_eq!(config.vector_weight, 0.7);
231        assert_eq!(config.fts_weight, 0.3);
232        assert_eq!(config.chunk_size, 512);
233        assert_eq!(config.chunk_overlap, 50);
234        assert_eq!(config.recall_min_query_length, 10);
235        assert_eq!(config.recall_cooldown_turns, 1);
236        assert_eq!(config.embedding_provider, "openai");
237    }
238
239    #[test]
240    fn deserialize_with_defaults() {
241        let yaml = r#"
242enabled: true
243storage_path: "/tmp/test.db"
244"#;
245        let config: MemoryConfig = serde_yaml_ng::from_str(yaml).unwrap();
246        assert!(config.enabled);
247        assert_eq!(config.storage_path, "/tmp/test.db");
248        assert_eq!(config.embedding_model, "text-embedding-3-small");
249    }
250
251    #[test]
252    fn container_tag_includes_hostname() {
253        let config = MemoryConfig::default();
254        assert!(config.container_tag.starts_with("project-"));
255    }
256}