use crate::error::Result;
use crate::substrate::{ReadOpts, SubstrateRead};
use serde_json::Value;
pub const EVAL_RUN_RELATION: &str = "mg:eval_run";
pub const HARNESS_NS: &str = "agent:harness";
#[derive(Debug, Clone)]
pub struct EvalRun {
pub run_id: String,
pub passed: u64,
pub failed: u64,
pub summary: serde_json::Map<String, Value>,
pub recorded_ms: i64,
}
impl EvalRun {
pub fn field(&self, name: &str) -> Option<f64> {
self.summary.get(name).and_then(Value::as_f64)
}
pub fn total(&self) -> u64 {
self.passed.saturating_add(self.failed)
}
}
pub fn eval_runs<S: SubstrateRead + ?Sized>(
sub: &S,
evalset_hash: &str,
since_ms: Option<i64>,
) -> Result<Vec<EvalRun>> {
let subject = format!("evalset:{evalset_hash}");
let facts = sub.grains_of_type(
crate::model::grain_type::FACT,
Some(HARNESS_NS),
ReadOpts { live_only: true, since_ms },
)?;
let mut out: Vec<EvalRun> = facts
.iter()
.filter(|f| f.str_field("relation") == Some(EVAL_RUN_RELATION))
.filter(|f| f.str_field("subject") == Some(subject.as_str()))
.filter_map(|f| {
let obj = f.str_field("object")?;
let Ok(Value::Object(summary)) = serde_json::from_str::<Value>(obj) else {
return None;
};
Some(EvalRun {
run_id: summary.get("run_id").and_then(Value::as_str)?.to_string(),
passed: summary.get("passed").and_then(Value::as_u64)?,
failed: summary.get("failed").and_then(Value::as_u64)?,
recorded_ms: f.created_at_ms,
summary,
})
})
.collect();
out.sort_by(|a, b| {
a.recorded_ms
.cmp(&b.recorded_ms)
.then_with(|| a.run_id.cmp(&b.run_id))
});
Ok(out)
}
pub fn newest_eval_run<S: SubstrateRead + ?Sized>(
sub: &S,
evalset_hash: &str,
since_ms: Option<i64>,
) -> Result<Option<EvalRun>> {
Ok(eval_runs(sub, evalset_hash, since_ms)?.pop())
}
pub fn newest_eval_run_before<S: SubstrateRead + ?Sized>(
sub: &S,
evalset_hash: &str,
before_ms: i64,
) -> Result<Option<EvalRun>> {
Ok(eval_runs(sub, evalset_hash, None)?
.into_iter()
.rfind(|r| r.recorded_ms < before_ms))
}
pub fn run_value(run: &EvalRun, field: &str) -> Option<f64> {
match field {
"failed" => Some(run.failed as f64),
"passed" => Some(run.passed as f64),
"total" => Some(run.total() as f64),
"error_rate" => match run.total() {
0 => None,
t => Some(run.failed as f64 / t as f64),
},
other => run.field(other),
}
}
pub fn eval_run_by_id<S: SubstrateRead + ?Sized>(
sub: &S,
evalset_hash: &str,
run_id: &str,
) -> Result<Option<EvalRun>> {
Ok(eval_runs(sub, evalset_hash, None)?
.into_iter()
.find(|r| r.run_id == run_id))
}
pub fn parse_evalset_metric(metric: &str) -> Option<(&str, &str)> {
let rest = metric.strip_prefix("evalset:")?;
let (hash, field) = rest.rsplit_once(':')?;
if hash.is_empty() || field.is_empty() || hash.contains(':') {
return None;
}
Some((hash, field))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn metric_strings_parse_into_hash_and_field() {
assert_eq!(
parse_evalset_metric("evalset:abc123:category_accuracy"),
Some(("abc123", "category_accuracy"))
);
assert_eq!(parse_evalset_metric("evalset:abc123:failed"), Some(("abc123", "failed")));
assert_eq!(parse_evalset_metric("tool_error_recurrence"), None);
assert_eq!(parse_evalset_metric("evalset:abc123"), None);
assert_eq!(parse_evalset_metric("evalset::field"), None);
assert_eq!(parse_evalset_metric("evalset:abc123:"), None);
assert_eq!(parse_evalset_metric("evalset:a:b:c"), None);
}
}