use async_trait::async_trait;
use regex::Regex;
use super::{EvalError, Evaluator, Score};
pub struct ContainsKeyword {
keywords: Vec<String>,
case_sensitive: bool,
all_required: bool,
}
impl ContainsKeyword {
pub fn new(keywords: Vec<String>) -> Self {
Self {
keywords,
case_sensitive: false,
all_required: true,
}
}
pub fn case_sensitive(mut self, v: bool) -> Self {
self.case_sensitive = v;
self
}
pub fn all_required(mut self, v: bool) -> Self {
self.all_required = v;
self
}
}
#[async_trait]
impl Evaluator for ContainsKeyword {
async fn eval(
&self,
_input: &str,
prediction: &str,
_reference: &str,
) -> Result<Score, EvalError> {
let pred = if self.case_sensitive {
prediction.to_string()
} else {
prediction.to_lowercase()
};
let matches: Vec<bool> = self
.keywords
.iter()
.map(|k| {
let k = if self.case_sensitive {
k.clone()
} else {
k.to_lowercase()
};
pred.contains(&k)
})
.collect();
let ok = if self.all_required {
matches.iter().all(|&m| m)
} else {
matches.iter().any(|&m| m)
};
Ok(Score::new(if ok { 1.0 } else { 0.0 })
.with_label(if ok { "contains" } else { "missing" }))
}
fn name(&self) -> &str {
"contains_keyword"
}
}
pub struct RegexMatch {
pattern: Regex,
}
impl RegexMatch {
pub fn new(pattern: &str) -> Result<Self, EvalError> {
Ok(Self {
pattern: Regex::new(pattern).map_err(|e| EvalError::ParseError(e.to_string()))?,
})
}
}
#[async_trait]
impl Evaluator for RegexMatch {
async fn eval(
&self,
_input: &str,
prediction: &str,
_reference: &str,
) -> Result<Score, EvalError> {
let ok = self.pattern.is_match(prediction);
Ok(Score::new(if ok { 1.0 } else { 0.0 })
.with_label(if ok { "match" } else { "no_match" }))
}
fn name(&self) -> &str {
"regex_match"
}
}
pub struct LengthCheck {
min: Option<usize>,
max: Option<usize>,
}
impl Default for LengthCheck {
fn default() -> Self {
Self::new()
}
}
impl LengthCheck {
pub fn new() -> Self {
Self {
min: None,
max: None,
}
}
pub fn min(mut self, m: usize) -> Self {
self.min = Some(m);
self
}
pub fn max(mut self, m: usize) -> Self {
self.max = Some(m);
self
}
}
#[async_trait]
impl Evaluator for LengthCheck {
async fn eval(
&self,
_input: &str,
prediction: &str,
_reference: &str,
) -> Result<Score, EvalError> {
let len = prediction.chars().count();
let ok = self.min.map_or(true, |m| len >= m) && self.max.map_or(true, |m| len <= m);
Ok(Score::new(if ok { 1.0 } else { 0.0 })
.with_label(if ok { "in_range" } else { "out_of_range" }))
}
fn name(&self) -> &str {
"length_check"
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_contains_all_required() {
let ev = ContainsKeyword::new(vec!["巴黎".into(), "法国".into()]);
assert_eq!(ev.eval("", "巴黎是法国首都", "").await.unwrap().value, 1.0);
assert_eq!(ev.eval("", "巴黎很大", "").await.unwrap().value, 0.0); }
#[tokio::test]
async fn test_contains_any() {
let ev = ContainsKeyword::new(vec!["巴黎".into(), "伦敦".into()]).all_required(false);
assert_eq!(ev.eval("", "伦敦很大", "").await.unwrap().value, 1.0);
assert_eq!(ev.eval("", "柏林很大", "").await.unwrap().value, 0.0);
}
#[tokio::test]
async fn test_contains_case_insensitive() {
let ev = ContainsKeyword::new(vec!["hello".into()]);
assert_eq!(ev.eval("", "Say HELLO world", "").await.unwrap().value, 1.0);
}
#[tokio::test]
async fn test_regex_match() {
let ev = RegexMatch::new(r"\d{4}-\d{2}-\d{2}").unwrap();
assert_eq!(ev.eval("", "日期是 2024-01-15", "").await.unwrap().value, 1.0);
assert_eq!(ev.eval("", "日期是 2024/01/15", "").await.unwrap().value, 0.0);
}
#[tokio::test]
async fn test_length_check() {
let ev = LengthCheck::new().min(2).max(5);
assert_eq!(ev.eval("", "你好", "").await.unwrap().value, 1.0); assert_eq!(ev.eval("", "你好世界测试", "").await.unwrap().value, 0.0); }
#[tokio::test]
async fn test_length_default_passes() {
let ev = LengthCheck::new();
assert_eq!(ev.eval("", "任意长度", "").await.unwrap().value, 1.0);
assert_eq!(ev.eval("", "", "").await.unwrap().value, 1.0);
}
}