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 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}