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}
24
25impl From<lc_core::judge::StructuredJudgeError> for EvalError {
28 fn from(e: lc_core::judge::StructuredJudgeError) -> Self {
29 match e {
30 lc_core::judge::StructuredJudgeError::Call(s) => EvalError::PredictorError(s),
31 lc_core::judge::StructuredJudgeError::Parse(s) => EvalError::ParseError(s),
32 _ => EvalError::PredictorError(e.to_string()),
35 }
36 }
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct Score {
42 pub value: f64,
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub label: Option<String>,
47}
48
49impl Score {
50 pub fn new(value: f64) -> Self {
56 let value = if value.is_nan() {
57 log::warn!("Score::new received NaN, treating as 0.0");
58 0.0
59 } else {
60 value
61 };
62 Self {
63 value: value.clamp(0.0, 1.0),
64 label: None,
65 }
66 }
67
68 pub fn with_label(mut self, label: impl Into<String>) -> Self {
70 self.label = Some(label.into());
71 self
72 }
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct Example {
78 pub input: String,
80 pub reference: String,
82}
83
84impl Example {
85 pub fn new(input: impl Into<String>, reference: impl Into<String>) -> Self {
87 Self {
88 input: input.into(),
89 reference: reference.into(),
90 }
91 }
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct Dataset {
97 pub examples: Vec<Example>,
99}
100
101impl Dataset {
102 pub fn new(examples: Vec<Example>) -> Self {
104 Self { examples }
105 }
106
107 pub async fn from_jsonl(path: &str) -> Result<Self, EvalError> {
111 let content = tokio::fs::read_to_string(path)
112 .await
113 .map_err(|e| EvalError::IoError(e.to_string()))?;
114 let mut examples = Vec::new();
115 for (i, line) in content.lines().enumerate() {
116 let line = line.trim();
117 if line.is_empty() {
118 continue;
119 }
120 let ex: Example = serde_json::from_str(line)
121 .map_err(|e| EvalError::ParseError(format!("line {}: {}", i + 1, e)))?;
122 examples.push(ex);
123 }
124 Ok(Self { examples })
125 }
126
127 pub fn len(&self) -> usize {
129 self.examples.len()
130 }
131
132 pub fn is_empty(&self) -> bool {
134 self.examples.is_empty()
135 }
136}
137
138#[async_trait]
140pub trait Evaluator: Send + Sync {
141 async fn eval(
143 &self,
144 input: &str,
145 prediction: &str,
146 reference: &str,
147 ) -> Result<Score, EvalError>;
148
149 fn name(&self) -> &str;
151}
152
153#[async_trait]
158pub trait PairwiseEvaluator: Send + Sync {
159 async fn eval_pair(&self, input: &str, a: &str, b: &str) -> Result<Score, EvalError>;
162
163 fn name(&self) -> &str;
165}
166
167#[async_trait]
169pub trait Predictor: Send + Sync {
170 async fn predict(&self, input: &str) -> Result<String, EvalError>;
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 #[test]
179 fn test_score_new_normal() {
180 assert!((Score::new(0.5).value - 0.5).abs() < 1e-9);
181 }
182
183 #[test]
184 fn test_score_new_clamps_overflow() {
185 assert_eq!(Score::new(2.0).value, 1.0);
186 assert_eq!(Score::new(-1.0).value, 0.0);
187 assert_eq!(Score::new(f64::INFINITY).value, 1.0);
188 assert_eq!(Score::new(f64::NEG_INFINITY).value, 0.0);
189 }
190
191 #[test]
193 fn test_score_new_nan_guarded() {
194 assert_eq!(Score::new(f64::NAN).value, 0.0);
195 assert!(Score::new(f64::NAN).value.is_finite());
197 }
198
199 #[tokio::test]
201 async fn test_from_jsonl_async() {
202 let dir = tempfile::tempdir().unwrap();
203 let path = dir.path().join("data.jsonl");
204 std::fs::write(
205 &path,
206 "{\"input\":\"q1\",\"reference\":\"a1\"}\n\n{\"input\":\"q2\",\"reference\":\"a2\"}\n",
207 )
208 .unwrap();
209 let dataset = Dataset::from_jsonl(path.to_str().unwrap()).await.unwrap();
210 assert_eq!(dataset.len(), 2);
211 assert_eq!(dataset.examples[1].input, "q2");
212 assert_eq!(dataset.examples[1].reference, "a2");
213 }
214
215 #[tokio::test]
216 async fn test_from_jsonl_missing_file() {
217 let err = Dataset::from_jsonl("不存在-的文件.jsonl")
218 .await
219 .unwrap_err();
220 assert!(matches!(err, EvalError::IoError(_)));
221 }
222
223 #[tokio::test]
224 async fn test_from_jsonl_bad_line() {
225 let dir = tempfile::tempdir().unwrap();
226 let path = dir.path().join("bad.jsonl");
227 std::fs::write(&path, "{\"input\":\"q\"}\n").unwrap();
228 let err = Dataset::from_jsonl(path.to_str().unwrap())
229 .await
230 .unwrap_err();
231 assert!(matches!(err, EvalError::ParseError(_)));
232 }
233}