1use anyhow::Result;
10use async_trait::async_trait;
11
12use crate::task::EvalTask;
13use crate::{
14 EvalReport, EvalRunResult, EvalSuite, SuiteReport, aggregate_metrics, build_task_report,
15 compute_metric,
16};
17
18#[async_trait]
25pub trait EvalExecutor: Send + Sync {
26 async fn execute_task(&self, task: &EvalTask) -> Result<EvalRunResult>;
28}
29
30pub async fn run_suite(executor: &dyn EvalExecutor, suite: &EvalSuite) -> Result<EvalReport> {
37 let mut all_task_reports = Vec::new();
38
39 for task in &suite.tasks {
40 let mut run_results = Vec::new();
41 for _attempt in 1..=suite.attempts {
42 let result = executor.execute_task(task).await?;
43 run_results.push(result);
44 }
45 let metric = compute_metric(&task.id, &run_results);
46 all_task_reports.push(build_task_report(&task.id, &task.name, task.category, metric));
47 }
48
49 let all_metrics: Vec<_> = all_task_reports.iter().map(|r| r.metric.clone()).collect();
50 let cap_metrics: Vec<_> = all_task_reports
51 .iter()
52 .filter(|r| r.category == "Capability")
53 .map(|r| r.metric.clone())
54 .collect();
55 let reg_metrics: Vec<_> = all_task_reports
56 .iter()
57 .filter(|r| r.category == "Regression")
58 .map(|r| r.metric.clone())
59 .collect();
60
61 Ok(EvalReport {
62 generated_at: chrono::Utc::now().to_rfc3339(),
63 suites: vec![SuiteReport {
64 suite_id: suite.id.clone(),
65 suite_name: suite.name.clone(),
66 task_reports: all_task_reports,
67 aggregate: aggregate_metrics(&all_metrics),
68 capability_metrics: aggregate_metrics(&cap_metrics),
69 regression_metrics: aggregate_metrics(®_metrics),
70 }],
71 })
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77 use crate::task::{EvalCategory, EvalRunResult, EvalTask, RunOutcome};
78 use std::sync::atomic::{AtomicUsize, Ordering};
79
80 struct FakeExecutor {
82 outcomes: Vec<RunOutcome>,
83 calls: AtomicUsize,
84 }
85
86 #[async_trait]
87 impl EvalExecutor for FakeExecutor {
88 async fn execute_task(&self, _task: &EvalTask) -> Result<EvalRunResult> {
89 let i = self.calls.fetch_add(1, Ordering::SeqCst);
90 let outcome = self.outcomes[i % self.outcomes.len()];
91 Ok(EvalRunResult {
92 task_id: _task.id.clone(),
93 outcome,
94 error_message: None,
95 duration_secs: 0.0,
96 attempt: (i + 1) as u32,
97 cost_usd: None,
98 transcript_path: None,
99 })
100 }
101 }
102
103 fn suite(attempts: u32) -> EvalSuite {
104 EvalSuite {
105 id: "s1".into(),
106 name: "demo".into(),
107 tasks: vec![
108 EvalTask {
109 id: "t1".into(),
110 name: "t1".into(),
111 category: EvalCategory::Capability,
112 prompt: "p".into(),
113 verify_commands: vec![],
114 timeout_secs: None,
115 },
116 EvalTask {
117 id: "t2".into(),
118 name: "t2".into(),
119 category: EvalCategory::Regression,
120 prompt: "p".into(),
121 verify_commands: vec![],
122 timeout_secs: None,
123 },
124 ],
125 attempts,
126 }
127 }
128
129 #[tokio::test]
130 async fn run_suite_aggregates_capability_and_regression() {
131 let exec = FakeExecutor {
133 outcomes: vec![
134 RunOutcome::Pass,
135 RunOutcome::Fail,
136 RunOutcome::Pass,
137 RunOutcome::Pass,
138 ],
139 calls: AtomicUsize::new(0),
140 };
141 let report = run_suite(&exec, &suite(2)).await.unwrap();
142 let s = &report.suites[0];
143 assert_eq!(s.aggregate.passed_runs, 3);
145 assert_eq!(s.aggregate.total_runs, 4);
146 assert!((s.capability_metrics.pass_at_k - 0.5).abs() < 1e-9);
147 assert!((s.regression_metrics.pass_at_k - 1.0).abs() < 1e-9);
148 assert!(s.capability_metrics.pass_all_k < 1.0);
149 assert_eq!(s.regression_metrics.pass_all_k, 1.0);
150 }
151}