1use std::collections::{HashMap, HashSet};
11
12use super::criteria::{Dataset, EvalError, Evaluator, PairwiseEvaluator, Predictor, Score};
13
14#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
16pub struct ExampleReport {
17 pub index: usize,
19 pub input: String,
20 pub reference: String,
21 pub prediction: String,
22 pub scores: HashMap<String, Score>,
24}
25
26#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
28pub struct ScoreSummary {
29 pub mean: f64,
30 pub std: f64,
31 pub count: usize,
32}
33
34#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
36pub struct FailureRecord {
37 pub index: usize,
39 pub stage: String,
41 pub error: String,
42}
43
44#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
46pub struct Report {
47 pub per_example: Vec<ExampleReport>,
49 pub summary: HashMap<String, ScoreSummary>,
51 pub failures: Vec<FailureRecord>,
53 #[serde(default)]
55 pub cost: crate::OverallCost,
56}
57
58pub struct EvalRunner {
60 evaluators: Vec<Box<dyn Evaluator>>,
61 pairwise: Vec<Box<dyn PairwiseEvaluator>>,
62 price_book: crate::PriceBook,
64}
65
66impl EvalRunner {
67 pub fn new(evaluators: Vec<Box<dyn Evaluator>>) -> Self {
69 Self {
70 evaluators,
71 pairwise: Vec::new(),
72 price_book: crate::PriceBook::default_set(),
73 }
74 }
75
76 pub fn with_pairwise(mut self, pairwise: Vec<Box<dyn PairwiseEvaluator>>) -> Self {
78 self.pairwise.extend(pairwise);
79 self
80 }
81
82 pub fn with_price_book(mut self, price_book: crate::PriceBook) -> Self {
84 self.price_book = price_book;
85 self
86 }
87
88 pub async fn run(
95 &self,
96 dataset: &Dataset,
97 predictor: &dyn Predictor,
98 ) -> Result<Report, EvalError> {
99 Self::warn_duplicate_names(&self.evaluators, &self.pairwise);
100
101 let mut per_example = Vec::with_capacity(dataset.len());
102 let mut failures = Vec::new();
103 let mut per_name: HashMap<String, Vec<f64>> = HashMap::new();
105 let mut cost = crate::OverallCost::default();
107
108 for (i, ex) in dataset.examples.iter().enumerate() {
109 let prediction = match predictor.predict(&ex.input).await {
110 Ok(p) => p,
111 Err(e) => {
112 failures.push(FailureRecord {
113 index: i,
114 stage: "predict".into(),
115 error: e.to_string(),
116 });
117 continue;
118 }
119 };
120
121 if let Some(usage) = predictor.report_token_usage().await {
123 cost.accumulate(&usage, &self.price_book);
124 }
125
126 let mut scores = HashMap::new();
127 for ev in &self.evaluators {
128 match ev.eval(&ex.input, &prediction, &ex.reference).await {
129 Ok(s) => {
130 per_name
131 .entry(ev.name().to_string())
132 .or_default()
133 .push(s.value);
134 scores.insert(ev.name().to_string(), s);
135 }
136 Err(e) => failures.push(FailureRecord {
137 index: i,
138 stage: ev.name().to_string(),
139 error: e.to_string(),
140 }),
141 }
142 }
143 for ev in &self.pairwise {
144 match ev.eval_pair(&ex.input, &prediction, &ex.reference).await {
145 Ok(s) => {
146 per_name
147 .entry(ev.name().to_string())
148 .or_default()
149 .push(s.value);
150 scores.insert(ev.name().to_string(), s);
151 }
152 Err(e) => failures.push(FailureRecord {
153 index: i,
154 stage: ev.name().to_string(),
155 error: e.to_string(),
156 }),
157 }
158 }
159
160 per_example.push(ExampleReport {
161 index: i,
162 input: ex.input.clone(),
163 reference: ex.reference.clone(),
164 prediction,
165 scores,
166 });
167 }
168
169 let mut summary = HashMap::new();
170 for (name, values) in per_name {
171 let count = values.len();
172 let mean = values.iter().sum::<f64>() / count as f64;
173 let variance = values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / count as f64;
175 summary.insert(
176 name,
177 ScoreSummary {
178 mean,
179 std: variance.sqrt(),
180 count,
181 },
182 );
183 }
184
185 Ok(Report {
186 per_example,
187 summary,
188 failures,
189 cost,
190 })
191 }
192
193 fn warn_duplicate_names(
195 evaluators: &[Box<dyn Evaluator>],
196 pairwise: &[Box<dyn PairwiseEvaluator>],
197 ) {
198 let mut seen = HashSet::new();
199 for ev in evaluators {
200 if !seen.insert(ev.name()) {
201 log::warn!(
202 "EvalRunner: duplicate evaluator name '{}', report data will be overwritten",
203 ev.name()
204 );
205 }
206 }
207 for ev in pairwise {
208 if !seen.insert(ev.name()) {
209 log::warn!(
210 "EvalRunner: duplicate evaluator name '{}', report data will be overwritten",
211 ev.name()
212 );
213 }
214 }
215 }
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221 use async_trait::async_trait;
222
223 struct ConstantEvaluator;
225
226 #[async_trait]
227 impl Evaluator for ConstantEvaluator {
228 async fn eval(
229 &self,
230 _input: &str,
231 _prediction: &str,
232 _reference: &str,
233 ) -> Result<Score, EvalError> {
234 Ok(Score::new(1.0))
235 }
236 fn name(&self) -> &str {
237 "constant"
238 }
239 }
240
241 struct UsagePredictor;
243
244 #[async_trait]
245 impl Predictor for UsagePredictor {
246 async fn predict(&self, input: &str) -> Result<String, EvalError> {
247 Ok(format!("{input}!"))
248 }
249 async fn report_token_usage(&self) -> Option<crate::TokenUsage> {
250 Some(crate::TokenUsage {
251 prompt_tokens: 100,
252 completion_tokens: 50,
253 model: Some("gpt-4o-mini".into()),
254 })
255 }
256 }
257
258 struct UnmeteredPredictor;
260
261 #[async_trait]
262 impl Predictor for UnmeteredPredictor {
263 async fn predict(&self, input: &str) -> Result<String, EvalError> {
264 Ok(input.to_string())
265 }
266 }
268
269 async fn dataset2() -> Dataset {
270 Dataset::new(vec![
271 crate::Example::new("q1", "a1"),
272 crate::Example::new("q2", "a2"),
273 ])
274 }
275
276 #[tokio::test]
277 async fn old_report_without_cost_field_still_deserializes() {
278 let old_json = r#"{
280 "per_example": [],
281 "summary": {},
282 "failures": []
283 }"#;
284 let report: Report = serde_json::from_str(old_json).unwrap();
285 assert_eq!(report.cost.total_tokens, 0);
287 assert!(report.cost.cost_usd.is_none());
288 }
289
290 #[tokio::test]
291 async fn report_round_trips_with_cost_field() {
292 let runner = EvalRunner::new(vec![Box::new(ConstantEvaluator)])
293 .with_price_book(crate::PriceBook::default_set());
294 let report = runner
295 .run(&dataset2().await, &UsagePredictor)
296 .await
297 .unwrap();
298
299 let json = serde_json::to_string(&report).unwrap();
300 let back: Report = serde_json::from_str(&json).unwrap();
301 assert_eq!(back.cost.total_tokens, report.cost.total_tokens);
302 assert_eq!(back.cost.cost_usd, report.cost.cost_usd);
303 }
304
305 #[tokio::test]
306 async fn runner_accumulates_priced_usage_across_examples() {
307 let runner = EvalRunner::new(vec![Box::new(ConstantEvaluator)])
308 .with_price_book(crate::PriceBook::default_set());
309 let report = runner
310 .run(&dataset2().await, &UsagePredictor)
311 .await
312 .unwrap();
313
314 assert_eq!(report.cost.prompt_tokens, 200);
316 assert_eq!(report.cost.completion_tokens, 100);
317 assert_eq!(report.cost.total_tokens, 300);
318 let expected = 200.0 / 1e6 * 0.15 + 100.0 / 1e6 * 0.60; let usd = report.cost.cost_usd.unwrap();
320 assert!(
321 (usd - expected).abs() < 1e-12,
322 "got {usd}, expected {expected}"
323 );
324 }
325
326 #[tokio::test]
327 async fn unmetered_predictor_leaves_cost_at_zero() {
328 let runner = EvalRunner::new(vec![Box::new(ConstantEvaluator)]);
329 let report = runner
330 .run(&dataset2().await, &UnmeteredPredictor)
331 .await
332 .unwrap();
333 assert_eq!(report.cost.total_tokens, 0);
335 assert!(report.cost.cost_usd.is_none());
336 }
337}