agent-evals 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +16 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +635 -0
  5. data/exe/agent-evals +9 -0
  6. data/lib/agent/evals/adapter.rb +47 -0
  7. data/lib/agent/evals/case.rb +36 -0
  8. data/lib/agent/evals/cli/cases.rb +48 -0
  9. data/lib/agent/evals/cli/command.rb +163 -0
  10. data/lib/agent/evals/cli/compare.rb +33 -0
  11. data/lib/agent/evals/cli/model_command.rb +393 -0
  12. data/lib/agent/evals/cli/progress_bars.rb +90 -0
  13. data/lib/agent/evals/cli/run.rb +61 -0
  14. data/lib/agent/evals/cli/verify.rb +24 -0
  15. data/lib/agent/evals/cli/version.rb +15 -0
  16. data/lib/agent/evals/cli.rb +40 -0
  17. data/lib/agent/evals/completion.rb +11 -0
  18. data/lib/agent/evals/config.rb +146 -0
  19. data/lib/agent/evals/configuration.rb +172 -0
  20. data/lib/agent/evals/contender.rb +31 -0
  21. data/lib/agent/evals/discovery.rb +95 -0
  22. data/lib/agent/evals/experiment.rb +133 -0
  23. data/lib/agent/evals/helper.rb +98 -0
  24. data/lib/agent/evals/launcher.rb +37 -0
  25. data/lib/agent/evals/metric.rb +26 -0
  26. data/lib/agent/evals/model.rb +55 -0
  27. data/lib/agent/evals/providers/base.rb +93 -0
  28. data/lib/agent/evals/providers/catalog.rb +32 -0
  29. data/lib/agent/evals/providers/open_router.rb +64 -0
  30. data/lib/agent/evals/providers/registry.rb +112 -0
  31. data/lib/agent/evals/providers/type_safe.rb +53 -0
  32. data/lib/agent/evals/reply.rb +18 -0
  33. data/lib/agent/evals/run_result.rb +14 -0
  34. data/lib/agent/evals/runtime.rb +74 -0
  35. data/lib/agent/evals/schema_validator.rb +84 -0
  36. data/lib/agent/evals/score.rb +26 -0
  37. data/lib/agent/evals/scorer.rb +45 -0
  38. data/lib/agent/evals/testing/echo_adapter.rb +29 -0
  39. data/lib/agent/evals/testing/fake_model.rb +18 -0
  40. data/lib/agent/evals/testing/fake_provider.rb +67 -0
  41. data/lib/agent/evals/tracker.rb +80 -0
  42. data/lib/agent/evals/trackers/braintrust.rb +91 -0
  43. data/lib/agent/evals/trackers/local.rb +78 -0
  44. data/lib/agent/evals/trackers/logfire.rb +143 -0
  45. data/lib/agent/evals/trackers/registry.rb +82 -0
  46. data/lib/agent/evals/usage.rb +33 -0
  47. data/lib/agent/evals/verify.rb +49 -0
  48. data/lib/agent/evals/version.rb +7 -0
  49. data/lib/agent/evals/warnings.rb +89 -0
  50. data/lib/agent/evals.rb +82 -0
  51. metadata +257 -0
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Agent
4
+ module Evals
5
+ # What one call cost, as the provider counted it. Summed over a run.
6
+ Usage = Data.define(:input_tokens, :output_tokens, :cost_usd) do
7
+ # @return [Usage] zero of everything
8
+ def self.zero = new(input_tokens: 0, output_tokens: 0, cost_usd: 0.0)
9
+
10
+ # Reads a Hash with Symbol or String keys, missing keys as zero.
11
+ #
12
+ # @param hash [Hash, Usage, nil]
13
+ # @return [Usage]
14
+ def self.from(hash)
15
+ return hash if hash.is_a?(Usage)
16
+ return zero if hash.nil?
17
+
18
+ h = hash.transform_keys(&:to_sym)
19
+ new(input_tokens: h.fetch(:input_tokens, 0).to_i,
20
+ output_tokens: h.fetch(:output_tokens, 0).to_i,
21
+ cost_usd: h.fetch(:cost_usd, 0.0).to_f)
22
+ end
23
+
24
+ # @param other [Usage]
25
+ # @return [Usage]
26
+ def +(other)
27
+ with(input_tokens: input_tokens + other.input_tokens,
28
+ output_tokens: output_tokens + other.output_tokens,
29
+ cost_usd: cost_usd + other.cost_usd)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Agent
4
+ module Evals
5
+ # Offline consistency checks over every case, so a typo in the suite is
6
+ # caught before it is billed as a model's mistake. The gem checks what
7
+ # every suite has: an input and a parsable expected answer. The adapter
8
+ # checks each dataset's shared files, and the scorer each case against
9
+ # what it will demand of it.
10
+ class Verify
11
+ # @param configuration [Configuration]
12
+ def initialize(configuration = Agent::Evals.configuration)
13
+ @configuration = configuration
14
+ end
15
+
16
+ # @return [Hash{String => Array<String>}] problems by "dataset/id"; empty when all is sound
17
+ def problems
18
+ discovery = @configuration.discovery
19
+ failures = {}
20
+ discovery.datasets.each do |dataset|
21
+ Array(adapter.verify(dataset)).each { (failures[dataset.to_s] ||= []) << it }
22
+ discovery.cases(dataset: dataset).each do |kase|
23
+ list = case_problems(kase)
24
+ failures[kase.key] = list unless list.empty?
25
+ end
26
+ end
27
+ failures
28
+ end
29
+
30
+ # @param kase [Case]
31
+ # @return [Array<String>]
32
+ def case_problems(kase)
33
+ problems = []
34
+ problems << "input is empty" if kase.input.empty?
35
+ problems << kase.metadata[:expected_error] if kase.metadata[:expected_error]
36
+ return problems unless kase.expected?
37
+
38
+ problems.concat(Array(scorer.verify(kase, context: adapter.context(kase))))
39
+ rescue StandardError => e
40
+ problems << "#{e.class}: #{e.message}"
41
+ end
42
+
43
+ private
44
+
45
+ def adapter = @configuration.adapter
46
+ def scorer = @configuration.scorer
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Agent
4
+ module Evals
5
+ VERSION = "0.1.1"
6
+ end
7
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+
5
+ module Agent
6
+ module Evals
7
+ # Everything a run would otherwise print as a warning while its progress
8
+ # bars redraw: cases that failed, and what Braintrust, RubyLLM and Ruby
9
+ # itself log. Printed among the bars, each one breaks the redraw; collected
10
+ # here, they are saved to one file, and the run prints only how many.
11
+ #
12
+ # It quacks as an IO, so a Logger, and $stderr, can write to it. Threads
13
+ # share it.
14
+ class Warnings
15
+ def initialize
16
+ @lines = []
17
+ @lock = Mutex.new
18
+ end
19
+
20
+ # @param text [#to_s]
21
+ # @return [self]
22
+ def record(text)
23
+ text = text.to_s.chomp
24
+ @lock.synchronize { @lines << text } unless text.strip.empty?
25
+ self
26
+ end
27
+
28
+ # @return [Integer] the bytes "written", as IO#write does
29
+ def write(*texts)
30
+ texts.each { record(it) }
31
+ texts.sum { it.to_s.bytesize }
32
+ end
33
+
34
+ def puts(*texts) = texts.each { record(it) } && nil
35
+ def print(*texts) = write(*texts) && nil
36
+ def <<(text) = record(text)
37
+ def flush = self
38
+ def sync = true
39
+ def sync=(_value); end
40
+ def close = nil
41
+ def tty? = false
42
+
43
+ # @return [Integer]
44
+ def count = @lock.synchronize { @lines.size }
45
+
46
+ # @return [Array<String>]
47
+ def to_a = @lock.synchronize { @lines.dup }
48
+
49
+ # @return [Boolean] whether nothing was recorded
50
+ def none? = count.zero?
51
+
52
+ # @param path [String]
53
+ # @return [String] the path
54
+ def save(path)
55
+ File.write(path, "#{to_a.join("\n\n")}\n")
56
+ path
57
+ end
58
+
59
+ # Runs the block with $stderr, Ruby's own warnings, and the loggers of
60
+ # whichever optional gems are loaded writing here, and puts them back after.
61
+ #
62
+ # @return [Object] what the block returns
63
+ def capture
64
+ logger = Logger.new(self, level: Logger::WARN)
65
+ stderr = $stderr
66
+ restore = redirect_loggers(logger)
67
+ $stderr = self
68
+ yield
69
+ ensure
70
+ restore&.call
71
+ $stderr = stderr
72
+ end
73
+
74
+ private
75
+
76
+ # @return [Proc] puts the loggers back
77
+ def redirect_loggers(logger)
78
+ braintrust = defined?(Braintrust::Log) ? Braintrust::Log.logger : nil
79
+ ruby_llm = defined?(RubyLLM) ? RubyLLM.instance_variable_get(:@logger) : nil
80
+ Braintrust::Log.logger = logger if defined?(Braintrust::Log)
81
+ RubyLLM.instance_variable_set(:@logger, logger) if defined?(RubyLLM)
82
+ lambda do
83
+ Braintrust::Log.logger = braintrust if defined?(Braintrust::Log)
84
+ RubyLLM.instance_variable_set(:@logger, ruby_llm) if defined?(RubyLLM)
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "dry/cli"
5
+ require "dry/cli/help"
6
+ require "dry/cli/ui"
7
+ require "dry/cli/autocomplete/command"
8
+ require "zeitwerk"
9
+
10
+ module Agent
11
+ # A generic evaluation runner with a one-way boundary. The consumer suite
12
+ # registers an {Adapter}, which runs one {Case} against a {Model} and
13
+ # returns a {Reply} without scoring it, and a {Scorer}, which turns a reply
14
+ # into a {Score} without calling anything. Everything else, from discovery
15
+ # to the leaderboard, is this gem's, and none of it knows what is being
16
+ # evaluated.
17
+ module Evals
18
+ class Error < StandardError; end
19
+
20
+ # Raised by a command that needs an optional gem the bundle does not hold.
21
+ class MissingDependency < Error; end
22
+
23
+ class << self
24
+ # The suite's settings, filled in by {.configure} from `evals/eval_helper.rb`.
25
+ #
26
+ # @return [Configuration]
27
+ def configuration
28
+ @configuration ||= Configuration.new
29
+ end
30
+
31
+ # Builds the suite's configuration. Called once from the eval helper.
32
+ #
33
+ # @example evals/eval_helper.rb
34
+ # Agent::Evals.configure do |c|
35
+ # c.suite "extract", root: __dir__, env_files: ["../.env"]
36
+ # c.cases "datasets/*/cases/**/input.txt"
37
+ # c.adapter ExtractAdapter.new
38
+ # c.scorer ExtractScorer.new
39
+ # c.before_run { MyGem.reset_configuration! }
40
+ # end
41
+ #
42
+ # @yieldparam configuration [Configuration]
43
+ # @return [Configuration]
44
+ def configure
45
+ yield configuration
46
+ configuration
47
+ end
48
+
49
+ # Forgets the configuration, so a spec that loads a second helper starts clean.
50
+ #
51
+ # @return [void]
52
+ def reset!
53
+ @configuration = nil
54
+ end
55
+
56
+ # @return [Boolean] whether a helper has registered an adapter and a scorer
57
+ def configured? = configuration.complete?
58
+
59
+ # Requires an optional gem the moment a command needs it, and names it
60
+ # when it is missing, instead of letting a bare LoadError surface three
61
+ # frames deep in a provider.
62
+ #
63
+ # @param feature [String] what to require, e.g. "ruby_llm"
64
+ # @param gem_name [String] the gem to add to the Gemfile; the feature when they agree
65
+ # @param needed_for [String] what the user was doing
66
+ # @return [void]
67
+ # @raise [MissingDependency]
68
+ def optional!(feature, gem_name = feature, needed_for:)
69
+ require feature
70
+ rescue LoadError
71
+ raise MissingDependency, "#{needed_for} needs the #{gem_name} gem: add `gem \"#{gem_name}\"` to your Gemfile"
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ require_relative "evals/version"
78
+
79
+ loader = Zeitwerk::Loader.for_gem_extension(Agent)
80
+ loader.inflector.inflect("cli" => "CLI")
81
+ loader.ignore(File.expand_path("evals/version.rb", __dir__))
82
+ loader.setup
metadata ADDED
@@ -0,0 +1,257 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: agent-evals
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Konstantin Gredeskoul
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: dotenv
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ -
17
+ - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: "3.2"
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ -
25
+ - ~>
26
+ - !ruby/object:Gem::Version
27
+ version: "3.2"
28
+ - !ruby/object:Gem::Dependency
29
+ name: dry-cli
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ -
33
+ - ~>
34
+ - !ruby/object:Gem::Version
35
+ version: "1.4"
36
+ type: :runtime
37
+ prerelease: false
38
+ version_requirements: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ -
41
+ - ~>
42
+ - !ruby/object:Gem::Version
43
+ version: "1.4"
44
+ - !ruby/object:Gem::Dependency
45
+ name: dry-cli-autocomplete
46
+ requirement: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ -
49
+ - ~>
50
+ - !ruby/object:Gem::Version
51
+ version: "0.6"
52
+ type: :runtime
53
+ prerelease: false
54
+ version_requirements: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ -
57
+ - ~>
58
+ - !ruby/object:Gem::Version
59
+ version: "0.6"
60
+ - !ruby/object:Gem::Dependency
61
+ name: dry-cli-help
62
+ requirement: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ -
65
+ - ~>
66
+ - !ruby/object:Gem::Version
67
+ version: "0.6"
68
+ type: :runtime
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ -
73
+ - ~>
74
+ - !ruby/object:Gem::Version
75
+ version: "0.6"
76
+ - !ruby/object:Gem::Dependency
77
+ name: dry-cli-ui
78
+ requirement: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ -
81
+ - ~>
82
+ - !ruby/object:Gem::Version
83
+ version: "0.7"
84
+ type: :runtime
85
+ prerelease: false
86
+ version_requirements: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ -
89
+ - ~>
90
+ - !ruby/object:Gem::Version
91
+ version: "0.7"
92
+ - !ruby/object:Gem::Dependency
93
+ name: zeitwerk
94
+ requirement: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ -
97
+ - ~>
98
+ - !ruby/object:Gem::Version
99
+ version: "2.8"
100
+ type: :runtime
101
+ prerelease: false
102
+ version_requirements: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ -
105
+ - ~>
106
+ - !ruby/object:Gem::Version
107
+ version: "2.8"
108
+ - !ruby/object:Gem::Dependency
109
+ name: pastel
110
+ requirement: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ -
113
+ - ~>
114
+ - !ruby/object:Gem::Version
115
+ version: "0.8"
116
+ type: :runtime
117
+ prerelease: false
118
+ version_requirements: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ -
121
+ - ~>
122
+ - !ruby/object:Gem::Version
123
+ version: "0.8"
124
+ - !ruby/object:Gem::Dependency
125
+ name: tty-box
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ -
129
+ - ~>
130
+ - !ruby/object:Gem::Version
131
+ version: "0.7"
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ -
137
+ - ~>
138
+ - !ruby/object:Gem::Version
139
+ version: "0.7"
140
+ - !ruby/object:Gem::Dependency
141
+ name: tty-table
142
+ requirement: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ -
145
+ - ~>
146
+ - !ruby/object:Gem::Version
147
+ version: "0.12"
148
+ type: :runtime
149
+ prerelease: false
150
+ version_requirements: !ruby/object:Gem::Requirement
151
+ requirements:
152
+ -
153
+ - ~>
154
+ - !ruby/object:Gem::Version
155
+ version: "0.12"
156
+ - !ruby/object:Gem::Dependency
157
+ name: logger
158
+ requirement: !ruby/object:Gem::Requirement
159
+ requirements:
160
+ -
161
+ - ~>
162
+ - !ruby/object:Gem::Version
163
+ version: "1.6"
164
+ type: :runtime
165
+ prerelease: false
166
+ version_requirements: !ruby/object:Gem::Requirement
167
+ requirements:
168
+ -
169
+ - ~>
170
+ - !ruby/object:Gem::Version
171
+ version: "1.6"
172
+ description: "Runs a suite of cases against one or more models through an adapter the consumer writes, scores each reply with a scorer the consumer writes, and prints leaderboards, per-case breakdowns and Braintrust experiments. The gem knows nothing about what is being evaluated: it owns discovery, the model lineup, providers, parallel execution, reporting and the command line."
173
+ email:
174
+ - kigster@gmail.com
175
+ executables:
176
+ - agent-evals
177
+ extensions: []
178
+ extra_rdoc_files: []
179
+ files:
180
+ - CHANGELOG.md
181
+ - LICENSE.txt
182
+ - README.md
183
+ - exe/agent-evals
184
+ - lib/agent/evals.rb
185
+ - lib/agent/evals/adapter.rb
186
+ - lib/agent/evals/case.rb
187
+ - lib/agent/evals/cli.rb
188
+ - lib/agent/evals/cli/cases.rb
189
+ - lib/agent/evals/cli/command.rb
190
+ - lib/agent/evals/cli/compare.rb
191
+ - lib/agent/evals/cli/model_command.rb
192
+ - lib/agent/evals/cli/progress_bars.rb
193
+ - lib/agent/evals/cli/run.rb
194
+ - lib/agent/evals/cli/verify.rb
195
+ - lib/agent/evals/cli/version.rb
196
+ - lib/agent/evals/completion.rb
197
+ - lib/agent/evals/config.rb
198
+ - lib/agent/evals/configuration.rb
199
+ - lib/agent/evals/contender.rb
200
+ - lib/agent/evals/discovery.rb
201
+ - lib/agent/evals/experiment.rb
202
+ - lib/agent/evals/helper.rb
203
+ - lib/agent/evals/launcher.rb
204
+ - lib/agent/evals/metric.rb
205
+ - lib/agent/evals/model.rb
206
+ - lib/agent/evals/providers/base.rb
207
+ - lib/agent/evals/providers/catalog.rb
208
+ - lib/agent/evals/providers/open_router.rb
209
+ - lib/agent/evals/providers/registry.rb
210
+ - lib/agent/evals/providers/type_safe.rb
211
+ - lib/agent/evals/reply.rb
212
+ - lib/agent/evals/run_result.rb
213
+ - lib/agent/evals/runtime.rb
214
+ - lib/agent/evals/schema_validator.rb
215
+ - lib/agent/evals/score.rb
216
+ - lib/agent/evals/scorer.rb
217
+ - lib/agent/evals/testing/echo_adapter.rb
218
+ - lib/agent/evals/testing/fake_model.rb
219
+ - lib/agent/evals/testing/fake_provider.rb
220
+ - lib/agent/evals/tracker.rb
221
+ - lib/agent/evals/trackers/braintrust.rb
222
+ - lib/agent/evals/trackers/local.rb
223
+ - lib/agent/evals/trackers/logfire.rb
224
+ - lib/agent/evals/trackers/registry.rb
225
+ - lib/agent/evals/usage.rb
226
+ - lib/agent/evals/verify.rb
227
+ - lib/agent/evals/version.rb
228
+ - lib/agent/evals/warnings.rb
229
+ homepage: "https://github.com/kigster/agent-evals"
230
+ licenses:
231
+ - MIT
232
+ metadata:
233
+ allowed_push_host: "https://rubygems.org"
234
+ homepage_uri: "https://github.com/kigster/agent-evals"
235
+ source_code_uri: "https://github.com/kigster/agent-evals"
236
+ changelog_uri: "https://github.com/kigster/agent-evals/blob/main/CHANGELOG.md"
237
+ rubygems_mfa_required: "true"
238
+ rdoc_options: []
239
+ require_paths:
240
+ - lib
241
+ required_ruby_version: !ruby/object:Gem::Requirement
242
+ requirements:
243
+ -
244
+ - ">="
245
+ - !ruby/object:Gem::Version
246
+ version: 3.4.0
247
+ required_rubygems_version: !ruby/object:Gem::Requirement
248
+ requirements:
249
+ -
250
+ - ">="
251
+ - !ruby/object:Gem::Version
252
+ version: "0"
253
+ requirements: []
254
+ rubygems_version: 4.1.0.beta2
255
+ specification_version: 4
256
+ summary: "A generic evaluation runner for LLM-backed code: adapters run, scorers score, the CLI reports"
257
+ test_files: []