use std::collections::BTreeMap;
use std::sync::Mutex;
use async_trait::async_trait;
use crate::brain::provider::{LLMRequest, Message, Provider};
const BINEVAL_JUDGE_SYSTEM: &str = "You are a strict evaluator. You are given an ARTIFACT and one \
yes/no QUESTION about it. Answer with a single word on the first line: YES or NO. Judge only \
what the artifact actually contains; if the fact is absent, answer NO.";
#[derive(Debug, Clone)]
pub struct BinaryQuestion {
pub dimension: String,
pub text: String,
}
impl BinaryQuestion {
pub fn new(dimension: impl Into<String>, text: impl Into<String>) -> Self {
Self {
dimension: dimension.into(),
text: text.into(),
}
}
}
#[derive(Debug, Clone)]
pub struct BinaryVerdict {
pub yes: bool,
pub explanation: Option<String>,
}
pub fn parse_verdict(reply: &str) -> bool {
let upper = reply.trim().to_ascii_uppercase();
for token in upper.split(|c: char| !c.is_ascii_alphabetic()) {
match token {
"YES" => return true,
"NO" => return false,
_ => {}
}
}
false
}
#[async_trait]
pub trait Judge: Send + Sync {
async fn judge(&self, question: &str, artifact: &str) -> BinaryVerdict;
}
pub struct SequenceJudge {
verdicts: Mutex<std::collections::VecDeque<bool>>,
}
impl SequenceJudge {
pub fn new(verdicts: impl IntoIterator<Item = bool>) -> Self {
Self {
verdicts: Mutex::new(verdicts.into_iter().collect()),
}
}
}
#[async_trait]
impl Judge for SequenceJudge {
async fn judge(&self, _question: &str, _artifact: &str) -> BinaryVerdict {
let yes = self
.verdicts
.lock()
.unwrap_or_else(|e| e.into_inner())
.pop_front()
.unwrap_or(false);
BinaryVerdict {
yes,
explanation: None,
}
}
}
pub struct ProviderJudge<'a> {
provider: &'a dyn Provider,
model: String,
}
impl<'a> ProviderJudge<'a> {
pub fn new(provider: &'a dyn Provider, model: impl Into<String>) -> Self {
Self {
provider,
model: model.into(),
}
}
}
pub async fn judge_via_provider(
provider: &dyn Provider,
model: &str,
question: &str,
artifact: &str,
) -> BinaryVerdict {
let prompt = format!("ARTIFACT:\n{artifact}\n\nQUESTION: {question}");
let request = LLMRequest::new(model.to_string(), vec![Message::user(prompt)])
.with_system(BINEVAL_JUDGE_SYSTEM);
match provider.complete(request).await {
Ok(resp) => {
let text = resp
.content
.iter()
.filter_map(|b| match b {
crate::brain::provider::ContentBlock::Text { text } => Some(text.as_str()),
_ => None,
})
.collect::<Vec<_>>()
.join("\n");
BinaryVerdict {
yes: parse_verdict(&text),
explanation: Some(text),
}
}
Err(e) => BinaryVerdict {
yes: false,
explanation: Some(format!("judge error: {e}")),
},
}
}
#[async_trait]
impl Judge for ProviderJudge<'_> {
async fn judge(&self, question: &str, artifact: &str) -> BinaryVerdict {
judge_via_provider(self.provider, &self.model, question, artifact).await
}
}
#[derive(Debug, Clone, Default)]
pub struct DimensionScore {
pub passed: usize,
pub total: usize,
}
impl DimensionScore {
pub fn fraction(&self) -> f64 {
if self.total == 0 {
0.0
} else {
self.passed as f64 / self.total as f64
}
}
}
#[derive(Debug, Clone)]
pub struct Scorecard {
pub results: Vec<(BinaryQuestion, BinaryVerdict)>,
pub per_dimension: BTreeMap<String, DimensionScore>,
pub passed: usize,
pub total: usize,
}
impl Scorecard {
pub fn from_verdicts(results: Vec<(BinaryQuestion, BinaryVerdict)>) -> Self {
let mut per_dimension: BTreeMap<String, DimensionScore> = BTreeMap::new();
let mut passed = 0;
for (q, v) in &results {
let entry = per_dimension.entry(q.dimension.clone()).or_default();
entry.total += 1;
if v.yes {
entry.passed += 1;
passed += 1;
}
}
let total = results.len();
Self {
results,
per_dimension,
passed,
total,
}
}
pub fn overall(&self) -> f64 {
if self.total == 0 {
0.0
} else {
self.passed as f64 / self.total as f64
}
}
pub fn passes(&self, threshold: f64) -> bool {
self.overall() >= threshold
}
pub fn render(&self) -> String {
let mut out = format!(
"overall: {}/{} ({:.0}%)\n",
self.passed,
self.total,
self.overall() * 100.0
);
for (dim, score) in &self.per_dimension {
out.push_str(&format!(
" {dim}: {}/{} ({:.0}%)\n",
score.passed,
score.total,
score.fraction() * 100.0
));
}
out
}
}
pub async fn score(judge: &dyn Judge, questions: Vec<BinaryQuestion>, artifact: &str) -> Scorecard {
let mut results = Vec::with_capacity(questions.len());
for q in questions {
let verdict = judge.judge(&q.text, artifact).await;
results.push((q, verdict));
}
Scorecard::from_verdicts(results)
}