use std::sync::Arc;
use crate::content::{Message, Part};
use crate::scorer::Scorer;
use crate::subject::Subject;
use crate::target::Target;
use crate::{Dataset, Metadata, Params, Sample};
pub type Responder = dyn Fn(&[Message]) -> Option<Vec<Part>> + Send + Sync;
#[derive(Clone, Debug, PartialEq)]
pub struct Axis {
pub name: String,
pub values: Vec<String>,
}
impl Axis {
pub fn new(
name: impl Into<String>,
values: impl IntoIterator<Item = impl Into<String>>,
) -> Self {
Self {
name: name.into(),
values: values.into_iter().map(Into::into).collect(),
}
}
}
pub struct Eval {
pub name: String,
pub description: String,
pub dataset: Dataset,
pub subject: Arc<dyn Subject>,
pub scorers: Vec<Box<dyn Scorer>>,
pub targets: Vec<Target>,
pub axes: Vec<Axis>,
pub max_turns: usize,
pub trials: usize,
pub seed: Option<u64>,
pub responder: Option<Arc<Responder>>,
pub metadata: Metadata,
}
impl Eval {
pub fn axis_combinations(&self) -> Vec<Params> {
let mut combos = vec![Params::new()];
for axis in &self.axes {
let mut next = Vec::new();
for combo in &combos {
for value in &axis.values {
let mut c = combo.clone();
c.insert(axis.name.clone(), value.clone());
next.push(c);
}
}
if !next.is_empty() {
combos = next;
}
}
combos
}
}
impl Eval {
#[allow(clippy::new_ret_no_self)]
pub fn new(name: impl Into<String>) -> EvalBuilder {
EvalBuilder {
name: name.into(),
description: String::new(),
dataset: Dataset::default(),
subject: None,
scorers: Vec::new(),
targets: Vec::new(),
axes: Vec::new(),
max_turns: 12,
trials: 1,
seed: None,
responder: None,
metadata: Metadata::new(),
}
}
}
pub struct EvalBuilder {
name: String,
description: String,
dataset: Dataset,
subject: Option<Arc<dyn Subject>>,
scorers: Vec<Box<dyn Scorer>>,
targets: Vec<Target>,
axes: Vec<Axis>,
max_turns: usize,
trials: usize,
seed: Option<u64>,
responder: Option<Arc<Responder>>,
metadata: Metadata,
}
impl EvalBuilder {
pub fn describe(mut self, description: impl Into<String>) -> Self {
self.description = description.into();
self
}
pub fn dataset(mut self, dataset: impl Into<Dataset>) -> Self {
self.dataset = dataset.into();
self
}
pub fn sample(mut self, id: impl Into<String>, prompt: impl Into<String>) -> Self {
self.dataset.samples.push(Sample::new(id, prompt));
self
}
pub fn add_sample(mut self, sample: Sample) -> Self {
self.dataset.samples.push(sample);
self
}
pub fn subject(mut self, subject: impl Subject + 'static) -> Self {
self.subject = Some(Arc::new(subject));
self
}
pub fn subject_arc(mut self, subject: Arc<dyn Subject>) -> Self {
self.subject = Some(subject);
self
}
pub fn scorer(mut self, scorer: Box<dyn Scorer>) -> Self {
self.scorers.push(scorer);
self
}
pub fn target(mut self, target: Target) -> Self {
self.targets.push(target);
self
}
pub fn targets(mut self, targets: impl IntoIterator<Item = Target>) -> Self {
self.targets = targets.into_iter().collect();
self
}
pub fn axis(
mut self,
name: impl Into<String>,
values: impl IntoIterator<Item = impl Into<String>>,
) -> Self {
self.axes.push(Axis::new(name, values));
self
}
pub fn max_turns(mut self, max_turns: usize) -> Self {
self.max_turns = max_turns;
self
}
pub fn trials(mut self, n: usize) -> Self {
self.trials = n.max(1);
self
}
pub fn seed(mut self, seed: u64) -> Self {
self.seed = Some(seed);
self
}
pub fn responder(
mut self,
responder: impl Fn(&[Message]) -> Option<Vec<Part>> + Send + Sync + 'static,
) -> Self {
self.responder = Some(Arc::new(responder));
self
}
pub fn meta(mut self, key: impl Into<String>, value: impl Into<serde_json::Value>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
pub fn build(self) -> Eval {
Eval {
name: self.name,
description: self.description,
dataset: self.dataset,
subject: self.subject.expect("eval requires a subject"),
scorers: self.scorers,
targets: if self.targets.is_empty() {
vec![Target::sim()]
} else {
self.targets
},
axes: self.axes,
max_turns: self.max_turns,
trials: self.trials.max(1),
seed: self.seed,
responder: self.responder,
metadata: self.metadata,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::subject::subject_fn;
use crate::{Transcript, scorer::contains};
#[test]
fn builder_defaults_to_sim() {
let eval = Eval::new("greet")
.sample("hi", "say hi")
.subject(subject_fn(|_, _| async { Transcript::response("hi") }))
.scorer(contains("hi"))
.build();
assert_eq!(eval.targets.len(), 1);
assert!(eval.targets[0].is_sim());
assert_eq!(eval.dataset.len(), 1);
}
#[test]
fn builder_keeps_metadata_and_matrix() {
let eval = Eval::new("e")
.describe("desc")
.meta("suite", "smoke")
.targets([Target::sim(), Target::anthropic("opus")])
.subject(subject_fn(|_, _| async { Transcript::default() }))
.build();
assert_eq!(eval.description, "desc");
assert_eq!(eval.metadata.get("suite").unwrap(), "smoke");
assert_eq!(eval.targets.len(), 2);
}
#[test]
fn trials_default_to_one_and_are_clamped() {
let eval = Eval::new("e")
.sample("a", "x")
.subject(subject_fn(|_, _| async { Transcript::default() }))
.build();
assert_eq!(eval.trials, 1);
assert_eq!(eval.seed, None);
let repeated = Eval::new("e")
.sample("a", "x")
.trials(0) .subject(subject_fn(|_, _| async { Transcript::default() }))
.build();
assert_eq!(repeated.trials, 1);
let seeded = Eval::new("e")
.sample("a", "x")
.trials(8)
.seed(123)
.subject(subject_fn(|_, _| async { Transcript::default() }))
.build();
assert_eq!(seeded.trials, 8);
assert_eq!(seeded.seed, Some(123));
}
}