use crate::planner::complexity::ComplexityLevel;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PlannerMetrics {
pub total_plans_created: usize,
pub total_tokens_used: usize,
pub total_cost_usd: f64,
pub avg_estimation_accuracy: f32,
pub budget_violations: usize,
pub model_switches: usize,
pub plans_by_complexity: HashMap<String, usize>,
pub avg_planning_time_ms: f64,
pub last_updated: i64,
}
impl PlannerMetrics {
pub fn new() -> Self {
Self {
total_plans_created: 0,
total_tokens_used: 0,
total_cost_usd: 0.0,
avg_estimation_accuracy: 0.0,
budget_violations: 0,
model_switches: 0,
plans_by_complexity: HashMap::new(),
avg_planning_time_ms: 0.0,
last_updated: chrono::Utc::now().timestamp(),
}
}
pub fn record_plan(
&mut self,
complexity: ComplexityLevel,
tokens_used: usize,
cost: f64,
planning_time_ms: u128,
) {
self.total_plans_created += 1;
self.total_tokens_used += tokens_used;
self.total_cost_usd += cost;
*self
.plans_by_complexity
.entry(complexity.to_string())
.or_insert(0) += 1;
let total_time = self.avg_planning_time_ms * (self.total_plans_created - 1) as f64;
self.avg_planning_time_ms =
(total_time + planning_time_ms as f64) / self.total_plans_created as f64;
self.last_updated = chrono::Utc::now().timestamp();
}
pub fn record_accuracy(&mut self, estimated: usize, actual: usize) {
let accuracy = if actual > 0 {
1.0 - ((estimated as f32 - actual as f32).abs() / actual as f32)
} else {
1.0
};
let total = self.avg_estimation_accuracy * (self.total_plans_created - 1) as f32;
self.avg_estimation_accuracy = (total + accuracy) / self.total_plans_created as f32;
}
pub fn record_budget_violation(&mut self) {
self.budget_violations += 1;
}
pub fn record_model_switch(&mut self) {
self.model_switches += 1;
}
pub fn avg_cost_per_plan(&self) -> f64 {
if self.total_plans_created == 0 {
0.0
} else {
self.total_cost_usd / self.total_plans_created as f64
}
}
pub fn avg_tokens_per_plan(&self) -> f64 {
if self.total_plans_created == 0 {
0.0
} else {
self.total_tokens_used as f64 / self.total_plans_created as f64
}
}
}
impl Default for PlannerMetrics {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ExecutionRecord {
pub timestamp: i64,
pub complexity: ComplexityLevel,
pub estimated_tokens: usize,
pub actual_tokens: usize,
pub estimated_cost: f64,
pub actual_cost: f64,
pub model_used: String,
pub planning_time_ms: u128,
pub execution_time_ms: u128,
pub budget_exceeded: bool,
pub optimizations: Vec<String>,
}
impl ExecutionRecord {
pub fn estimation_accuracy(&self) -> f32 {
if self.actual_tokens > 0 {
1.0 - ((self.estimated_tokens as f32 - self.actual_tokens as f32).abs()
/ self.actual_tokens as f32)
} else {
1.0
}
}
pub fn cost_accuracy(&self) -> f32 {
if self.actual_cost > 0.0 {
1.0 - ((self.estimated_cost - self.actual_cost).abs() as f32 / self.actual_cost as f32)
} else {
1.0
}
}
}
pub struct MetricsTracker {
metrics: Arc<RwLock<PlannerMetrics>>,
history: Arc<RwLock<Vec<ExecutionRecord>>>,
max_history: usize,
}
impl MetricsTracker {
pub fn new(max_history: usize) -> Self {
Self {
metrics: Arc::new(RwLock::new(PlannerMetrics::new())),
history: Arc::new(RwLock::new(Vec::new())),
max_history,
}
}
pub fn record_execution(&self, record: ExecutionRecord) {
let mut metrics = self.metrics.write().unwrap();
metrics.record_plan(
record.complexity,
record.actual_tokens,
record.actual_cost,
record.planning_time_ms,
);
metrics.record_accuracy(record.estimated_tokens, record.actual_tokens);
if record.budget_exceeded {
metrics.record_budget_violation();
}
if !record.optimizations.is_empty() {
metrics.record_model_switch();
}
drop(metrics);
let mut history = self.history.write().unwrap();
history.push(record);
if history.len() > self.max_history {
history.remove(0);
}
}
pub fn get_metrics(&self) -> PlannerMetrics {
self.metrics.read().unwrap().clone()
}
pub fn get_history(&self) -> Vec<ExecutionRecord> {
self.history.read().unwrap().clone()
}
pub fn get_recent(&self, count: usize) -> Vec<ExecutionRecord> {
let history = self.history.read().unwrap();
let start = history.len().saturating_sub(count);
history[start..].to_vec()
}
pub fn get_accuracy_for_complexity(&self, complexity: ComplexityLevel) -> f32 {
let history = self.history.read().unwrap();
let records: Vec<_> = history
.iter()
.filter(|r| r.complexity == complexity)
.collect();
if records.is_empty() {
return 0.0;
}
let total_accuracy: f32 = records.iter().map(|r| r.estimation_accuracy()).sum();
total_accuracy / records.len() as f32
}
pub fn clear(&self) {
*self.metrics.write().unwrap() = PlannerMetrics::new();
self.history.write().unwrap().clear();
}
}
impl Default for MetricsTracker {
fn default() -> Self {
Self::new(1000)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_metrics_creation() {
let metrics = PlannerMetrics::new();
assert_eq!(metrics.total_plans_created, 0);
assert_eq!(metrics.total_cost_usd, 0.0);
}
#[test]
fn test_record_plan() {
let mut metrics = PlannerMetrics::new();
metrics.record_plan(ComplexityLevel::Simple, 1000, 0.05, 100);
assert_eq!(metrics.total_plans_created, 1);
assert_eq!(metrics.total_tokens_used, 1000);
assert_eq!(metrics.total_cost_usd, 0.05);
assert_eq!(metrics.avg_planning_time_ms, 100.0);
}
#[test]
fn test_accuracy_recording() {
let mut metrics = PlannerMetrics::new();
metrics.record_plan(ComplexityLevel::Simple, 1000, 0.05, 100);
metrics.record_accuracy(1000, 1000);
assert_eq!(metrics.avg_estimation_accuracy, 1.0);
}
#[test]
fn test_metrics_tracker() {
let tracker = MetricsTracker::new(10);
let record = ExecutionRecord {
timestamp: chrono::Utc::now().timestamp(),
complexity: ComplexityLevel::Simple,
estimated_tokens: 1000,
actual_tokens: 950,
estimated_cost: 0.05,
actual_cost: 0.048,
model_used: "gpt-4".to_string(),
planning_time_ms: 100,
execution_time_ms: 500,
budget_exceeded: false,
optimizations: vec![],
};
tracker.record_execution(record);
let metrics = tracker.get_metrics();
assert_eq!(metrics.total_plans_created, 1);
}
#[test]
fn test_history_trimming() {
let tracker = MetricsTracker::new(5);
for i in 0..10 {
let record = ExecutionRecord {
timestamp: chrono::Utc::now().timestamp(),
complexity: ComplexityLevel::Simple,
estimated_tokens: 1000 + i,
actual_tokens: 950 + i,
estimated_cost: 0.05,
actual_cost: 0.048,
model_used: "gpt-4".to_string(),
planning_time_ms: 100,
execution_time_ms: 500,
budget_exceeded: false,
optimizations: vec![],
};
tracker.record_execution(record);
}
let history = tracker.get_history();
assert_eq!(history.len(), 5); }
}