ricecoder_completion/
history.rs

1/// Completion history tracking for frequency and recency scoring
2use crate::types::*;
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::path::PathBuf;
7use std::sync::{Arc, RwLock};
8
9/// Represents a single completion usage event
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct CompletionUsage {
12    /// The completion label
13    pub label: String,
14    /// The language context
15    pub language: String,
16    /// Timestamp of usage
17    pub timestamp: DateTime<Utc>,
18    /// Number of times used
19    pub frequency: u32,
20}
21
22impl CompletionUsage {
23    pub fn new(label: String, language: String) -> Self {
24        Self {
25            label,
26            language,
27            timestamp: Utc::now(),
28            frequency: 1,
29        }
30    }
31
32    /// Update the usage record with a new occurrence
33    pub fn record_usage(&mut self) {
34        self.frequency += 1;
35        self.timestamp = Utc::now();
36    }
37}
38
39/// Completion history tracker
40pub struct CompletionHistory {
41    /// In-memory cache of completion usage
42    usage_cache: Arc<RwLock<HashMap<String, CompletionUsage>>>,
43    /// Path to persist history
44    history_path: Option<PathBuf>,
45}
46
47impl CompletionHistory {
48    /// Create a new completion history tracker
49    pub fn new() -> Self {
50        Self {
51            usage_cache: Arc::new(RwLock::new(HashMap::new())),
52            history_path: None,
53        }
54    }
55
56    /// Create a new completion history tracker with persistence
57    pub fn with_path(history_path: PathBuf) -> Self {
58        Self {
59            usage_cache: Arc::new(RwLock::new(HashMap::new())),
60            history_path: Some(history_path),
61        }
62    }
63
64    /// Record a completion usage
65    pub fn record_usage(&self, label: String, language: String) -> CompletionResult<()> {
66        let mut cache = self.usage_cache.write().map_err(|_| {
67            CompletionError::InternalError(
68                "Failed to acquire write lock on history cache".to_string(),
69            )
70        })?;
71
72        let key = format!("{}:{}", language, label);
73        cache
74            .entry(key)
75            .and_modify(|usage| usage.record_usage())
76            .or_insert_with(|| CompletionUsage::new(label, language));
77
78        Ok(())
79    }
80
81    /// Get the frequency score for a completion (0.0 to 1.0)
82    pub fn get_frequency_score(&self, label: &str, language: &str) -> CompletionResult<f32> {
83        let cache = self.usage_cache.read().map_err(|_| {
84            CompletionError::InternalError(
85                "Failed to acquire read lock on history cache".to_string(),
86            )
87        })?;
88
89        let key = format!("{}:{}", language, label);
90        if let Some(usage) = cache.get(&key) {
91            // Normalize frequency to 0.0-1.0 range
92            // Assume max frequency of 100 for normalization
93            let score = (usage.frequency as f32 / 100.0).min(1.0);
94            Ok(score)
95        } else {
96            Ok(0.0)
97        }
98    }
99
100    /// Get the recency score for a completion (0.0 to 1.0)
101    /// Recent completions get higher scores
102    pub fn get_recency_score(&self, label: &str, language: &str) -> CompletionResult<f32> {
103        let cache = self.usage_cache.read().map_err(|_| {
104            CompletionError::InternalError(
105                "Failed to acquire read lock on history cache".to_string(),
106            )
107        })?;
108
109        let key = format!("{}:{}", language, label);
110        if let Some(usage) = cache.get(&key) {
111            // Calculate recency score based on time since last use
112            let now = Utc::now();
113            let duration = now.signed_duration_since(usage.timestamp);
114            let hours_ago = duration.num_hours() as f32;
115
116            // Score decays over time: 1.0 if used now, 0.5 if used 24 hours ago, 0.0 if used 7+ days ago
117            let score = if hours_ago <= 0.0 {
118                1.0
119            } else if hours_ago >= 168.0 {
120                // 7 days
121                0.0
122            } else {
123                1.0 - (hours_ago / 168.0)
124            };
125
126            Ok(score)
127        } else {
128            Ok(0.0)
129        }
130    }
131
132    /// Get combined frequency and recency score
133    pub fn get_usage_score(
134        &self,
135        label: &str,
136        language: &str,
137        frequency_weight: f32,
138        recency_weight: f32,
139    ) -> CompletionResult<f32> {
140        let frequency_score = self.get_frequency_score(label, language)?;
141        let recency_score = self.get_recency_score(label, language)?;
142
143        let total_weight = frequency_weight + recency_weight;
144        if total_weight == 0.0 {
145            return Ok(0.0);
146        }
147
148        let combined_score =
149            (frequency_score * frequency_weight + recency_score * recency_weight) / total_weight;
150        Ok(combined_score)
151    }
152
153    /// Load history from file
154    pub fn load(&self) -> CompletionResult<()> {
155        if let Some(path) = &self.history_path {
156            if path.exists() {
157                let content = std::fs::read_to_string(path).map_err(CompletionError::IoError)?;
158                let usages: Vec<CompletionUsage> =
159                    serde_json::from_str(&content).map_err(CompletionError::SerializationError)?;
160
161                let mut cache = self.usage_cache.write().map_err(|_| {
162                    CompletionError::InternalError(
163                        "Failed to acquire write lock on history cache".to_string(),
164                    )
165                })?;
166
167                for usage in usages {
168                    let key = format!("{}:{}", usage.language, usage.label);
169                    cache.insert(key, usage);
170                }
171            }
172        }
173
174        Ok(())
175    }
176
177    /// Save history to file
178    pub fn save(&self) -> CompletionResult<()> {
179        if let Some(path) = &self.history_path {
180            let cache = self.usage_cache.read().map_err(|_| {
181                CompletionError::InternalError(
182                    "Failed to acquire read lock on history cache".to_string(),
183                )
184            })?;
185
186            let usages: Vec<CompletionUsage> = cache.values().cloned().collect();
187            let content = serde_json::to_string_pretty(&usages)
188                .map_err(CompletionError::SerializationError)?;
189
190            std::fs::write(path, content).map_err(CompletionError::IoError)?;
191        }
192
193        Ok(())
194    }
195
196    /// Clear all history
197    pub fn clear(&self) -> CompletionResult<()> {
198        let mut cache = self.usage_cache.write().map_err(|_| {
199            CompletionError::InternalError(
200                "Failed to acquire write lock on history cache".to_string(),
201            )
202        })?;
203
204        cache.clear();
205        Ok(())
206    }
207
208    /// Get all usage records
209    pub fn get_all_usages(&self) -> CompletionResult<Vec<CompletionUsage>> {
210        let cache = self.usage_cache.read().map_err(|_| {
211            CompletionError::InternalError(
212                "Failed to acquire read lock on history cache".to_string(),
213            )
214        })?;
215
216        Ok(cache.values().cloned().collect())
217    }
218}
219
220impl Default for CompletionHistory {
221    fn default() -> Self {
222        Self::new()
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229
230    #[test]
231    fn test_record_usage() {
232        let history = CompletionHistory::new();
233        assert!(history
234            .record_usage("test".to_string(), "rust".to_string())
235            .is_ok());
236    }
237
238    #[test]
239    fn test_frequency_score_new_completion() {
240        let history = CompletionHistory::new();
241        let score = history.get_frequency_score("test", "rust").unwrap();
242        assert_eq!(score, 0.0);
243    }
244
245    #[test]
246    fn test_frequency_score_after_usage() {
247        let history = CompletionHistory::new();
248        history
249            .record_usage("test".to_string(), "rust".to_string())
250            .unwrap();
251        let score = history.get_frequency_score("test", "rust").unwrap();
252        assert!(score > 0.0);
253    }
254
255    #[test]
256    fn test_frequency_score_multiple_usages() {
257        let history = CompletionHistory::new();
258        for _ in 0..5 {
259            history
260                .record_usage("test".to_string(), "rust".to_string())
261                .unwrap();
262        }
263        let score = history.get_frequency_score("test", "rust").unwrap();
264        assert!(score > 0.0);
265    }
266
267    #[test]
268    fn test_recency_score_new_completion() {
269        let history = CompletionHistory::new();
270        let score = history.get_recency_score("test", "rust").unwrap();
271        assert_eq!(score, 0.0);
272    }
273
274    #[test]
275    fn test_recency_score_after_usage() {
276        let history = CompletionHistory::new();
277        history
278            .record_usage("test".to_string(), "rust".to_string())
279            .unwrap();
280        let score = history.get_recency_score("test", "rust").unwrap();
281        assert!(score > 0.9); // Should be very recent
282    }
283
284    #[test]
285    fn test_combined_usage_score() {
286        let history = CompletionHistory::new();
287        history
288            .record_usage("test".to_string(), "rust".to_string())
289            .unwrap();
290        let score = history.get_usage_score("test", "rust", 0.3, 0.2).unwrap();
291        assert!(score > 0.0);
292    }
293
294    #[test]
295    fn test_clear_history() {
296        let history = CompletionHistory::new();
297        history
298            .record_usage("test".to_string(), "rust".to_string())
299            .unwrap();
300        history.clear().unwrap();
301        let score = history.get_frequency_score("test", "rust").unwrap();
302        assert_eq!(score, 0.0);
303    }
304
305    #[test]
306    fn test_get_all_usages() {
307        let history = CompletionHistory::new();
308        history
309            .record_usage("test1".to_string(), "rust".to_string())
310            .unwrap();
311        history
312            .record_usage("test2".to_string(), "rust".to_string())
313            .unwrap();
314        let usages = history.get_all_usages().unwrap();
315        assert_eq!(usages.len(), 2);
316    }
317
318    #[test]
319    fn test_save_and_load() {
320        let temp_dir = tempfile::tempdir().unwrap();
321        let history_path = temp_dir.path().join("history.json");
322
323        let history = CompletionHistory::with_path(history_path.clone());
324        history
325            .record_usage("test".to_string(), "rust".to_string())
326            .unwrap();
327        assert!(history.save().is_ok());
328
329        let history2 = CompletionHistory::with_path(history_path);
330        assert!(history2.load().is_ok());
331        let usages = history2.get_all_usages().unwrap();
332        assert_eq!(usages.len(), 1);
333        assert_eq!(usages[0].label, "test");
334    }
335}