Skip to main content

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}