lc_evaluation/runner.rs
1//! Batch runner: `Report` and `EvalRunner`.
2//!
3//! `EvalRunner` calls the `Predictor` per example in the dataset, then scores with the pointwise
4//! `Evaluator`s and pairwise `PairwiseEvaluator`s, aggregating into a `Report`.
5//!
6//! P1-3: per-item tolerance — a failed predict or a failed evaluator score is recorded in
7//! `Report::failures`, computed results are kept, and the run does not abort. P1-4: `Report`
8//! carries the original text + stddev and implements `Serialize`/`Deserialize` for post-hoc analysis.
9
10use std::collections::{HashMap, HashSet};
11
12use super::criteria::{Dataset, EvalError, Evaluator, PairwiseEvaluator, Predictor, Score};
13
14/// Complete evaluation record for one example (includes the original text, for tracing low scores).
15#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
16pub struct ExampleReport {
17 /// Example index in the dataset (0-based)
18 pub index: usize,
19 pub input: String,
20 pub reference: String,
21 pub prediction: String,
22 /// Scores each evaluator assigned to this example (failed or not-run evaluators are absent)
23 pub scores: HashMap<String, Score>,
24}
25
26/// Summary statistics for one evaluator (mean + population stddev + sample count).
27#[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/// Failure record: a predict or an evaluator score failed for the example at a given index.
35#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
36pub struct FailureRecord {
37 /// Example index in the dataset (0-based)
38 pub index: usize,
39 /// Failure stage: `"predict"` or an evaluator's `name()`
40 pub stage: String,
41 pub error: String,
42}
43
44/// Evaluation report (with original text, stddev, failure list; deserializable).
45#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
46pub struct Report {
47 /// Per-example complete records (including input/reference/prediction originals)
48 pub per_example: Vec<ExampleReport>,
49 /// Per-evaluator summaries (mean + stddev + sample count)
50 pub summary: HashMap<String, ScoreSummary>,
51 /// Failure records collected by per-item tolerance (empty = all succeeded)
52 pub failures: Vec<FailureRecord>,
53}
54
55/// Batch runner: holds both pointwise and pairwise evaluators.
56pub struct EvalRunner {
57 evaluators: Vec<Box<dyn Evaluator>>,
58 pairwise: Vec<Box<dyn PairwiseEvaluator>>,
59}
60
61impl EvalRunner {
62 /// Creates a batch runner (pointwise evaluators only).
63 pub fn new(evaluators: Vec<Box<dyn Evaluator>>) -> Self {
64 Self {
65 evaluators,
66 pairwise: Vec::new(),
67 }
68 }
69
70 /// Appends pairwise evaluators (P1-1, arena evaluation enters the unified report).
71 pub fn with_pairwise(mut self, pairwise: Vec<Box<dyn PairwiseEvaluator>>) -> Self {
72 self.pairwise.extend(pairwise);
73 self
74 }
75
76 /// Runs all evaluators on the dataset, returning the report.
77 ///
78 /// P1-3: per-item tolerance — a failed predict records a `"predict"` failure and skips the example;
79 /// a failed evaluator score records only that evaluator's failure, others still score.
80 /// P1-1: pairwise evaluators participate too, using `(prediction, reference)` as the A/B candidates
81 /// (arena usage: put the answer under comparison in the reference slot).
82 pub async fn run(
83 &self,
84 dataset: &Dataset,
85 predictor: &dyn Predictor,
86 ) -> Result<Report, EvalError> {
87 Self::warn_duplicate_names(&self.evaluators, &self.pairwise);
88
89 let mut per_example = Vec::with_capacity(dataset.len());
90 let mut failures = Vec::new();
91 // accumulate each evaluator's successful sample scores per name, for mean/std computation
92 let mut per_name: HashMap<String, Vec<f64>> = HashMap::new();
93
94 for (i, ex) in dataset.examples.iter().enumerate() {
95 let prediction = match predictor.predict(&ex.input).await {
96 Ok(p) => p,
97 Err(e) => {
98 failures.push(FailureRecord {
99 index: i,
100 stage: "predict".into(),
101 error: e.to_string(),
102 });
103 continue;
104 }
105 };
106
107 let mut scores = HashMap::new();
108 for ev in &self.evaluators {
109 match ev.eval(&ex.input, &prediction, &ex.reference).await {
110 Ok(s) => {
111 per_name
112 .entry(ev.name().to_string())
113 .or_default()
114 .push(s.value);
115 scores.insert(ev.name().to_string(), s);
116 }
117 Err(e) => failures.push(FailureRecord {
118 index: i,
119 stage: ev.name().to_string(),
120 error: e.to_string(),
121 }),
122 }
123 }
124 for ev in &self.pairwise {
125 match ev.eval_pair(&ex.input, &prediction, &ex.reference).await {
126 Ok(s) => {
127 per_name
128 .entry(ev.name().to_string())
129 .or_default()
130 .push(s.value);
131 scores.insert(ev.name().to_string(), s);
132 }
133 Err(e) => failures.push(FailureRecord {
134 index: i,
135 stage: ev.name().to_string(),
136 error: e.to_string(),
137 }),
138 }
139 }
140
141 per_example.push(ExampleReport {
142 index: i,
143 input: ex.input.clone(),
144 reference: ex.reference.clone(),
145 prediction,
146 scores,
147 });
148 }
149
150 let mut summary = HashMap::new();
151 for (name, values) in per_name {
152 let count = values.len();
153 let mean = values.iter().sum::<f64>() / count as f64;
154 // population stddev: spread/variance reflects evaluator stability better than the mean alone
155 let variance = values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / count as f64;
156 summary.insert(
157 name,
158 ScoreSummary {
159 mean,
160 std: variance.sqrt(),
161 count,
162 },
163 );
164 }
165
166 Ok(Report {
167 per_example,
168 summary,
169 failures,
170 })
171 }
172
173 /// P1-4: duplicate-named evaluators silently overwrite each other in the summary/report; at least `log::warn`.
174 fn warn_duplicate_names(
175 evaluators: &[Box<dyn Evaluator>],
176 pairwise: &[Box<dyn PairwiseEvaluator>],
177 ) {
178 let mut seen = HashSet::new();
179 for ev in evaluators {
180 if !seen.insert(ev.name()) {
181 log::warn!(
182 "EvalRunner: duplicate evaluator name '{}', report data will be overwritten",
183 ev.name()
184 );
185 }
186 }
187 for ev in pairwise {
188 if !seen.insert(ev.name()) {
189 log::warn!(
190 "EvalRunner: duplicate evaluator name '{}', report data will be overwritten",
191 ev.name()
192 );
193 }
194 }
195 }
196}