use serde::{Deserialize, Serialize};
use std::fmt;
use crate::api::types::Message;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EvalResult {
pub score: f64,
pub passed: bool,
pub details: Vec<EvalDetail>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EvalDetail {
pub criterion: String,
pub score: f64,
pub passed: bool,
pub message: String,
}
pub trait TaskEvaluator: Send + Sync {
fn evaluate(&self, response: &str) -> EvalResult;
}
pub struct BenchTask {
pub id: String,
pub description: String,
pub messages: Vec<Message>,
pub evaluator: Box<dyn TaskEvaluator>,
}
impl fmt::Debug for BenchTask {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("BenchTask")
.field("id", &self.id)
.field("description", &self.description)
.field("messages_count", &self.messages.len())
.finish()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StreamResult {
pub task_id: String,
pub stream_id: usize,
pub success: bool,
pub transport_succeeded: bool,
pub response: String,
pub prompt_tokens: u64,
pub completion_tokens: u64,
pub latency_ms: u64,
pub eval: Option<EvalResult>,
pub error: Option<String>,
}
pub struct KeywordEvaluator {
pub keywords: Vec<String>,
pub threshold: f64,
}
impl KeywordEvaluator {
pub fn new(keywords: Vec<String>) -> Self {
Self {
keywords,
threshold: 0.5,
}
}
pub fn with_threshold(mut self, threshold: f64) -> Self {
self.threshold = threshold;
self
}
}
impl TaskEvaluator for KeywordEvaluator {
fn evaluate(&self, response: &str) -> EvalResult {
let lower = response.to_lowercase();
let mut details = Vec::new();
let mut hits = 0;
for kw in &self.keywords {
let found = lower.contains(&kw.to_lowercase());
if found {
hits += 1;
}
details.push(EvalDetail {
criterion: format!("keyword:{kw}"),
score: if found { 1.0 } else { 0.0 },
passed: found,
message: if found {
format!("Found '{kw}'")
} else {
format!("Missing '{kw}'")
},
});
}
let score = if self.keywords.is_empty() {
1.0
} else {
hits as f64 / self.keywords.len() as f64
};
EvalResult {
score,
passed: score >= self.threshold,
details,
}
}
}
pub struct JsonEvaluator {
pub required_fields: Vec<String>,
}
impl JsonEvaluator {
pub fn new(required_fields: Vec<String>) -> Self {
Self { required_fields }
}
}
impl TaskEvaluator for JsonEvaluator {
fn evaluate(&self, response: &str) -> EvalResult {
let json_str = extract_json_block(response);
let parsed = match serde_json::from_str::<serde_json::Value>(json_str) {
Ok(v) => v,
Err(e) => {
return EvalResult {
score: 0.0,
passed: false,
details: vec![EvalDetail {
criterion: "valid_json".into(),
score: 0.0,
passed: false,
message: format!("Invalid JSON: {e}"),
}],
};
}
};
let mut details = vec![EvalDetail {
criterion: "valid_json".into(),
score: 1.0,
passed: true,
message: "Valid JSON".into(),
}];
let mut hits = 1; let total = 1 + self.required_fields.len();
for field in &self.required_fields {
let found = parsed.get(field).is_some();
if found {
hits += 1;
}
details.push(EvalDetail {
criterion: format!("field:{field}"),
score: if found { 1.0 } else { 0.0 },
passed: found,
message: if found {
format!("Field '{field}' present")
} else {
format!("Field '{field}' missing")
},
});
}
let score = hits as f64 / total as f64;
EvalResult {
score,
passed: score >= 0.5,
details,
}
}
}
pub struct NoopEvaluator;
impl TaskEvaluator for NoopEvaluator {
fn evaluate(&self, _response: &str) -> EvalResult {
EvalResult {
score: 1.0,
passed: true,
details: vec![],
}
}
}
fn extract_json_block(text: &str) -> &str {
if let Some(start) = text.find("```json") {
let json_start = start + 7;
if let Some(end) = text[json_start..].find("```") {
return text[json_start..json_start + end].trim();
}
}
if let Some(start) = text.find("```") {
let inner_start = start + 3;
if let Some(end) = text[inner_start..].find("```") {
let block = text[inner_start..inner_start + end].trim();
if let Some(newline) = block.find('\n') {
let first_line = &block[..newline];
if !first_line.starts_with('{') && !first_line.starts_with('[') {
return block[newline + 1..].trim();
}
}
return block;
}
}
text.trim()
}
#[cfg(test)]
#[path = "../../tests/unit/bench_harness/task/task_test.rs"]
mod tests;