lc_evaluation/
criteria.rs1use async_trait::async_trait;
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, thiserror::Error)]
9#[non_exhaustive]
10pub enum EvalError {
11 #[error("IO error: {0}")]
13 IoError(String),
14 #[error("parse error: {0}")]
16 ParseError(String),
17 #[error("embedding error: {0}")]
19 EmbeddingError(String),
20 #[error("prediction error: {0}")]
22 PredictorError(String),
23 #[error(
25 "length mismatch: {predictions} predictions vs {references} references; \
26 sample counts must match"
27 )]
28 LengthMismatch {
29 predictions: usize,
31 references: usize,
33 },
34}
35
36impl From<lc_core::judge::StructuredJudgeError> for EvalError {
39 fn from(e: lc_core::judge::StructuredJudgeError) -> Self {
40 match e {
41 lc_core::judge::StructuredJudgeError::Call(s) => EvalError::PredictorError(s),
42 lc_core::judge::StructuredJudgeError::Parse(s) => EvalError::ParseError(s),
43 _ => EvalError::PredictorError(e.to_string()),
46 }
47 }
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct Score {
53 pub value: f64,
55 #[serde(skip_serializing_if = "Option::is_none")]
57 pub label: Option<String>,
58}
59
60impl Score {
61 pub fn new(value: f64) -> Self {
67 let value = if value.is_nan() {
68 log::warn!("Score::new received NaN, treating as 0.0");
69 0.0
70 } else {
71 value
72 };
73 Self {
74 value: value.clamp(0.0, 1.0),
75 label: None,
76 }
77 }
78
79 pub fn with_label(mut self, label: impl Into<String>) -> Self {
81 self.label = Some(label.into());
82 self
83 }
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct Example {
89 pub input: String,
91 pub reference: String,
93 #[serde(default)]
98 pub contexts: Vec<String>,
99}
100
101impl Example {
102 pub fn new(input: impl Into<String>, reference: impl Into<String>) -> Self {
104 Self {
105 input: input.into(),
106 reference: reference.into(),
107 contexts: Vec::new(),
108 }
109 }
110
111 pub fn with_contexts(
113 input: impl Into<String>,
114 reference: impl Into<String>,
115 contexts: Vec<String>,
116 ) -> Self {
117 Self {
118 input: input.into(),
119 reference: reference.into(),
120 contexts,
121 }
122 }
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct Dataset {
128 pub examples: Vec<Example>,
130}
131
132impl Dataset {
133 pub fn new(examples: Vec<Example>) -> Self {
135 Self { examples }
136 }
137
138 pub async fn from_jsonl(path: &str) -> Result<Self, EvalError> {
142 let content = tokio::fs::read_to_string(path)
143 .await
144 .map_err(|e| EvalError::IoError(e.to_string()))?;
145 let mut examples = Vec::new();
146 for (i, line) in content.lines().enumerate() {
147 let line = line.trim();
148 if line.is_empty() {
149 continue;
150 }
151 let ex: Example = serde_json::from_str(line)
152 .map_err(|e| EvalError::ParseError(format!("line {}: {}", i + 1, e)))?;
153 examples.push(ex);
154 }
155 Ok(Self { examples })
156 }
157
158 pub fn len(&self) -> usize {
160 self.examples.len()
161 }
162
163 pub fn is_empty(&self) -> bool {
165 self.examples.is_empty()
166 }
167}
168
169#[async_trait]
171pub trait Evaluator: Send + Sync {
172 async fn eval(
174 &self,
175 input: &str,
176 prediction: &str,
177 reference: &str,
178 ) -> Result<Score, EvalError>;
179
180 fn name(&self) -> &str;
182}
183
184#[async_trait]
189pub trait PairwiseEvaluator: Send + Sync {
190 async fn eval_pair(&self, input: &str, a: &str, b: &str) -> Result<Score, EvalError>;
193
194 fn name(&self) -> &str;
196}
197
198#[async_trait]
206pub trait RagEvaluator: Send + Sync {
207 async fn eval_rag(
209 &self,
210 input: &str,
211 prediction: &str,
212 contexts: &[String],
213 reference: &str,
214 ) -> Result<Score, EvalError>;
215
216 fn name(&self) -> &str;
218}
219
220#[async_trait]
222pub trait Predictor: Send + Sync {
223 async fn predict(&self, input: &str) -> Result<String, EvalError>;
225
226 async fn report_token_usage(&self) -> Option<crate::TokenUsage> {
230 None
231 }
232
233 async fn begin_run(&self, _run_id: &str) {}
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 #[test]
246 fn test_score_new_normal() {
247 assert!((Score::new(0.5).value - 0.5).abs() < 1e-9);
248 }
249
250 #[test]
251 fn test_score_new_clamps_overflow() {
252 assert_eq!(Score::new(2.0).value, 1.0);
253 assert_eq!(Score::new(-1.0).value, 0.0);
254 assert_eq!(Score::new(f64::INFINITY).value, 1.0);
255 assert_eq!(Score::new(f64::NEG_INFINITY).value, 0.0);
256 }
257
258 #[test]
260 fn test_score_new_nan_guarded() {
261 assert_eq!(Score::new(f64::NAN).value, 0.0);
262 assert!(Score::new(f64::NAN).value.is_finite());
264 }
265
266 #[tokio::test]
268 async fn test_from_jsonl_async() {
269 let dir = tempfile::tempdir().unwrap();
270 let path = dir.path().join("data.jsonl");
271 std::fs::write(
272 &path,
273 "{\"input\":\"q1\",\"reference\":\"a1\"}\n\n{\"input\":\"q2\",\"reference\":\"a2\"}\n",
274 )
275 .unwrap();
276 let dataset = Dataset::from_jsonl(path.to_str().unwrap()).await.unwrap();
277 assert_eq!(dataset.len(), 2);
278 assert_eq!(dataset.examples[1].input, "q2");
279 assert_eq!(dataset.examples[1].reference, "a2");
280 }
281
282 #[tokio::test]
283 async fn test_from_jsonl_missing_file() {
284 let err = Dataset::from_jsonl("不存在-的文件.jsonl")
285 .await
286 .unwrap_err();
287 assert!(matches!(err, EvalError::IoError(_)));
288 }
289
290 #[tokio::test]
291 async fn test_contexts_round_trip_and_old_row_default() {
292 let old: Example = serde_json::from_str(r#"{"input":"q","reference":"r"}"#).unwrap();
294 assert!(old.contexts.is_empty());
295
296 let with_ctx = Example::with_contexts("q", "r", vec!["top".into(), "second".into()]);
298 let json = serde_json::to_string(&with_ctx).unwrap();
299 let back: Example = serde_json::from_str(&json).unwrap();
300 assert_eq!(back.contexts, vec!["top", "second"]);
301 }
302
303 #[tokio::test]
304 async fn test_from_jsonl_bad_line() {
305 let dir = tempfile::tempdir().unwrap();
306 let path = dir.path().join("bad.jsonl");
307 std::fs::write(&path, "{\"input\":\"q\"}\n").unwrap();
308 let err = Dataset::from_jsonl(path.to_str().unwrap())
309 .await
310 .unwrap_err();
311 assert!(matches!(err, EvalError::ParseError(_)));
312 }
313}