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}
94
95impl Example {
96 pub fn new(input: impl Into<String>, reference: impl Into<String>) -> Self {
98 Self {
99 input: input.into(),
100 reference: reference.into(),
101 }
102 }
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct Dataset {
108 pub examples: Vec<Example>,
110}
111
112impl Dataset {
113 pub fn new(examples: Vec<Example>) -> Self {
115 Self { examples }
116 }
117
118 pub async fn from_jsonl(path: &str) -> Result<Self, EvalError> {
122 let content = tokio::fs::read_to_string(path)
123 .await
124 .map_err(|e| EvalError::IoError(e.to_string()))?;
125 let mut examples = Vec::new();
126 for (i, line) in content.lines().enumerate() {
127 let line = line.trim();
128 if line.is_empty() {
129 continue;
130 }
131 let ex: Example = serde_json::from_str(line)
132 .map_err(|e| EvalError::ParseError(format!("line {}: {}", i + 1, e)))?;
133 examples.push(ex);
134 }
135 Ok(Self { examples })
136 }
137
138 pub fn len(&self) -> usize {
140 self.examples.len()
141 }
142
143 pub fn is_empty(&self) -> bool {
145 self.examples.is_empty()
146 }
147}
148
149#[async_trait]
151pub trait Evaluator: Send + Sync {
152 async fn eval(
154 &self,
155 input: &str,
156 prediction: &str,
157 reference: &str,
158 ) -> Result<Score, EvalError>;
159
160 fn name(&self) -> &str;
162}
163
164#[async_trait]
169pub trait PairwiseEvaluator: Send + Sync {
170 async fn eval_pair(&self, input: &str, a: &str, b: &str) -> Result<Score, EvalError>;
173
174 fn name(&self) -> &str;
176}
177
178#[async_trait]
180pub trait Predictor: Send + Sync {
181 async fn predict(&self, input: &str) -> Result<String, EvalError>;
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 #[test]
190 fn test_score_new_normal() {
191 assert!((Score::new(0.5).value - 0.5).abs() < 1e-9);
192 }
193
194 #[test]
195 fn test_score_new_clamps_overflow() {
196 assert_eq!(Score::new(2.0).value, 1.0);
197 assert_eq!(Score::new(-1.0).value, 0.0);
198 assert_eq!(Score::new(f64::INFINITY).value, 1.0);
199 assert_eq!(Score::new(f64::NEG_INFINITY).value, 0.0);
200 }
201
202 #[test]
204 fn test_score_new_nan_guarded() {
205 assert_eq!(Score::new(f64::NAN).value, 0.0);
206 assert!(Score::new(f64::NAN).value.is_finite());
208 }
209
210 #[tokio::test]
212 async fn test_from_jsonl_async() {
213 let dir = tempfile::tempdir().unwrap();
214 let path = dir.path().join("data.jsonl");
215 std::fs::write(
216 &path,
217 "{\"input\":\"q1\",\"reference\":\"a1\"}\n\n{\"input\":\"q2\",\"reference\":\"a2\"}\n",
218 )
219 .unwrap();
220 let dataset = Dataset::from_jsonl(path.to_str().unwrap()).await.unwrap();
221 assert_eq!(dataset.len(), 2);
222 assert_eq!(dataset.examples[1].input, "q2");
223 assert_eq!(dataset.examples[1].reference, "a2");
224 }
225
226 #[tokio::test]
227 async fn test_from_jsonl_missing_file() {
228 let err = Dataset::from_jsonl("不存在-的文件.jsonl")
229 .await
230 .unwrap_err();
231 assert!(matches!(err, EvalError::IoError(_)));
232 }
233
234 #[tokio::test]
235 async fn test_from_jsonl_bad_line() {
236 let dir = tempfile::tempdir().unwrap();
237 let path = dir.path().join("bad.jsonl");
238 std::fs::write(&path, "{\"input\":\"q\"}\n").unwrap();
239 let err = Dataset::from_jsonl(path.to_str().unwrap())
240 .await
241 .unwrap_err();
242 assert!(matches!(err, EvalError::ParseError(_)));
243 }
244}