pub trait QualityScorer: Send + Sync {
fn score(&self, request: &str, response: &str) -> f32;
fn record(&mut self, request: &str, response: &str, score: f32);
}
pub struct NoopScorer;
impl NoopScorer {
pub fn new() -> Self {
Self
}
}
impl Default for NoopScorer {
fn default() -> Self {
Self::new()
}
}
impl QualityScorer for NoopScorer {
fn score(&self, _request: &str, _response: &str) -> f32 {
0.5
}
fn record(&mut self, _request: &str, _response: &str, _score: f32) {
}
}
pub struct BasicScorer {
history: Vec<(u64, f32)>,
}
impl BasicScorer {
pub fn new() -> Self {
Self {
history: Vec::new(),
}
}
pub fn history(&self) -> &[(u64, f32)] {
&self.history
}
}
impl Default for BasicScorer {
fn default() -> Self {
Self::new()
}
}
impl QualityScorer for BasicScorer {
fn score(&self, _request: &str, response: &str) -> f32 {
let lower = response.to_lowercase();
let word_count = lower.split_whitespace().count();
let length_score = (word_count as f32 / 500.0).min(1.0) * 0.4;
let error_phrases = [
"i can't",
"i cannot",
"i don't know",
"i'm unable",
"i am unable",
"as an ai",
"i'm not able",
];
let error_penalty = if error_phrases.iter().any(|p| lower.contains(p)) {
0.3
} else {
0.0
};
let tool_bonus = if lower.contains("tool_use") || lower.contains("\"function\"") {
0.2
} else {
0.0
};
let base = if word_count > 0 { 0.3 } else { 0.0 };
(base + length_score + tool_bonus - error_penalty).clamp(0.0, 1.0)
}
fn record(&mut self, request: &str, _response: &str, score: f32) {
let hash = simple_hash(request);
self.history.push((hash, score));
}
}
fn simple_hash(s: &str) -> u64 {
use std::hash::{DefaultHasher, Hash, Hasher};
let mut hasher = DefaultHasher::new();
s.hash(&mut hasher);
hasher.finish()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn noop_always_returns_half() {
let scorer = NoopScorer::new();
assert!((scorer.score("any request", "any response") - 0.5).abs() < f32::EPSILON);
assert!((scorer.score("", "") - 0.5).abs() < f32::EPSILON);
}
#[test]
fn noop_record_does_not_panic() {
let mut scorer = NoopScorer::new();
scorer.record("req", "resp", 0.5);
}
#[test]
fn noop_default_trait() {
let scorer = NoopScorer::default();
assert!((scorer.score("x", "y") - 0.5).abs() < f32::EPSILON);
}
#[test]
fn basic_long_response_scores_higher_than_short() {
let scorer = BasicScorer::new();
let short_score = scorer.score("question", "yes");
let long_score = scorer.score("question", &"word ".repeat(200));
assert!(
long_score > short_score,
"long ({long_score}) should beat short ({short_score})"
);
}
#[test]
fn basic_error_response_scores_lower() {
let scorer = BasicScorer::new();
let good_score = scorer.score("question", "Here is the answer with details.");
let error_score = scorer.score("question", "I can't help with that request.");
assert!(
good_score > error_score,
"good ({good_score}) should beat error ({error_score})"
);
}
#[test]
fn basic_tool_use_gets_bonus() {
let scorer = BasicScorer::new();
let no_tool = scorer.score("question", "Here is the answer.");
let with_tool = scorer.score("question", r#"Here is the answer. {"tool_use": "search"}"#);
assert!(
with_tool > no_tool,
"tool_use ({with_tool}) should beat no tool ({no_tool})"
);
}
#[test]
fn basic_empty_response_scores_zero() {
let scorer = BasicScorer::new();
let score = scorer.score("question", "");
assert!(
score.abs() < f32::EPSILON,
"empty response should score 0.0, got {score}"
);
}
#[test]
fn basic_score_clamped_to_unit_range() {
let scorer = BasicScorer::new();
let response = format!("{} tool_use \"function\"", "word ".repeat(1000));
let score = scorer.score("q", &response);
assert!(score <= 1.0, "score should be <= 1.0, got {score}");
assert!(score >= 0.0, "score should be >= 0.0, got {score}");
}
#[test]
fn basic_record_stores_history() {
let mut scorer = BasicScorer::new();
scorer.record("req1", "resp1", 0.8);
scorer.record("req2", "resp2", 0.6);
assert_eq!(scorer.history().len(), 2);
assert!((scorer.history()[0].1 - 0.8).abs() < f32::EPSILON);
assert!((scorer.history()[1].1 - 0.6).abs() < f32::EPSILON);
}
#[test]
fn basic_default_trait() {
let scorer = BasicScorer::default();
assert!(scorer.history().is_empty());
}
#[test]
fn basic_multiple_error_phrases_detected() {
let scorer = BasicScorer::new();
let phrases = [
"I cannot do that.",
"I don't know the answer.",
"I'm unable to help.",
"I am unable to process.",
"As an AI, I can't.",
"I'm not able to assist.",
];
for phrase in &phrases {
let score = scorer.score("question", phrase);
let clean_score = scorer.score("question", "Here is the answer.");
assert!(
clean_score > score,
"clean ({clean_score}) should beat error phrase '{phrase}' ({score})"
);
}
}
}