Class: Ask::Eval::Experiment
- Inherits:
-
Object
- Object
- Ask::Eval::Experiment
- Defined in:
- lib/ask/eval/experiment.rb
Overview
Runs a Dataset against a single variant and compares runs.
An experiment executes every dataset item through a runner (a
callable that turns an input into the agent's output), optionally
scores each output, and collects per-item results. Running the same
dataset with two different runners (a changed prompt, a different
model) and comparing the experiments is the A/B-testing loop of
eval-driven development.
Defined Under Namespace
Classes: Result
Instance Attribute Summary collapse
- #dataset ⇒ Ask::Eval::Dataset readonly
Instance Method Summary collapse
-
#compare(other) ⇒ Hash
Side-by-side comparison with another experiment over the same (or overlapping) dataset items.
-
#initialize(dataset, runner:, scorer: nil) ⇒ Experiment
constructor
A new instance of Experiment.
-
#results ⇒ Array<Result>
Per-item results (runs first if needed).
-
#run ⇒ self
Execute every dataset item through the runner.
-
#summary ⇒ Hash
Aggregate statistics.
Constructor Details
#initialize(dataset, runner:, scorer: nil) ⇒ Experiment
Returns a new instance of Experiment.
37 38 39 40 41 42 |
# File 'lib/ask/eval/experiment.rb', line 37 def initialize(dataset, runner:, scorer: nil) @dataset = dataset @runner = runner @scorer = scorer @results = nil end |
Instance Attribute Details
#dataset ⇒ Ask::Eval::Dataset (readonly)
45 46 47 |
# File 'lib/ask/eval/experiment.rb', line 45 def dataset @dataset end |
Instance Method Details
#compare(other) ⇒ Hash
Side-by-side comparison with another experiment over the same (or overlapping) dataset items.
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 |
# File 'lib/ask/eval/experiment.rb', line 97 def compare(other) a = results b = other.results ha = a.to_h { |r| [r.item.id, r] } hb = b.to_h { |r| [r.item.id, r] } deltas = (ha.keys & hb.keys).map do |id| ra = ha[id] rb = hb[id] { item_id: id, input: ra.item.input, a: { output: ra.output, score: ra.score, error: ra.error }, b: { output: rb.output, score: rb.score, error: rb.error }, delta: (ra.score && rb.score) ? (rb.score - ra.score).round(3) : nil } end { deltas: deltas, a: summary, b: other.summary, verdict: verdict(summary, other.summary) } end |
#results ⇒ Array<Result>
Returns per-item results (runs first if needed).
74 75 76 |
# File 'lib/ask/eval/experiment.rb', line 74 def results @results || run.results end |
#run ⇒ self
Execute every dataset item through the runner. A runner error is recorded on that item and the run continues.
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
# File 'lib/ask/eval/experiment.rb', line 51 def run @results = @dataset.items.map do |item| start = Process.clock_gettime(Process::CLOCK_MONOTONIC) output = nil error = nil begin output = @runner.call(item.input).to_s rescue StandardError => e error = e. end score = nil if error.nil? && @scorer score = @scorer.call(input: item.input, output: output, expected: item.expected) end duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000).round Result.new(item: item, output: output, score: score, error: error, duration_ms: duration_ms) end self end |
#summary ⇒ Hash
Returns aggregate statistics.
79 80 81 82 83 84 85 86 87 88 89 |
# File 'lib/ask/eval/experiment.rb', line 79 def summary rs = results scores = rs.filter_map(&:score) { total: rs.size, passed: rs.count(&:passed?), failed: rs.count { |r| !r.passed? }, avg_score: scores.empty? ? nil : (scores.sum.fdiv(scores.size)).round(3), total_duration_ms: rs.sum(&:duration_ms) } end |