use std::collections::{HashMap, VecDeque};
use tracing::info;
#[derive(Debug)]
pub struct QualityTracker {
observations: HashMap<String, VecDeque<f64>>,
window_size: usize,
}
impl QualityTracker {
pub fn new(window_size: usize) -> Self {
Self {
observations: HashMap::new(),
window_size,
}
}
pub fn record(&mut self, model: &str, quality: f64) {
let scores = self.observations.entry(model.to_string()).or_default();
scores.push_back(quality.clamp(0.0, 1.0));
if scores.len() > self.window_size {
scores.pop_front();
}
}
pub fn estimated_quality(&self, model: &str) -> Option<f64> {
self.observations.get(model).and_then(|scores| {
if scores.is_empty() {
None
} else {
Some(scores.iter().sum::<f64>() / scores.len() as f64)
}
})
}
pub fn observation_count(&self, model: &str) -> usize {
self.observations.get(model).map(|s| s.len()).unwrap_or(0)
}
pub fn tracked_models(&self) -> Vec<&str> {
self.observations.keys().map(|s| s.as_str()).collect()
}
pub fn seed_from_history(&mut self, observations: &[(String, f64)]) {
let mut count = 0usize;
for (model, score) in observations {
self.record(model, *score);
count += 1;
}
if count > 0 {
info!(
count,
models = self.observations.len(),
"seeded QualityTracker from historical observations"
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tracker_record_and_query() {
let mut tracker = QualityTracker::new(100);
tracker.record("model-a", 0.9);
tracker.record("model-a", 0.8);
let q = tracker.estimated_quality("model-a").unwrap();
assert!((q - 0.85).abs() < f64::EPSILON);
assert_eq!(tracker.observation_count("model-a"), 2);
}
#[test]
fn tracker_unknown_model() {
let tracker = QualityTracker::new(100);
assert!(tracker.estimated_quality("unknown").is_none());
assert_eq!(tracker.observation_count("unknown"), 0);
}
#[test]
fn tracker_window_size() {
let mut tracker = QualityTracker::new(3);
for i in 0..5 {
tracker.record("m", i as f64 * 0.2);
}
assert_eq!(tracker.observation_count("m"), 3);
let q = tracker.estimated_quality("m").unwrap();
assert!((q - (0.4 + 0.6 + 0.8) / 3.0).abs() < 1e-10);
}
#[test]
fn tracker_clamp() {
let mut tracker = QualityTracker::new(10);
tracker.record("m", 1.5);
tracker.record("m", -0.5);
assert!((tracker.estimated_quality("m").unwrap() - 0.5).abs() < f64::EPSILON);
}
#[test]
fn tracked_models_list() {
let mut tracker = QualityTracker::new(100);
tracker.record("a", 0.5);
tracker.record("b", 0.6);
let models = tracker.tracked_models();
assert_eq!(models.len(), 2);
}
#[test]
fn seed_from_history_populates_tracker() {
let mut tracker = QualityTracker::new(100);
let history = vec![
("model-a".to_string(), 0.7),
("model-a".to_string(), 0.9),
("model-b".to_string(), 0.85),
];
tracker.seed_from_history(&history);
assert_eq!(tracker.observation_count("model-a"), 2);
assert_eq!(tracker.observation_count("model-b"), 1);
let q = tracker.estimated_quality("model-a").unwrap();
assert!((q - 0.8).abs() < f64::EPSILON);
}
#[test]
fn seed_from_history_empty() {
let mut tracker = QualityTracker::new(100);
tracker.seed_from_history(&[]);
assert!(tracker.tracked_models().is_empty());
}
#[test]
fn seed_from_history_respects_window() {
let mut tracker = QualityTracker::new(2);
let history = vec![
("m".to_string(), 0.1),
("m".to_string(), 0.2),
("m".to_string(), 0.9),
];
tracker.seed_from_history(&history);
assert_eq!(tracker.observation_count("m"), 2);
let q = tracker.estimated_quality("m").unwrap();
assert!((q - 0.55).abs() < f64::EPSILON);
}
}