1use std::collections::HashMap;
4use std::time::SystemTime;
5
6use serde::{Deserialize, Serialize};
7
8use skm_core::SkillName;
9use skm_select::Confidence;
10
11use crate::error::LearnError;
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct SelectionEvent {
16 pub timestamp: SystemTime,
18
19 pub query: String,
21
22 pub selected_skill: Option<SkillName>,
24
25 pub score: Option<f32>,
27
28 pub confidence: Option<Confidence>,
30
31 pub strategy: Option<String>,
33
34 pub latency_ms: u64,
36
37 pub feedback: Option<Feedback>,
39
40 pub metadata: HashMap<String, String>,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
46pub enum Feedback {
47 Correct,
49
50 Incorrect {
52 expected: Option<SkillName>,
54 },
55
56 Partial { reason: String },
58}
59
60pub trait AnalyticsStore: Send + Sync {
62 fn record(&self, event: SelectionEvent) -> Result<(), LearnError>;
64
65 fn query_recent(&self, limit: usize) -> Result<Vec<SelectionEvent>, LearnError>;
67
68 fn query_by_skill(
70 &self,
71 skill: &SkillName,
72 limit: usize,
73 ) -> Result<Vec<SelectionEvent>, LearnError>;
74
75 fn count(&self) -> Result<usize, LearnError>;
77}
78
79pub struct InMemoryAnalyticsStore {
81 events: std::sync::Mutex<Vec<SelectionEvent>>,
82 max_events: usize,
83}
84
85impl InMemoryAnalyticsStore {
86 pub fn new(max_events: usize) -> Self {
88 Self {
89 events: std::sync::Mutex::new(Vec::new()),
90 max_events,
91 }
92 }
93}
94
95impl Default for InMemoryAnalyticsStore {
96 fn default() -> Self {
97 Self::new(10000)
98 }
99}
100
101impl AnalyticsStore for InMemoryAnalyticsStore {
102 fn record(&self, event: SelectionEvent) -> Result<(), LearnError> {
103 let mut events = self.events.lock().unwrap();
104
105 if events.len() >= self.max_events {
106 events.remove(0);
107 }
108
109 events.push(event);
110 Ok(())
111 }
112
113 fn query_recent(&self, limit: usize) -> Result<Vec<SelectionEvent>, LearnError> {
114 let events = self.events.lock().unwrap();
115 let start = events.len().saturating_sub(limit);
116 Ok(events[start..].to_vec())
117 }
118
119 fn query_by_skill(
120 &self,
121 skill: &SkillName,
122 limit: usize,
123 ) -> Result<Vec<SelectionEvent>, LearnError> {
124 let events = self.events.lock().unwrap();
125 let filtered: Vec<_> = events
126 .iter()
127 .filter(|e| e.selected_skill.as_ref() == Some(skill))
128 .cloned()
129 .collect();
130
131 let start = filtered.len().saturating_sub(limit);
132 Ok(filtered[start..].to_vec())
133 }
134
135 fn count(&self) -> Result<usize, LearnError> {
136 Ok(self.events.lock().unwrap().len())
137 }
138}
139
140#[cfg(feature = "analytics-sqlite")]
142pub struct SqliteAnalyticsStore {
143 conn: std::sync::Mutex<rusqlite::Connection>,
144}
145
146#[cfg(feature = "analytics-sqlite")]
147impl SqliteAnalyticsStore {
148 pub fn new(path: &std::path::Path) -> Result<Self, LearnError> {
150 let conn = rusqlite::Connection::open(path)?;
151
152 conn.execute(
153 "CREATE TABLE IF NOT EXISTS selection_events (
154 id INTEGER PRIMARY KEY,
155 timestamp INTEGER NOT NULL,
156 query TEXT NOT NULL,
157 selected_skill TEXT,
158 score REAL,
159 confidence TEXT,
160 strategy TEXT,
161 latency_ms INTEGER NOT NULL,
162 feedback TEXT,
163 metadata TEXT
164 )",
165 [],
166 )?;
167
168 conn.execute(
169 "CREATE INDEX IF NOT EXISTS idx_timestamp ON selection_events(timestamp)",
170 [],
171 )?;
172
173 conn.execute(
174 "CREATE INDEX IF NOT EXISTS idx_skill ON selection_events(selected_skill)",
175 [],
176 )?;
177
178 Ok(Self {
179 conn: std::sync::Mutex::new(conn),
180 })
181 }
182
183 pub fn in_memory() -> Result<Self, LearnError> {
185 let conn = rusqlite::Connection::open_in_memory()?;
186
187 conn.execute(
188 "CREATE TABLE IF NOT EXISTS selection_events (
189 id INTEGER PRIMARY KEY,
190 timestamp INTEGER NOT NULL,
191 query TEXT NOT NULL,
192 selected_skill TEXT,
193 score REAL,
194 confidence TEXT,
195 strategy TEXT,
196 latency_ms INTEGER NOT NULL,
197 feedback TEXT,
198 metadata TEXT
199 )",
200 [],
201 )?;
202
203 Ok(Self {
204 conn: std::sync::Mutex::new(conn),
205 })
206 }
207}
208
209#[cfg(feature = "analytics-sqlite")]
210impl AnalyticsStore for SqliteAnalyticsStore {
211 fn record(&self, event: SelectionEvent) -> Result<(), LearnError> {
212 let conn = self.conn.lock().unwrap();
213
214 let timestamp = event
215 .timestamp
216 .duration_since(SystemTime::UNIX_EPOCH)
217 .unwrap()
218 .as_secs() as i64;
219
220 conn.execute(
221 "INSERT INTO selection_events
222 (timestamp, query, selected_skill, score, confidence, strategy, latency_ms, feedback, metadata)
223 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
224 rusqlite::params![
225 timestamp,
226 event.query,
227 event.selected_skill.as_ref().map(|s| s.as_str()),
228 event.score,
229 event.confidence.map(|c| format!("{:?}", c)),
230 event.strategy,
231 event.latency_ms as i64,
232 event.feedback.as_ref().map(|f| serde_json::to_string(f).unwrap()),
233 serde_json::to_string(&event.metadata).ok(),
234 ],
235 )?;
236
237 Ok(())
238 }
239
240 fn query_recent(&self, limit: usize) -> Result<Vec<SelectionEvent>, LearnError> {
241 let conn = self.conn.lock().unwrap();
242
243 let mut stmt = conn.prepare(
244 "SELECT timestamp, query, selected_skill, score, confidence, strategy, latency_ms, feedback, metadata
245 FROM selection_events
246 ORDER BY timestamp DESC
247 LIMIT ?1",
248 )?;
249
250 let events: Vec<SelectionEvent> = stmt
251 .query_map([limit], |row| {
252 let timestamp_secs: i64 = row.get(0)?;
253 let query: String = row.get(1)?;
254 let selected_skill: Option<String> = row.get(2)?;
255 let score: Option<f64> = row.get(3)?;
256 let confidence: Option<String> = row.get(4)?;
257 let strategy: Option<String> = row.get(5)?;
258 let latency_ms: i64 = row.get(6)?;
259 let feedback: Option<String> = row.get(7)?;
260 let metadata: Option<String> = row.get(8)?;
261
262 Ok(SelectionEvent {
263 timestamp: SystemTime::UNIX_EPOCH
264 + std::time::Duration::from_secs(timestamp_secs as u64),
265 query,
266 selected_skill: selected_skill.and_then(|s| SkillName::new(&s).ok()),
267 score: score.map(|s| s as f32),
268 confidence: confidence.and_then(|c| match c.as_str() {
269 "None" => Some(Confidence::None),
270 "Low" => Some(Confidence::Low),
271 "Medium" => Some(Confidence::Medium),
272 "High" => Some(Confidence::High),
273 "Definite" => Some(Confidence::Definite),
274 _ => None,
275 }),
276 strategy,
277 latency_ms: latency_ms as u64,
278 feedback: feedback.and_then(|f| serde_json::from_str(&f).ok()),
279 metadata: metadata
280 .and_then(|m| serde_json::from_str(&m).ok())
281 .unwrap_or_default(),
282 })
283 })?
284 .filter_map(|r| r.ok())
285 .collect();
286
287 Ok(events)
288 }
289
290 fn query_by_skill(
291 &self,
292 skill: &SkillName,
293 limit: usize,
294 ) -> Result<Vec<SelectionEvent>, LearnError> {
295 let conn = self.conn.lock().unwrap();
296
297 let mut stmt = conn.prepare(
298 "SELECT timestamp, query, selected_skill, score, confidence, strategy, latency_ms, feedback, metadata
299 FROM selection_events
300 WHERE selected_skill = ?1
301 ORDER BY timestamp DESC
302 LIMIT ?2",
303 )?;
304
305 let events: Vec<SelectionEvent> = stmt
306 .query_map(rusqlite::params![skill.as_str(), limit], |row| {
307 let timestamp_secs: i64 = row.get(0)?;
308 let query: String = row.get(1)?;
309 let selected_skill: Option<String> = row.get(2)?;
310 let score: Option<f64> = row.get(3)?;
311 let confidence: Option<String> = row.get(4)?;
312 let strategy: Option<String> = row.get(5)?;
313 let latency_ms: i64 = row.get(6)?;
314 let feedback: Option<String> = row.get(7)?;
315 let metadata: Option<String> = row.get(8)?;
316
317 Ok(SelectionEvent {
318 timestamp: SystemTime::UNIX_EPOCH
319 + std::time::Duration::from_secs(timestamp_secs as u64),
320 query,
321 selected_skill: selected_skill.and_then(|s| SkillName::new(&s).ok()),
322 score: score.map(|s| s as f32),
323 confidence: confidence.and_then(|c| match c.as_str() {
324 "None" => Some(Confidence::None),
325 "Low" => Some(Confidence::Low),
326 "Medium" => Some(Confidence::Medium),
327 "High" => Some(Confidence::High),
328 "Definite" => Some(Confidence::Definite),
329 _ => None,
330 }),
331 strategy,
332 latency_ms: latency_ms as u64,
333 feedback: feedback.and_then(|f| serde_json::from_str(&f).ok()),
334 metadata: metadata
335 .and_then(|m| serde_json::from_str(&m).ok())
336 .unwrap_or_default(),
337 })
338 })?
339 .filter_map(|r| r.ok())
340 .collect();
341
342 Ok(events)
343 }
344
345 fn count(&self) -> Result<usize, LearnError> {
346 let conn = self.conn.lock().unwrap();
347 let count: i64 = conn.query_row("SELECT COUNT(*) FROM selection_events", [], |row| {
348 row.get(0)
349 })?;
350 Ok(count as usize)
351 }
352}
353
354pub struct UsageAnalytics {
356 store: Box<dyn AnalyticsStore>,
357}
358
359impl UsageAnalytics {
360 pub fn new(store: Box<dyn AnalyticsStore>) -> Self {
362 Self { store }
363 }
364
365 pub fn in_memory() -> Self {
367 Self::new(Box::new(InMemoryAnalyticsStore::default()))
368 }
369
370 pub fn record(&self, event: SelectionEvent) -> Result<(), LearnError> {
372 self.store.record(event)
373 }
374
375 pub fn recent(&self, limit: usize) -> Result<Vec<SelectionEvent>, LearnError> {
377 self.store.query_recent(limit)
378 }
379
380 pub fn by_skill(&self, skill: &SkillName, limit: usize) -> Result<Vec<SelectionEvent>, LearnError> {
382 self.store.query_by_skill(skill, limit)
383 }
384
385 pub fn count(&self) -> Result<usize, LearnError> {
387 self.store.count()
388 }
389}
390
391#[cfg(test)]
392mod tests {
393 use super::*;
394
395 #[test]
396 fn test_in_memory_store() {
397 let store = InMemoryAnalyticsStore::new(100);
398
399 let event = SelectionEvent {
400 timestamp: SystemTime::now(),
401 query: "test query".to_string(),
402 selected_skill: Some(SkillName::new("test-skill").unwrap()),
403 score: Some(0.9),
404 confidence: Some(Confidence::High),
405 strategy: Some("trigger".to_string()),
406 latency_ms: 10,
407 feedback: None,
408 metadata: HashMap::new(),
409 };
410
411 store.record(event).unwrap();
412
413 assert_eq!(store.count().unwrap(), 1);
414
415 let events = store.query_recent(10).unwrap();
416 assert_eq!(events.len(), 1);
417 assert_eq!(events[0].query, "test query");
418 }
419
420 #[test]
421 fn test_in_memory_store_limit() {
422 let store = InMemoryAnalyticsStore::new(5);
423
424 for i in 0..10 {
425 let event = SelectionEvent {
426 timestamp: SystemTime::now(),
427 query: format!("query {}", i),
428 selected_skill: None,
429 score: None,
430 confidence: None,
431 strategy: None,
432 latency_ms: 10,
433 feedback: None,
434 metadata: HashMap::new(),
435 };
436 store.record(event).unwrap();
437 }
438
439 assert_eq!(store.count().unwrap(), 5);
441 }
442
443 #[test]
444 fn test_query_by_skill() {
445 let store = InMemoryAnalyticsStore::default();
446
447 let skill_a = SkillName::new("skill-a").unwrap();
448 let skill_b = SkillName::new("skill-b").unwrap();
449
450 for _ in 0..3 {
452 store
453 .record(SelectionEvent {
454 timestamp: SystemTime::now(),
455 query: "query".to_string(),
456 selected_skill: Some(skill_a.clone()),
457 score: None,
458 confidence: None,
459 strategy: None,
460 latency_ms: 10,
461 feedback: None,
462 metadata: HashMap::new(),
463 })
464 .unwrap();
465 }
466
467 store
469 .record(SelectionEvent {
470 timestamp: SystemTime::now(),
471 query: "query".to_string(),
472 selected_skill: Some(skill_b.clone()),
473 score: None,
474 confidence: None,
475 strategy: None,
476 latency_ms: 10,
477 feedback: None,
478 metadata: HashMap::new(),
479 })
480 .unwrap();
481
482 let events = store.query_by_skill(&skill_a, 10).unwrap();
483 assert_eq!(events.len(), 3);
484 }
485
486 #[cfg(feature = "analytics-sqlite")]
487 #[test]
488 fn test_sqlite_store() {
489 let store = SqliteAnalyticsStore::in_memory().unwrap();
490
491 let event = SelectionEvent {
492 timestamp: SystemTime::now(),
493 query: "test query".to_string(),
494 selected_skill: Some(SkillName::new("test-skill").unwrap()),
495 score: Some(0.9),
496 confidence: Some(Confidence::High),
497 strategy: Some("trigger".to_string()),
498 latency_ms: 10,
499 feedback: Some(Feedback::Correct),
500 metadata: HashMap::new(),
501 };
502
503 store.record(event).unwrap();
504
505 assert_eq!(store.count().unwrap(), 1);
506
507 let events = store.query_recent(10).unwrap();
508 assert_eq!(events.len(), 1);
509 assert_eq!(events[0].query, "test query");
510 }
511
512 #[test]
513 fn test_usage_analytics() {
514 let analytics = UsageAnalytics::in_memory();
515
516 let event = SelectionEvent {
517 timestamp: SystemTime::now(),
518 query: "test".to_string(),
519 selected_skill: None,
520 score: None,
521 confidence: None,
522 strategy: None,
523 latency_ms: 10,
524 feedback: None,
525 metadata: HashMap::new(),
526 };
527
528 analytics.record(event).unwrap();
529
530 assert_eq!(analytics.count().unwrap(), 1);
531 }
532}