Skip to main content

synth_ai_core/data/
judgements.rs

1//! Judgement and rubric assignment types.
2//!
3//! Types for recording evaluation results and criterion scores.
4
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7use std::collections::HashMap;
8
9/// Score data for a single criterion.
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct CriterionScoreData {
12    /// The numeric score.
13    pub score: f64,
14    /// Explanation/reasoning for the score.
15    #[serde(default)]
16    pub reason: Option<String>,
17    /// Weight used in aggregation.
18    #[serde(default = "default_weight")]
19    pub weight: f64,
20    /// Normalized score (0-1 range).
21    #[serde(default)]
22    pub normalized_score: Option<f64>,
23    /// Whether this criterion passed (for required criteria).
24    #[serde(default)]
25    pub passed: Option<bool>,
26}
27
28fn default_weight() -> f64 {
29    1.0
30}
31
32impl CriterionScoreData {
33    /// Create a new criterion score.
34    pub fn new(score: f64) -> Self {
35        Self {
36            score,
37            reason: None,
38            weight: 1.0,
39            normalized_score: None,
40            passed: None,
41        }
42    }
43
44    /// Create a score with reason.
45    pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
46        self.reason = Some(reason.into());
47        self
48    }
49
50    /// Set the weight.
51    pub fn with_weight(mut self, weight: f64) -> Self {
52        self.weight = weight;
53        self
54    }
55
56    /// Mark as passed/failed.
57    pub fn with_passed(mut self, passed: bool) -> Self {
58        self.passed = Some(passed);
59        self
60    }
61
62    /// Calculate weighted score.
63    pub fn weighted_score(&self) -> f64 {
64        self.score * self.weight
65    }
66}
67
68impl Default for CriterionScoreData {
69    fn default() -> Self {
70        Self::new(0.0)
71    }
72}
73
74/// Assignment of scores to a rubric's criteria.
75#[derive(Debug, Clone, Default, Serialize, Deserialize)]
76pub struct RubricAssignment {
77    /// Map of criterion ID to score data.
78    #[serde(default)]
79    pub criterion_scores: HashMap<String, CriterionScoreData>,
80    /// Aggregated total score.
81    #[serde(default)]
82    pub total: f64,
83    /// Reference to the rubric used.
84    #[serde(default)]
85    pub rubric_ref: Option<String>,
86    /// Summary of the evaluation.
87    #[serde(default)]
88    pub summary: Option<String>,
89    /// Whether all required criteria passed.
90    #[serde(default)]
91    pub all_required_passed: Option<bool>,
92    /// Normalized total (0-1 range).
93    #[serde(default)]
94    pub normalized_total: Option<f64>,
95}
96
97impl RubricAssignment {
98    /// Create a new rubric assignment.
99    pub fn new() -> Self {
100        Self::default()
101    }
102
103    /// Add a criterion score.
104    pub fn with_score(mut self, criterion_id: impl Into<String>, score: CriterionScoreData) -> Self {
105        self.criterion_scores.insert(criterion_id.into(), score);
106        self
107    }
108
109    /// Set the total score.
110    pub fn with_total(mut self, total: f64) -> Self {
111        self.total = total;
112        self
113    }
114
115    /// Set the rubric reference.
116    pub fn with_rubric_ref(mut self, rubric_ref: impl Into<String>) -> Self {
117        self.rubric_ref = Some(rubric_ref.into());
118        self
119    }
120
121    /// Set the summary.
122    pub fn with_summary(mut self, summary: impl Into<String>) -> Self {
123        self.summary = Some(summary.into());
124        self
125    }
126
127    /// Calculate total from criterion scores using weighted sum.
128    pub fn calculate_weighted_total(&mut self) {
129        let total_weight: f64 = self.criterion_scores.values().map(|s| s.weight).sum();
130        if total_weight > 0.0 {
131            let weighted_sum: f64 = self.criterion_scores.values().map(|s| s.weighted_score()).sum();
132            self.total = weighted_sum / total_weight;
133        }
134    }
135
136    /// Get score for a criterion.
137    pub fn get_score(&self, criterion_id: &str) -> Option<f64> {
138        self.criterion_scores.get(criterion_id).map(|s| s.score)
139    }
140}
141
142/// A complete judgement including rubric assignment and annotations.
143#[derive(Debug, Clone, Default, Serialize, Deserialize)]
144pub struct Judgement {
145    /// The rubric-based evaluation.
146    #[serde(default)]
147    pub rubric_assignment: Option<RubricAssignment>,
148    /// Free-form annotations.
149    #[serde(default)]
150    pub annotation: HashMap<String, Value>,
151    /// Overall pass/fail determination.
152    #[serde(default)]
153    pub passed: Option<bool>,
154    /// Confidence in the judgement (0-1).
155    #[serde(default)]
156    pub confidence: Option<f64>,
157    /// Source of the judgement (e.g., "verifier", "human", "model").
158    #[serde(default)]
159    pub source: Option<String>,
160    /// Timestamp of when judgement was made.
161    #[serde(default)]
162    pub judged_at: Option<String>,
163}
164
165impl Judgement {
166    /// Create a new judgement.
167    pub fn new() -> Self {
168        Self::default()
169    }
170
171    /// Set the rubric assignment.
172    pub fn with_rubric_assignment(mut self, assignment: RubricAssignment) -> Self {
173        self.rubric_assignment = Some(assignment);
174        self
175    }
176
177    /// Add an annotation.
178    pub fn with_annotation(mut self, key: impl Into<String>, value: Value) -> Self {
179        self.annotation.insert(key.into(), value);
180        self
181    }
182
183    /// Set passed status.
184    pub fn with_passed(mut self, passed: bool) -> Self {
185        self.passed = Some(passed);
186        self
187    }
188
189    /// Set confidence.
190    pub fn with_confidence(mut self, confidence: f64) -> Self {
191        self.confidence = Some(confidence.clamp(0.0, 1.0));
192        self
193    }
194
195    /// Set source.
196    pub fn with_source(mut self, source: impl Into<String>) -> Self {
197        self.source = Some(source.into());
198        self
199    }
200
201    /// Get the total score from the rubric assignment.
202    pub fn total_score(&self) -> Option<f64> {
203        self.rubric_assignment.as_ref().map(|a| a.total)
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    #[test]
212    fn test_criterion_score() {
213        let score = CriterionScoreData::new(8.5)
214            .with_reason("Good explanation")
215            .with_weight(2.0);
216
217        assert_eq!(score.score, 8.5);
218        assert_eq!(score.weighted_score(), 17.0);
219    }
220
221    #[test]
222    fn test_rubric_assignment() {
223        let mut assignment = RubricAssignment::new()
224            .with_score("clarity", CriterionScoreData::new(9.0).with_weight(1.0))
225            .with_score("accuracy", CriterionScoreData::new(7.0).with_weight(2.0))
226            .with_rubric_ref("eval_v1");
227
228        assignment.calculate_weighted_total();
229
230        // Weighted average: (9*1 + 7*2) / (1+2) = 23/3 ≈ 7.67
231        assert!((assignment.total - 7.666).abs() < 0.01);
232    }
233
234    #[test]
235    fn test_judgement() {
236        let assignment = RubricAssignment::new()
237            .with_total(8.5)
238            .with_summary("Good overall performance");
239
240        let judgement = Judgement::new()
241            .with_rubric_assignment(assignment)
242            .with_passed(true)
243            .with_confidence(0.95)
244            .with_source("verifier");
245
246        assert_eq!(judgement.total_score(), Some(8.5));
247        assert_eq!(judgement.passed, Some(true));
248        assert_eq!(judgement.confidence, Some(0.95));
249    }
250
251    #[test]
252    fn test_serde() {
253        let judgement = Judgement::new()
254            .with_passed(true)
255            .with_annotation("note", serde_json::json!("test"));
256
257        let json = serde_json::to_string(&judgement).unwrap();
258        let parsed: Judgement = serde_json::from_str(&json).unwrap();
259
260        assert_eq!(parsed.passed, Some(true));
261        assert_eq!(parsed.annotation.get("note"), Some(&serde_json::json!("test")));
262    }
263}