use async_trait::async_trait;
use serde::{Deserialize, Serialize};
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum EvalError {
#[error("IO error: {0}")]
IoError(String),
#[error("parse error: {0}")]
ParseError(String),
#[error("embedding error: {0}")]
EmbeddingError(String),
#[error("prediction error: {0}")]
PredictorError(String),
#[error(
"length mismatch: {predictions} predictions vs {references} references; \
sample counts must match"
)]
LengthMismatch {
predictions: usize,
references: usize,
},
}
impl From<lc_core::judge::StructuredJudgeError> for EvalError {
fn from(e: lc_core::judge::StructuredJudgeError) -> Self {
match e {
lc_core::judge::StructuredJudgeError::Call(s) => EvalError::PredictorError(s),
lc_core::judge::StructuredJudgeError::Parse(s) => EvalError::ParseError(s),
_ => EvalError::PredictorError(e.to_string()),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Score {
pub value: f64,
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
}
impl Score {
pub fn new(value: f64) -> Self {
let value = if value.is_nan() {
log::warn!("Score::new received NaN, treating as 0.0");
0.0
} else {
value
};
Self {
value: value.clamp(0.0, 1.0),
label: None,
}
}
pub fn with_label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Example {
pub input: String,
pub reference: String,
#[serde(default)]
pub contexts: Vec<String>,
}
impl Example {
pub fn new(input: impl Into<String>, reference: impl Into<String>) -> Self {
Self {
input: input.into(),
reference: reference.into(),
contexts: Vec::new(),
}
}
pub fn with_contexts(
input: impl Into<String>,
reference: impl Into<String>,
contexts: Vec<String>,
) -> Self {
Self {
input: input.into(),
reference: reference.into(),
contexts,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Dataset {
pub examples: Vec<Example>,
}
impl Dataset {
pub fn new(examples: Vec<Example>) -> Self {
Self { examples }
}
pub async fn from_jsonl(path: &str) -> Result<Self, EvalError> {
let content = tokio::fs::read_to_string(path)
.await
.map_err(|e| EvalError::IoError(e.to_string()))?;
let mut examples = Vec::new();
for (i, line) in content.lines().enumerate() {
let line = line.trim();
if line.is_empty() {
continue;
}
let ex: Example = serde_json::from_str(line)
.map_err(|e| EvalError::ParseError(format!("line {}: {}", i + 1, e)))?;
examples.push(ex);
}
Ok(Self { examples })
}
pub fn len(&self) -> usize {
self.examples.len()
}
pub fn is_empty(&self) -> bool {
self.examples.is_empty()
}
}
#[async_trait]
pub trait Evaluator: Send + Sync {
async fn eval(
&self,
input: &str,
prediction: &str,
reference: &str,
) -> Result<Score, EvalError>;
fn name(&self) -> &str;
}
#[async_trait]
pub trait PairwiseEvaluator: Send + Sync {
async fn eval_pair(&self, input: &str, a: &str, b: &str) -> Result<Score, EvalError>;
fn name(&self) -> &str;
}
#[async_trait]
pub trait RagEvaluator: Send + Sync {
async fn eval_rag(
&self,
input: &str,
prediction: &str,
contexts: &[String],
reference: &str,
) -> Result<Score, EvalError>;
fn name(&self) -> &str;
}
#[async_trait]
pub trait Predictor: Send + Sync {
async fn predict(&self, input: &str) -> Result<String, EvalError>;
async fn report_token_usage(&self) -> Option<crate::TokenUsage> {
None
}
async fn begin_run(&self, _run_id: &str) {}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_score_new_normal() {
assert!((Score::new(0.5).value - 0.5).abs() < 1e-9);
}
#[test]
fn test_score_new_clamps_overflow() {
assert_eq!(Score::new(2.0).value, 1.0);
assert_eq!(Score::new(-1.0).value, 0.0);
assert_eq!(Score::new(f64::INFINITY).value, 1.0);
assert_eq!(Score::new(f64::NEG_INFINITY).value, 0.0);
}
#[test]
fn test_score_new_nan_guarded() {
assert_eq!(Score::new(f64::NAN).value, 0.0);
assert!(Score::new(f64::NAN).value.is_finite());
}
#[tokio::test]
async fn test_from_jsonl_async() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("data.jsonl");
std::fs::write(
&path,
"{\"input\":\"q1\",\"reference\":\"a1\"}\n\n{\"input\":\"q2\",\"reference\":\"a2\"}\n",
)
.unwrap();
let dataset = Dataset::from_jsonl(path.to_str().unwrap()).await.unwrap();
assert_eq!(dataset.len(), 2);
assert_eq!(dataset.examples[1].input, "q2");
assert_eq!(dataset.examples[1].reference, "a2");
}
#[tokio::test]
async fn test_from_jsonl_missing_file() {
let err = Dataset::from_jsonl("不存在-的文件.jsonl")
.await
.unwrap_err();
assert!(matches!(err, EvalError::IoError(_)));
}
#[tokio::test]
async fn test_contexts_round_trip_and_old_row_default() {
let old: Example = serde_json::from_str(r#"{"input":"q","reference":"r"}"#).unwrap();
assert!(old.contexts.is_empty());
let with_ctx = Example::with_contexts("q", "r", vec!["top".into(), "second".into()]);
let json = serde_json::to_string(&with_ctx).unwrap();
let back: Example = serde_json::from_str(&json).unwrap();
assert_eq!(back.contexts, vec!["top", "second"]);
}
#[tokio::test]
async fn test_from_jsonl_bad_line() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("bad.jsonl");
std::fs::write(&path, "{\"input\":\"q\"}\n").unwrap();
let err = Dataset::from_jsonl(path.to_str().unwrap())
.await
.unwrap_err();
assert!(matches!(err, EvalError::ParseError(_)));
}
}