lemans 1.4.0 → 1.5.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -0
- data/README.md +17 -2
- data/exe/lemans-remote +141 -3
- data/exe/lemans-viewer +998 -0
- data/lib/lemans/agent.rb +4 -1
- data/lib/lemans/agents/miniswen.rb +10 -5
- data/lib/lemans/agents/miniswen_installed.rb +16 -5
- data/lib/lemans/agents/nop.rb +1 -1
- data/lib/lemans/agents/oracle.rb +1 -1
- data/lib/lemans/agents.rb +4 -2
- data/lib/lemans/cli/regrade.rb +29 -77
- data/lib/lemans/cli/report/aggregate.rb +54 -30
- data/lib/lemans/cli/report.rb +168 -28
- data/lib/lemans/cli.rb +110 -34
- data/lib/lemans/environments/docker/proxy/Dockerfile +4 -0
- data/lib/lemans/environments/docker/proxy/README.md +49 -0
- data/lib/lemans/environments/docker/proxy/server.rb +79 -0
- data/lib/lemans/environments/docker.rb +79 -16
- data/lib/lemans/result.rb +65 -6
- data/lib/lemans/runner/task.rb +9 -5
- data/lib/lemans/runner.rb +45 -2
- data/lib/lemans/store.rb +6 -0
- data/lib/lemans/stores/fs.rb +16 -4
- data/lib/lemans/trial/patch.rb +21 -0
- data/lib/lemans/trial/verifier/test_scanner.rb +136 -0
- data/lib/lemans/trial/verifier.rb +19 -10
- data/lib/lemans/trial.rb +108 -40
- data/lib/lemans/version.rb +1 -1
- data/lib/lemans.rb +2 -0
- data/lib/miniswen/agent.rb +52 -16
- data/lib/miniswen/cli.rb +8 -3
- data/lib/miniswen/environment/docker.rb +1 -1
- data/lib/miniswen/jail.rb +1 -1
- data/lib/miniswen/local.rb +1 -1
- data/lib/miniswen/version.rb +1 -1
- metadata +49 -1
data/lib/lemans/agent.rb
CHANGED
|
@@ -15,6 +15,9 @@ module Lemans
|
|
|
15
15
|
|
|
16
16
|
attr_reader :profile, :model
|
|
17
17
|
|
|
18
|
+
# Whether #run can go on from the history (raw result) of an interrupted run
|
|
19
|
+
def self.recoverable? = false
|
|
20
|
+
|
|
18
21
|
def initialize(profile:, model: nil)
|
|
19
22
|
@profile = profile
|
|
20
23
|
|
|
@@ -28,7 +31,7 @@ module Lemans
|
|
|
28
31
|
def install(_task, _environment) = nil
|
|
29
32
|
|
|
30
33
|
# Run the task.
|
|
31
|
-
def run(task, environment)
|
|
34
|
+
def run(task, environment, history: nil)
|
|
32
35
|
raise NotImplementedError
|
|
33
36
|
end
|
|
34
37
|
|
|
@@ -21,9 +21,13 @@ module Lemans
|
|
|
21
21
|
cost_limit: :cost_ceiling_reached
|
|
22
22
|
}.freeze
|
|
23
23
|
|
|
24
|
-
def
|
|
25
|
-
|
|
24
|
+
def self.recoverable? = true
|
|
25
|
+
|
|
26
|
+
def run(task, environment, history: nil)
|
|
27
|
+
history &&= ::Miniswen::Agent::Result.from_h(JSON.parse(history))
|
|
28
|
+
run_result = obtain_result(task, environment, history)
|
|
26
29
|
trajectory = trajectory_for(run_result)
|
|
30
|
+
raw_result = raw_result_for(run_result)
|
|
27
31
|
|
|
28
32
|
# A failed model call is still an answer: the trial saves the
|
|
29
33
|
# trajectory as evidence before failing.
|
|
@@ -39,16 +43,17 @@ module Lemans
|
|
|
39
43
|
|
|
40
44
|
private
|
|
41
45
|
|
|
42
|
-
def obtain_result(task, environment)
|
|
46
|
+
def obtain_result(task, environment, history)
|
|
43
47
|
agent = agent_for(environment)
|
|
44
48
|
begin
|
|
45
|
-
agent.run(task.instruction)
|
|
49
|
+
agent.run(task.instruction, history:)
|
|
46
50
|
rescue InfrastructureError, ::Miniswen::InfrastructureError => e
|
|
47
51
|
agent.partial_result(e.message)
|
|
48
52
|
end
|
|
49
53
|
end
|
|
50
54
|
|
|
51
|
-
|
|
55
|
+
# The run as miniswen records it, signatures included: what a recovery continues from
|
|
56
|
+
def raw_result_for(run_result) = JSON.generate(run_result.to_h)
|
|
52
57
|
|
|
53
58
|
def agent_for(environment)
|
|
54
59
|
raise ConfigError, "miniswen needs a model to drive" if model.to_s.empty?
|
|
@@ -13,6 +13,7 @@ module Lemans
|
|
|
13
13
|
class MiniswenInstalled < Miniswen
|
|
14
14
|
NAME = "miniswen-installed"
|
|
15
15
|
RESULTS_PATH = "/tmp/lemans-miniswen.result.json"
|
|
16
|
+
HISTORY_PATH = "/tmp/lemans-miniswen.history.json"
|
|
16
17
|
INSTALL_TIMEOUT_SEC = 300
|
|
17
18
|
# The CLI enforces max-time itself, but between steps only: a command
|
|
18
19
|
# started just before the deadline runs to its own exec timeout first,
|
|
@@ -31,8 +32,9 @@ module Lemans
|
|
|
31
32
|
|
|
32
33
|
# An in-sandbox run self-reports: everything but the verifier's reward
|
|
33
34
|
# comes from a file the sandbox wrote.
|
|
34
|
-
def obtain_result(task, environment)
|
|
35
|
-
|
|
35
|
+
def obtain_result(task, environment, history)
|
|
36
|
+
upload_history(environment, history) if history
|
|
37
|
+
run = environment.exec(command_for(task, history:), timeout: outer_timeout, env: provider_env(environment))
|
|
36
38
|
|
|
37
39
|
begin
|
|
38
40
|
Tempfile.create(%w[miniswen .result.json]) do |file|
|
|
@@ -47,7 +49,7 @@ module Lemans
|
|
|
47
49
|
end
|
|
48
50
|
end
|
|
49
51
|
|
|
50
|
-
|
|
52
|
+
def raw_result_for(_run_result) = @raw_result
|
|
51
53
|
|
|
52
54
|
def outer_timeout = profile.timeout + profile.exec_timeout + EXEC_SLACK_SEC
|
|
53
55
|
|
|
@@ -64,9 +66,18 @@ module Lemans
|
|
|
64
66
|
policy.mode == "allowlist" ? policy.domains : []
|
|
65
67
|
end
|
|
66
68
|
|
|
67
|
-
def
|
|
69
|
+
def upload_history(environment, history)
|
|
70
|
+
Tempfile.create(%w[miniswen .history.json]) do |file|
|
|
71
|
+
file.write(JSON.generate(history.to_h))
|
|
72
|
+
file.flush
|
|
73
|
+
environment.upload(file.path, HISTORY_PATH)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def command_for(task, history: nil)
|
|
68
78
|
argv = [ "miniswen", "-q", "--no-refresh-registry", "--jail",
|
|
69
|
-
"-m", model.to_s,
|
|
79
|
+
"-m", model.to_s,
|
|
80
|
+
*(history ? [ "--continue-from", HISTORY_PATH ] : [ "-p", task.instruction ]),
|
|
70
81
|
"--results-path", RESULTS_PATH,
|
|
71
82
|
"--max-steps", profile.step_limit, "--max-time", profile.timeout.to_i,
|
|
72
83
|
"--exec-timeout", profile.exec_timeout.to_i,
|
data/lib/lemans/agents/nop.rb
CHANGED
data/lib/lemans/agents/oracle.rb
CHANGED
|
@@ -13,7 +13,7 @@ module Lemans
|
|
|
13
13
|
ENTRYPOINT = "solve.sh"
|
|
14
14
|
PATCH = "solution.patch"
|
|
15
15
|
|
|
16
|
-
def run(task, environment)
|
|
16
|
+
def run(task, environment, history: nil)
|
|
17
17
|
files = task.solution_files
|
|
18
18
|
if files.empty?
|
|
19
19
|
raise ConfigError, "#{task.name}: no solution/ to run — the oracle has nothing to prove" if
|
data/lib/lemans/agents.rb
CHANGED
|
@@ -11,11 +11,13 @@ module Lemans
|
|
|
11
11
|
"miniswen-installed" => "MiniswenInstalled"
|
|
12
12
|
}.freeze
|
|
13
13
|
|
|
14
|
-
def self.build(name, profile:, model: nil)
|
|
14
|
+
def self.build(name, profile:, model: nil) = lookup(name).new(profile: profile, model: model)
|
|
15
|
+
|
|
16
|
+
def self.lookup(name)
|
|
15
17
|
constant = REGISTRY[name] or
|
|
16
18
|
raise ConfigError, "unknown agent #{name.inspect} (known: #{REGISTRY.keys.join(", ")})"
|
|
17
19
|
|
|
18
|
-
const_get(constant)
|
|
20
|
+
const_get(constant)
|
|
19
21
|
end
|
|
20
22
|
end
|
|
21
23
|
end
|
data/lib/lemans/cli/regrade.rb
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
-
require "prism"
|
|
5
4
|
|
|
6
5
|
module Lemans
|
|
7
6
|
class CLI < Thor
|
|
@@ -12,13 +11,19 @@ module Lemans
|
|
|
12
11
|
ALLOWED = "fail (allowed)"
|
|
13
12
|
CHECKS = "checks.json"
|
|
14
13
|
|
|
15
|
-
|
|
14
|
+
# A checks.json-shaped file: `checks`, `grading`, and the features as
|
|
15
|
+
# `features: { name => [checks] }`.
|
|
16
|
+
Mapping = Struct.new(:checks, :base_credit, :points, :features, :stray_features, keyword_init: true) do
|
|
16
17
|
def self.from_json(data)
|
|
17
18
|
checks = data["checks"] or raise ConfigError, "a mapping needs a `checks` section"
|
|
18
19
|
grading = data["grading"] || {}
|
|
19
20
|
declared = grading["points"] || {}
|
|
20
21
|
allowed = checks.select { |_, status| status == ALLOWED }.keys
|
|
21
|
-
|
|
22
|
+
features = data["features"] || {}
|
|
23
|
+
unknown = features.values.flatten - checks.keys
|
|
24
|
+
raise ConfigError, "the mapping's features name unknown checks: #{unknown.inspect}" if unknown.any?
|
|
25
|
+
|
|
26
|
+
new(checks:, base_credit: grading["base_credit"], points: allowed.to_h { [ it, declared.fetch(it, 1) ] }, features:)
|
|
22
27
|
end
|
|
23
28
|
|
|
24
29
|
def names = checks.keys.sort
|
|
@@ -28,82 +33,18 @@ module Lemans
|
|
|
28
33
|
def grading = { base_credit:, points: }.compact
|
|
29
34
|
end
|
|
30
35
|
|
|
31
|
-
Change = Struct.new(:result, :reward, :credit, keyword_init: true)
|
|
32
|
-
|
|
33
|
-
# Reads the grading schema off the test file without running it: every
|
|
34
|
-
# `def test_*` and ActiveSupport `test "..."` is a check, an
|
|
35
|
-
# `allow_failure` call inside makes it an extra worth its `points:`.
|
|
36
|
-
class TestScanner < Prism::Visitor
|
|
37
|
-
attr_reader :tests, :points, :base_credit
|
|
38
|
-
|
|
39
|
-
def initialize
|
|
40
|
-
super
|
|
41
|
-
@scope = []
|
|
42
|
-
@tests = []
|
|
43
|
-
@points = {}
|
|
44
|
-
@current = nil
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def visit_module_node(node) = scoped(node) { super }
|
|
48
|
-
|
|
49
|
-
def visit_class_node(node) = scoped(node) { super }
|
|
50
|
-
|
|
51
|
-
def visit_def_node(node)
|
|
52
|
-
return super unless node.name.start_with?("test_")
|
|
53
|
-
|
|
54
|
-
within("#{@scope.join("::")}##{node.name}") { super }
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def visit_call_node(node)
|
|
58
|
-
case node.name
|
|
59
|
-
when :test
|
|
60
|
-
title = node.arguments&.arguments&.first
|
|
61
|
-
if node.receiver.nil? && node.block && title.is_a?(Prism::StringNode)
|
|
62
|
-
return within("#{@scope.join("::")}#test_#{title.unescaped.gsub(/\s+/, "_")}") { super }
|
|
63
|
-
end
|
|
64
|
-
when :allow_failure
|
|
65
|
-
@points[@current] ||= points_of(node) if @current
|
|
66
|
-
when :base_credit=
|
|
67
|
-
@base_credit = node.arguments.arguments.first.value if node.receiver.is_a?(Prism::ConstantReadNode) && node.receiver.name == :LemansReport
|
|
68
|
-
end
|
|
69
|
-
super
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
private
|
|
73
|
-
|
|
74
|
-
def scoped(node)
|
|
75
|
-
@scope.push(node.constant_path.full_name)
|
|
76
|
-
yield
|
|
77
|
-
ensure
|
|
78
|
-
@scope.pop
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def within(check)
|
|
82
|
-
@tests << check
|
|
83
|
-
@current = check
|
|
84
|
-
yield
|
|
85
|
-
ensure
|
|
86
|
-
@current = nil
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def points_of(node)
|
|
90
|
-
keywords = node.arguments&.arguments&.grep(Prism::KeywordHashNode)&.first
|
|
91
|
-
pair = keywords&.elements&.find { it.is_a?(Prism::AssocNode) && it.key.is_a?(Prism::SymbolNode) && it.key.unescaped == "points" }
|
|
92
|
-
pair ? pair.value.value : 1
|
|
93
|
-
end
|
|
94
|
-
end
|
|
36
|
+
Change = Struct.new(:result, :reward, :credit, :features, keyword_init: true)
|
|
95
37
|
|
|
96
38
|
class << self
|
|
97
39
|
def mapping_from_file(path) = Mapping.from_json(JSON.parse(File.read(path)))
|
|
98
40
|
|
|
99
41
|
def mapping_for(task)
|
|
100
|
-
|
|
101
|
-
raise ConfigError, "#{task.name} has no verification_test.rb to read the grading from" unless
|
|
42
|
+
scanner = Trial::Verifier::TestScanner.for(task)
|
|
43
|
+
raise ConfigError, "#{task.name} has no verification_test.rb to read the grading from" unless scanner
|
|
102
44
|
|
|
103
|
-
scanner = TestScanner.new
|
|
104
|
-
Prism.parse_file(local.to_s).value.accept(scanner)
|
|
105
45
|
checks = scanner.tests.to_h { [ it, scanner.points.key?(it) ? ALLOWED : "fail" ] }
|
|
106
|
-
Mapping.new(checks:, base_credit: scanner.base_credit, points: scanner.points
|
|
46
|
+
Mapping.new(checks:, base_credit: scanner.base_credit, points: scanner.points,
|
|
47
|
+
features: scanner.features, stray_features: scanner.stray_features)
|
|
107
48
|
end
|
|
108
49
|
end
|
|
109
50
|
|
|
@@ -115,8 +56,14 @@ module Lemans
|
|
|
115
56
|
@mapping = mapping
|
|
116
57
|
end
|
|
117
58
|
|
|
118
|
-
def results
|
|
119
|
-
|
|
59
|
+
def results = runs.select(&:scored?)
|
|
60
|
+
|
|
61
|
+
# Older multistep results lack the task's step count; returns the runs that gained it.
|
|
62
|
+
def record_total_steps!(total)
|
|
63
|
+
runs.select { it.steps && it.total_steps != total }.each do |result|
|
|
64
|
+
result.total_steps = total
|
|
65
|
+
store.save(result)
|
|
66
|
+
end
|
|
120
67
|
end
|
|
121
68
|
|
|
122
69
|
# A statically read mapping is only trusted once a stored checks.json
|
|
@@ -149,6 +96,8 @@ module Lemans
|
|
|
149
96
|
|
|
150
97
|
private
|
|
151
98
|
|
|
99
|
+
def runs = @runs ||= store.query(task:).sort_by { it.id.to_s }
|
|
100
|
+
|
|
152
101
|
def checks_of(result)
|
|
153
102
|
raw = store.read_artifact(result, CHECKS)
|
|
154
103
|
raw && JSON.parse(raw)
|
|
@@ -164,11 +113,14 @@ module Lemans
|
|
|
164
113
|
updated[:grading] = mapping.grading unless mapping.grading.empty?
|
|
165
114
|
reward = failures.empty? ? 1.0 : 0.0
|
|
166
115
|
credit = credit_of(statuses, reward)
|
|
167
|
-
|
|
116
|
+
features = Trial::Verifier.features_from(statuses, mapping.features)
|
|
117
|
+
return if reward == result.reward && credit == result.credit && features == result.features &&
|
|
118
|
+
JSON.parse(JSON.generate(updated)) == checks
|
|
168
119
|
|
|
169
120
|
store.save_artifact(result, "#{JSON.pretty_generate(updated)}\n", path: CHECKS, force: true)
|
|
170
|
-
change = Change.new(result:, reward: [ result.reward, reward ], credit: [ result.credit, credit ]
|
|
171
|
-
|
|
121
|
+
change = Change.new(result:, reward: [ result.reward, reward ], credit: [ result.credit, credit ],
|
|
122
|
+
features: [ result.features, features ])
|
|
123
|
+
store.save(result.graded!(reward, credit:, features:))
|
|
172
124
|
change
|
|
173
125
|
end
|
|
174
126
|
|
|
@@ -10,7 +10,7 @@ module Lemans
|
|
|
10
10
|
# task, agent, model — "task-model" reads as two columns.
|
|
11
11
|
class Aggregate
|
|
12
12
|
KEYS = %i[task agent model].freeze
|
|
13
|
-
METRICS = %i[score credit time cost steps tokens].freeze
|
|
13
|
+
METRICS = %i[score credit features time cost steps tokens].freeze
|
|
14
14
|
METRIC_SOURCES = { credit: :credit, time: :duration, cost: :cost_usd, steps: :steps, tokens: :tokens }.freeze
|
|
15
15
|
|
|
16
16
|
attr_reader :report, :keys
|
|
@@ -32,32 +32,36 @@ module Lemans
|
|
|
32
32
|
.sort_by { |group| keys.map { group[it].to_s } }
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
-
def order_by!(
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
elsif column == :score
|
|
41
|
-
Report.sort_rows(@groups, descending: true) { [ Rational(it[:solved], it[:attempts]), it[:attempts] ] }
|
|
42
|
-
else
|
|
43
|
-
Report.sort_rows(@groups, descending: true) { it[METRIC_SOURCES.fetch(column)] }
|
|
44
|
-
end
|
|
35
|
+
def order_by!(spec)
|
|
36
|
+
sort_keys = Report.sort_columns(spec, allowed: keys + METRICS + report.feature_columns).map do |column, reversed|
|
|
37
|
+
[ ->(group) { sort_value(group, column) }, !keys.include?(column) != reversed ]
|
|
38
|
+
end
|
|
39
|
+
@groups = Report.sort_rows(@groups, sort_keys)
|
|
45
40
|
self
|
|
46
41
|
end
|
|
47
42
|
|
|
48
43
|
def to_rows
|
|
49
|
-
metrics = report.fractional?
|
|
50
|
-
|
|
44
|
+
metrics = METRICS - [ (:credit unless report.fractional?), (:features unless report.features?) ].compact
|
|
45
|
+
columns = keys + metrics + report.shown_feature_columns
|
|
46
|
+
columns -= Report.hidden_columns(report.hide_columns, allowed: columns)
|
|
47
|
+
[ columns.map(&:to_s) ] +
|
|
51
48
|
@groups.map do |group|
|
|
52
|
-
|
|
49
|
+
columns.map do |column|
|
|
50
|
+
if keys.include?(column) then display_key(column, group[column])
|
|
51
|
+
elsif metrics.include?(column) then cell(column, group)
|
|
52
|
+
else pass_rate(group[column])
|
|
53
|
+
end
|
|
54
|
+
end
|
|
53
55
|
end
|
|
54
56
|
end
|
|
55
57
|
|
|
56
58
|
def to_csv
|
|
57
|
-
columns = keys + %i[solved attempts credit duration cost_usd steps tokens]
|
|
59
|
+
columns = keys + %i[solved attempts credit features_passed features_total duration cost_usd steps tokens]
|
|
58
60
|
CSV.generate do |csv|
|
|
59
|
-
csv << columns
|
|
60
|
-
@groups.each
|
|
61
|
+
csv << columns + report.shown_feature_columns
|
|
62
|
+
@groups.each do |group|
|
|
63
|
+
csv << columns.map { group[it] } + report.shown_feature_columns.map { group[it] && pass_rate(group[it]) }
|
|
64
|
+
end
|
|
61
65
|
end
|
|
62
66
|
end
|
|
63
67
|
|
|
@@ -67,6 +71,16 @@ module Lemans
|
|
|
67
71
|
|
|
68
72
|
private
|
|
69
73
|
|
|
74
|
+
def sort_value(group, column)
|
|
75
|
+
if column == :model then Report.short_model(group[:model])
|
|
76
|
+
elsif keys.include?(column) then group[column].to_s
|
|
77
|
+
elsif report.feature_columns.include?(column) then group[column] && Rational(*group[column])
|
|
78
|
+
elsif column == :features then group[:features_total] && Rational(group[:features_passed], group[:features_total])
|
|
79
|
+
elsif column == :score then [ Rational(group[:solved], group[:attempts]), group[:attempts] ]
|
|
80
|
+
else group[METRIC_SOURCES.fetch(column)]
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
70
84
|
# Attempts count every run; means and the median skip runs that never
|
|
71
85
|
# measured the value, so one invalid trial cannot zero out a cell.
|
|
72
86
|
def build(values, group)
|
|
@@ -77,18 +91,37 @@ module Lemans
|
|
|
77
91
|
duration: median(group.filter_map { it[:duration] }),
|
|
78
92
|
cost_usd: mean(group.filter_map { it[:cost_usd] }),
|
|
79
93
|
steps: mean(group.filter_map { it[:steps] }),
|
|
80
|
-
tokens: mean(group.filter_map { it[:tokens] })
|
|
94
|
+
tokens: mean(group.filter_map { it[:tokens] }),
|
|
95
|
+
**features_sum(group),
|
|
96
|
+
**report.feature_columns.to_h { [ it, feature_tally(group, it) ] }
|
|
81
97
|
)
|
|
82
98
|
end
|
|
83
99
|
|
|
100
|
+
# Passed out of the runs that graded the feature; nil when none did
|
|
101
|
+
def feature_tally(group, column)
|
|
102
|
+
graded = group.map { Report.feature_of(it, column) }.reject(&:nil?)
|
|
103
|
+
[ graded.count(true), graded.size ] unless graded.empty?
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def pass_rate(tally) = tally ? tally.join("/") : "-"
|
|
107
|
+
|
|
108
|
+
# Features passed out of graded, summed over the group's runs
|
|
109
|
+
def features_sum(group)
|
|
110
|
+
tallies = group.filter_map { Report.features_tally(it) }
|
|
111
|
+
return { features_passed: nil, features_total: nil } if tallies.empty?
|
|
112
|
+
|
|
113
|
+
{ features_passed: tallies.sum(&:first), features_total: tallies.sum(&:last) }
|
|
114
|
+
end
|
|
115
|
+
|
|
84
116
|
def cell(metric, group)
|
|
85
117
|
case metric
|
|
86
118
|
when :score then "#{group[:solved]}/#{group[:attempts]}"
|
|
87
119
|
when :credit then mean_display(group[:credit], 2)
|
|
88
|
-
when :
|
|
89
|
-
when :
|
|
120
|
+
when :features then group[:features_total] ? "#{group[:features_passed]}/#{group[:features_total]}" : "-"
|
|
121
|
+
when :time then Report.duration_display(group[:duration])
|
|
122
|
+
when :cost then Report.cost_display(group[:cost_usd])
|
|
90
123
|
when :steps then mean_display(group[:steps], 1)
|
|
91
|
-
when :tokens then
|
|
124
|
+
when :tokens then Report.tokens_display(group[:tokens])
|
|
92
125
|
end
|
|
93
126
|
end
|
|
94
127
|
|
|
@@ -108,15 +141,6 @@ module Lemans
|
|
|
108
141
|
key == :model ? Report.short_model(value) : value.to_s
|
|
109
142
|
end
|
|
110
143
|
|
|
111
|
-
def time(sec)
|
|
112
|
-
return "-" if sec.nil?
|
|
113
|
-
|
|
114
|
-
minutes, seconds = sec.round.divmod(60)
|
|
115
|
-
minutes.positive? ? "#{minutes}m #{seconds}s" : "#{seconds}s"
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
def cost(value) = value.nil? ? "-" : "$#{format("%g", value.round(4))}"
|
|
119
|
-
|
|
120
144
|
def mean_display(value, digits) = value.nil? ? "-" : format("%g", value.round(digits))
|
|
121
145
|
end
|
|
122
146
|
end
|