use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CriterionScoreData {
pub score: f64,
#[serde(default)]
pub reason: Option<String>,
#[serde(default = "default_weight")]
pub weight: f64,
#[serde(default)]
pub normalized_score: Option<f64>,
#[serde(default)]
pub passed: Option<bool>,
}
fn default_weight() -> f64 {
1.0
}
impl CriterionScoreData {
pub fn new(score: f64) -> Self {
Self {
score,
reason: None,
weight: 1.0,
normalized_score: None,
passed: None,
}
}
pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
self.reason = Some(reason.into());
self
}
pub fn with_weight(mut self, weight: f64) -> Self {
self.weight = weight;
self
}
pub fn with_passed(mut self, passed: bool) -> Self {
self.passed = Some(passed);
self
}
pub fn weighted_score(&self) -> f64 {
self.score * self.weight
}
}
impl Default for CriterionScoreData {
fn default() -> Self {
Self::new(0.0)
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct RubricAssignment {
#[serde(default)]
pub criterion_scores: HashMap<String, CriterionScoreData>,
#[serde(default)]
pub total: f64,
#[serde(default)]
pub rubric_ref: Option<String>,
#[serde(default)]
pub summary: Option<String>,
#[serde(default)]
pub all_required_passed: Option<bool>,
#[serde(default)]
pub normalized_total: Option<f64>,
}
impl RubricAssignment {
pub fn new() -> Self {
Self::default()
}
pub fn with_score(
mut self,
criterion_id: impl Into<String>,
score: CriterionScoreData,
) -> Self {
self.criterion_scores.insert(criterion_id.into(), score);
self
}
pub fn with_total(mut self, total: f64) -> Self {
self.total = total;
self
}
pub fn with_rubric_ref(mut self, rubric_ref: impl Into<String>) -> Self {
self.rubric_ref = Some(rubric_ref.into());
self
}
pub fn with_summary(mut self, summary: impl Into<String>) -> Self {
self.summary = Some(summary.into());
self
}
pub fn calculate_weighted_total(&mut self) {
let total_weight: f64 = self.criterion_scores.values().map(|s| s.weight).sum();
if total_weight > 0.0 {
let weighted_sum: f64 = self
.criterion_scores
.values()
.map(|s| s.weighted_score())
.sum();
self.total = weighted_sum / total_weight;
}
}
pub fn get_score(&self, criterion_id: &str) -> Option<f64> {
self.criterion_scores.get(criterion_id).map(|s| s.score)
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Judgement {
#[serde(default)]
pub rubric_assignment: Option<RubricAssignment>,
#[serde(default)]
pub annotation: HashMap<String, Value>,
#[serde(default)]
pub passed: Option<bool>,
#[serde(default)]
pub confidence: Option<f64>,
#[serde(default)]
pub source: Option<String>,
#[serde(default)]
pub judged_at: Option<String>,
}
impl Judgement {
pub fn new() -> Self {
Self::default()
}
pub fn with_rubric_assignment(mut self, assignment: RubricAssignment) -> Self {
self.rubric_assignment = Some(assignment);
self
}
pub fn with_annotation(mut self, key: impl Into<String>, value: Value) -> Self {
self.annotation.insert(key.into(), value);
self
}
pub fn with_passed(mut self, passed: bool) -> Self {
self.passed = Some(passed);
self
}
pub fn with_confidence(mut self, confidence: f64) -> Self {
self.confidence = Some(confidence.clamp(0.0, 1.0));
self
}
pub fn with_source(mut self, source: impl Into<String>) -> Self {
self.source = Some(source.into());
self
}
pub fn total_score(&self) -> Option<f64> {
self.rubric_assignment.as_ref().map(|a| a.total)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_criterion_score() {
let score = CriterionScoreData::new(8.5)
.with_reason("Good explanation")
.with_weight(2.0);
assert_eq!(score.score, 8.5);
assert_eq!(score.weighted_score(), 17.0);
}
#[test]
fn test_rubric_assignment() {
let mut assignment = RubricAssignment::new()
.with_score("clarity", CriterionScoreData::new(9.0).with_weight(1.0))
.with_score("accuracy", CriterionScoreData::new(7.0).with_weight(2.0))
.with_rubric_ref("eval_v1");
assignment.calculate_weighted_total();
assert!((assignment.total - 7.666).abs() < 0.01);
}
#[test]
fn test_judgement() {
let assignment = RubricAssignment::new()
.with_total(8.5)
.with_summary("Good overall performance");
let judgement = Judgement::new()
.with_rubric_assignment(assignment)
.with_passed(true)
.with_confidence(0.95)
.with_source("verifier");
assert_eq!(judgement.total_score(), Some(8.5));
assert_eq!(judgement.passed, Some(true));
assert_eq!(judgement.confidence, Some(0.95));
}
#[test]
fn test_serde() {
let judgement = Judgement::new()
.with_passed(true)
.with_annotation("note", serde_json::json!("test"));
let json = serde_json::to_string(&judgement).unwrap();
let parsed: Judgement = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.passed, Some(true));
assert_eq!(
parsed.annotation.get("note"),
Some(&serde_json::json!("test"))
);
}
}