actionagent 1.9.7 → 1.9.8
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/app/assets/builds/action_agent.css +1 -1
- data/app/assets/builds/action_agent.js +75 -63
- data/app/models/action_agent/agent_run.rb +25 -6
- data/app/models/action_agent/evaluation_run.rb +43 -0
- data/app/models/action_agent/scenario_product.rb +24 -0
- data/app/models/action_agent/scenario_set.rb +9 -0
- data/app/serializers/action_agent/evaluation_serializer.rb +6 -2
- data/app/services/action_agent/evaluation_report_import.rb +49 -2
- data/app/services/action_agent/scenario_catalog_import.rb +5 -0
- data/app/services/action_agent/scenario_evaluation_runner.rb +66 -5
- data/lib/action_agent/version.rb +1 -1
- data/lib/generators/action_agent/templates/migrations/034_add_catalog_product_criteria.rb.erb +24 -0
- data/lib/generators/action_agent/templates/migrations/035_add_catalog_pass_bars.rb.erb +28 -0
- metadata +4 -2
|
@@ -313,18 +313,37 @@ module ActionAgent
|
|
|
313
313
|
LiveUpdates.broadcast("agent_runs_#{agent_id}", type: "update", id: id, status: status)
|
|
314
314
|
end
|
|
315
315
|
|
|
316
|
+
# Statuses a cancel moves to `cancelled`. A finished run keeps its end.
|
|
317
|
+
CANCELLABLE_STATUSES = %i[pending running awaiting_input].freeze
|
|
318
|
+
|
|
316
319
|
# Cancels a run that is in progress or waiting for input, and every
|
|
317
|
-
# request for input it still has pending.
|
|
320
|
+
# request for input it still has pending. Returns whether this call
|
|
321
|
+
# cancelled it.
|
|
322
|
+
#
|
|
323
|
+
# The status is checked by the write itself, as AgentExecutionJob claims
|
|
324
|
+
# a pending run, because this copy of the run may be older than the
|
|
325
|
+
# database's: the request's, or a caller's that has held it since it
|
|
326
|
+
# executed. A run that finished in the meantime is left as it finished,
|
|
327
|
+
# and this copy is reloaded to show how. The update skips callbacks, so
|
|
328
|
+
# the cancel is broadcast here, once.
|
|
318
329
|
#
|
|
319
330
|
# @param reason [String] recorded as the run's error message
|
|
331
|
+
# @return [Boolean]
|
|
320
332
|
def cancel!(reason = "Cancelled by user")
|
|
321
|
-
return unless in_progress? || awaiting_input?
|
|
333
|
+
return false unless in_progress? || awaiting_input?
|
|
322
334
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
335
|
+
now = Time.current
|
|
336
|
+
cancelled = transaction do
|
|
337
|
+
changed = self.class.where(id: id, status: self.class.statuses.values_at(*CANCELLABLE_STATUSES))
|
|
338
|
+
.update_all(status: self.class.statuses[:cancelled], completed_at: now, error_message: reason, updated_at: now)
|
|
339
|
+
next false if changed.zero?
|
|
340
|
+
|
|
341
|
+
input_requests.pending.update_all(status: InputRequest.statuses[:cancelled], updated_at: now)
|
|
342
|
+
true
|
|
326
343
|
end
|
|
327
|
-
|
|
344
|
+
reload
|
|
345
|
+
broadcast_update if cancelled
|
|
346
|
+
cancelled
|
|
328
347
|
end
|
|
329
348
|
|
|
330
349
|
# Expires the run's overdue requests for input, which fails the run (see
|
|
@@ -87,6 +87,48 @@ module ActionAgent
|
|
|
87
87
|
comparison if comparison.is_a?(Hash) && comparison.present?
|
|
88
88
|
end
|
|
89
89
|
|
|
90
|
+
# The bars this run was gated by (ActiveAgent::Evals::PassThresholds),
|
|
91
|
+
# as the run recorded them when it started or was imported, so a later
|
|
92
|
+
# change to the catalog does not regrade it; nil for a run whose suite
|
|
93
|
+
# declared none, one recorded before runs kept them, and a framework
|
|
94
|
+
# without per-group bars.
|
|
95
|
+
def pass_thresholds
|
|
96
|
+
self.class.pass_thresholds_from(scores&.dig("_pass_thresholds"))
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# A recorded `pass_thresholds` mapping (Report#to_h's: "threshold",
|
|
100
|
+
# "min_pass_rate", "groups") as PassThresholds, or nil for anything
|
|
101
|
+
# that is not one.
|
|
102
|
+
def self.pass_thresholds_from(value)
|
|
103
|
+
return nil unless value.is_a?(Hash) && defined?(ActiveAgent::Evals::PassThresholds)
|
|
104
|
+
|
|
105
|
+
value = value.stringify_keys
|
|
106
|
+
groups = value["groups"].nil? ? {} : value["groups"]
|
|
107
|
+
return nil unless groups.is_a?(Hash)
|
|
108
|
+
|
|
109
|
+
ActiveAgent::Evals::PassThresholds.new(suite: value.slice(*ActiveAgent::Evals::PassThresholds::SUITE_KEYS), groups: groups)
|
|
110
|
+
rescue ArgumentError
|
|
111
|
+
nil
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# The gates the run's results met or missed (Report#gates, each
|
|
115
|
+
# `{ "group", "name", "label", "passed", "total", "pass_rate",
|
|
116
|
+
# "min_pass_rate", "gated", "met" }`), as recorded when it completed;
|
|
117
|
+
# nil for a run without bars.
|
|
118
|
+
def gates
|
|
119
|
+
value = scores&.dig("_gates")
|
|
120
|
+
value.select { |gate| gate.is_a?(Hash) } if value.is_a?(Array)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Whether every gate was met, as Report#passed? answers: false when no
|
|
124
|
+
# gate was reached; nil for a run without bars.
|
|
125
|
+
def gates_met
|
|
126
|
+
rows = gates
|
|
127
|
+
return nil if rows.nil?
|
|
128
|
+
|
|
129
|
+
rows.any? && rows.all? { |gate| gate["met"] == true }
|
|
130
|
+
end
|
|
131
|
+
|
|
90
132
|
# How the report names the judge, the way the suite panel does: the
|
|
91
133
|
# judge the recorded verdict names — unless that is only the pass-rate
|
|
92
134
|
# ranking — else the evaluation's judge model. nil when neither is set,
|
|
@@ -307,6 +349,7 @@ module ActionAgent
|
|
|
307
349
|
kwargs[:judge_usage] = judge_usage_run if judge_usage_run && self.class.report_accepts?(:judge_usage)
|
|
308
350
|
kwargs[:release] = release_summary if release_summary && self.class.report_accepts?(:release)
|
|
309
351
|
kwargs[:comparison] = recorded_comparison if recorded_comparison && self.class.report_accepts?(:comparison)
|
|
352
|
+
kwargs[:pass_thresholds] = pass_thresholds if pass_thresholds && self.class.report_accepts?(:pass_thresholds)
|
|
310
353
|
self.class.report_class.new(**kwargs)
|
|
311
354
|
end
|
|
312
355
|
|
|
@@ -21,6 +21,28 @@ module ActionAgent
|
|
|
21
21
|
value.is_a?(Hash) ? value : {}
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
+
# The product's named criteria lists, as the catalog's `criteria:` gives
|
|
25
|
+
# them: list name => criterion hashes. A stored value that is not such a
|
|
26
|
+
# mapping, and a list left with no criterion, read as no list.
|
|
27
|
+
# @return [Hash{String => Array<Hash>}]
|
|
28
|
+
def criteria
|
|
29
|
+
value = self[:criteria]
|
|
30
|
+
return {} unless value.is_a?(Hash)
|
|
31
|
+
|
|
32
|
+
value.each_with_object({}) do |(name, list), lists|
|
|
33
|
+
entries = Array(list).select { |criterion| criterion.is_a?(Hash) } if list.is_a?(Array)
|
|
34
|
+
lists[name.to_s] = entries if entries.present?
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# The bars the catalog's `pass:` declares (ActiveAgent::Evals::PassThresholds);
|
|
39
|
+
# a stored value that is not a mapping reads as none.
|
|
40
|
+
# @return [Hash{String => Object}]
|
|
41
|
+
def pass
|
|
42
|
+
value = self[:pass]
|
|
43
|
+
value.is_a?(Hash) ? value : {}
|
|
44
|
+
end
|
|
45
|
+
|
|
24
46
|
# The agent a set of this product runs against when the caller names
|
|
25
47
|
# none: the product's agent, else the project's agent under test, else
|
|
26
48
|
# the owner's agent named by the document.
|
|
@@ -36,6 +58,8 @@ module ActionAgent
|
|
|
36
58
|
"name" => name,
|
|
37
59
|
"description" => description,
|
|
38
60
|
"agent" => agent_name.presence || agent&.name,
|
|
61
|
+
"criteria" => criteria.presence,
|
|
62
|
+
"pass" => pass.presence,
|
|
39
63
|
"sets" => sets.map(&:to_document)
|
|
40
64
|
}.merge(metadata).compact
|
|
41
65
|
end
|
|
@@ -37,6 +37,14 @@ module ActionAgent
|
|
|
37
37
|
value.is_a?(Hash) ? value : {}
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
+
# The bars the catalog's `pass:` declares (ActiveAgent::Evals::PassThresholds);
|
|
41
|
+
# a stored value that is not a mapping reads as none.
|
|
42
|
+
# @return [Hash{String => Object}]
|
|
43
|
+
def pass
|
|
44
|
+
value = self[:pass]
|
|
45
|
+
value.is_a?(Hash) ? value : {}
|
|
46
|
+
end
|
|
47
|
+
|
|
40
48
|
# The evaluation's name: the catalog, product and set keys.
|
|
41
49
|
def evaluation_name
|
|
42
50
|
"#{catalog.key}/#{product.key}/#{key}".first(EvaluationReportImport::MAX_EVALUATION_NAME)
|
|
@@ -117,6 +125,7 @@ module ActionAgent
|
|
|
117
125
|
"description" => description,
|
|
118
126
|
"judge" => judge.presence,
|
|
119
127
|
"criteria" => criteria.presence,
|
|
128
|
+
"pass" => pass.presence,
|
|
120
129
|
"scenarios" => scenarios.map(&:to_document)
|
|
121
130
|
}.merge(metadata).compact
|
|
122
131
|
end
|
|
@@ -81,7 +81,9 @@ module ActionAgent
|
|
|
81
81
|
# checkout sandbox the run replayed against, if any: its session id and
|
|
82
82
|
# checkout, never its token. `agent_version` is the version of the
|
|
83
83
|
# agent the run scored, and `version_state` whether that is the agent's
|
|
84
|
-
# current version ("current", "earlier" or "unrecorded").
|
|
84
|
+
# current version ("current", "earlier" or "unrecorded"). `gates` are the
|
|
85
|
+
# per-group pass gates the run was held to (EvaluationRun#gates) and
|
|
86
|
+
# `gates_met` whether it met every one; both nil for a run without bars.
|
|
85
87
|
def run_summary(run, number: nil)
|
|
86
88
|
{
|
|
87
89
|
id: run.id,
|
|
@@ -95,7 +97,9 @@ module ActionAgent
|
|
|
95
97
|
created_at: run.created_at.iso8601,
|
|
96
98
|
sandbox: run.sandbox,
|
|
97
99
|
agent_version: run.agent_version_summary,
|
|
98
|
-
version_state: run.evaluation.standing_info.version_state(run)
|
|
100
|
+
version_state: run.evaluation.standing_info.version_state(run),
|
|
101
|
+
gates: run.gates,
|
|
102
|
+
gates_met: run.gates_met
|
|
99
103
|
}
|
|
100
104
|
end
|
|
101
105
|
|
|
@@ -95,6 +95,10 @@ module ActionAgent
|
|
|
95
95
|
JUDGE_USAGE_KEYS = %w[calls input_tokens output_tokens cost model by_kind source].freeze
|
|
96
96
|
JUDGE_USAGE_SOURCES = %w[reported traces meter].freeze
|
|
97
97
|
MAX_JUDGE_KINDS = 10
|
|
98
|
+
# Groups a report's `pass_thresholds` may name; a group holds at least
|
|
99
|
+
# one result, so no report needs more.
|
|
100
|
+
MAX_PASS_GROUPS = MAX_RESULTS
|
|
101
|
+
PASS_THRESHOLD_KEYS = %w[threshold min_pass_rate groups].freeze
|
|
98
102
|
SCOPE_PATTERN = %r{\A[\w .:/@-]{1,100}\z}
|
|
99
103
|
STATUSES = %w[passed failed errored].freeze
|
|
100
104
|
# The envelope statuses a delivery may name.
|
|
@@ -674,10 +678,21 @@ module ActionAgent
|
|
|
674
678
|
"_verdict" => report["verdict"],
|
|
675
679
|
"_selection" => selection,
|
|
676
680
|
"_metadata" => metadata,
|
|
677
|
-
"_judge_usage_run" => bounded_judge_usage(report.dig("judge_usage", "run"))
|
|
681
|
+
"_judge_usage_run" => bounded_judge_usage(report.dig("judge_usage", "run")),
|
|
682
|
+
"_pass_thresholds" => recorded_pass_thresholds
|
|
678
683
|
}.compact.merge("_judge_label" => judge_label)
|
|
679
684
|
end
|
|
680
685
|
|
|
686
|
+
# The bars the report was gated by, as PassThresholds normalizes them
|
|
687
|
+
# (validate_pass_thresholds! has refused anything else), so the rebuilt
|
|
688
|
+
# report gates the stored results the way the application's did. nil
|
|
689
|
+
# for a report whose suite declared none.
|
|
690
|
+
def recorded_pass_thresholds
|
|
691
|
+
return nil if report["pass_thresholds"].nil?
|
|
692
|
+
|
|
693
|
+
EvaluationRun.pass_thresholds_from(report["pass_thresholds"])&.to_h
|
|
694
|
+
end
|
|
695
|
+
|
|
681
696
|
# The run's scores in the shape the Evaluations view renders
|
|
682
697
|
# (ScenarioEvaluationRunner#scores_for), summarized from the stored
|
|
683
698
|
# results as the application reported them — no cost is estimated here,
|
|
@@ -689,10 +704,14 @@ module ActionAgent
|
|
|
689
704
|
def summarized_scores(run)
|
|
690
705
|
rebuilt = run.to_report(estimate: false)
|
|
691
706
|
comparison = rebuilt.comparison if rebuilt.respond_to?(:comparison)
|
|
707
|
+
# The gates are worked out from the stored results as well: the
|
|
708
|
+
# report's own "gates" are not taken as the verdict.
|
|
709
|
+
gates = rebuilt.gates.map(&:to_h) if rebuilt.respond_to?(:pass_thresholds) && rebuilt.pass_thresholds
|
|
692
710
|
rebuilt.criterion_scores.merge(
|
|
693
711
|
"_models" => rebuilt.summary_by_model,
|
|
694
712
|
"_recommendations" => rebuilt.recommendations,
|
|
695
|
-
"_comparison" => comparison
|
|
713
|
+
"_comparison" => comparison,
|
|
714
|
+
"_gates" => gates
|
|
696
715
|
).compact.merge(recorded_scores)
|
|
697
716
|
end
|
|
698
717
|
|
|
@@ -725,6 +744,7 @@ module ActionAgent
|
|
|
725
744
|
judge_trace_ids!(metadata["judge_trace_ids"])
|
|
726
745
|
string!(report["judge"], "report.judge", 200)
|
|
727
746
|
validate_release!
|
|
747
|
+
validate_pass_thresholds!
|
|
728
748
|
validate_judge_usage!(report.dig("judge_usage", "run"), "report.judge_usage.run") if report["judge_usage"].is_a?(Hash)
|
|
729
749
|
optional_object!(report["judge_usage"], "report.judge_usage")
|
|
730
750
|
object!(report["models"], "report.models")
|
|
@@ -856,6 +876,33 @@ module ActionAgent
|
|
|
856
876
|
string!(release["label"], "report.release.label", 200)
|
|
857
877
|
end
|
|
858
878
|
|
|
879
|
+
# The bars the suite declared, as Report#to_h writes them: "threshold"
|
|
880
|
+
# and "min_pass_rate" from 0 to 1, and "groups", each group's
|
|
881
|
+
# "min_pass_rate" and "gated", by group key. Checked by PassThresholds
|
|
882
|
+
# itself, so the import refuses what the framework would.
|
|
883
|
+
def validate_pass_thresholds!
|
|
884
|
+
bars = report["pass_thresholds"]
|
|
885
|
+
return if bars.nil?
|
|
886
|
+
|
|
887
|
+
object!(bars, "report.pass_thresholds")
|
|
888
|
+
unknown = bars.keys - PASS_THRESHOLD_KEYS
|
|
889
|
+
raise Invalid, "report.pass_thresholds has unknown keys #{unknown.join(', ')}" if unknown.any?
|
|
890
|
+
|
|
891
|
+
groups = bars["groups"]
|
|
892
|
+
optional_object!(groups, "report.pass_thresholds.groups")
|
|
893
|
+
if groups && groups.size > MAX_PASS_GROUPS
|
|
894
|
+
raise Invalid, "report.pass_thresholds.groups must name at most #{MAX_PASS_GROUPS} groups"
|
|
895
|
+
end
|
|
896
|
+
|
|
897
|
+
(groups || {}).each_key { |key| string!(key, "report.pass_thresholds.groups key", 200) }
|
|
898
|
+
# A framework without per-group bars stores none, as before.
|
|
899
|
+
return unless defined?(ActiveAgent::Evals::PassThresholds)
|
|
900
|
+
|
|
901
|
+
ActiveAgent::Evals::PassThresholds.new(suite: bars.slice(*ActiveAgent::Evals::PassThresholds::SUITE_KEYS), groups: groups || {})
|
|
902
|
+
rescue ArgumentError => e
|
|
903
|
+
raise Invalid, "report.pass_thresholds: #{e.message}"
|
|
904
|
+
end
|
|
905
|
+
|
|
859
906
|
# What the judge spent, as ActiveAgent::Evals::Report writes it per
|
|
860
907
|
# result and for the run.
|
|
861
908
|
def validate_judge_usage!(usage, name)
|
|
@@ -181,6 +181,10 @@ module ActionAgent
|
|
|
181
181
|
metadata: entry.metadata, agent: resolve_agent(entry) || (merge? ? product.agent : nil),
|
|
182
182
|
project: resolve_project(entry) || (merge? ? product.project : nil)
|
|
183
183
|
)
|
|
184
|
+
# Until add_catalog_product_criteria runs, imports keep working without the lists.
|
|
185
|
+
product.criteria = entry.criteria if product.has_attribute?(:criteria)
|
|
186
|
+
# Until add_catalog_pass_bars runs, imports keep working without the bars.
|
|
187
|
+
product.pass = entry.pass if product.has_attribute?(:pass)
|
|
184
188
|
product.position = position_for(product, entry.position, offset)
|
|
185
189
|
product.save!
|
|
186
190
|
replace_sets(product, entry)
|
|
@@ -197,6 +201,7 @@ module ActionAgent
|
|
|
197
201
|
name: set_entry.name, description: set_entry.description, judge: set_entry.judge,
|
|
198
202
|
criteria: set_entry.criteria, metadata: set_entry.metadata
|
|
199
203
|
)
|
|
204
|
+
set.pass = set_entry.pass if set.has_attribute?(:pass)
|
|
200
205
|
set.position = position_for(set, set_entry.position, offset)
|
|
201
206
|
set.save!
|
|
202
207
|
replace_scenarios(set, set_entry)
|
|
@@ -22,6 +22,11 @@ module ActionAgent
|
|
|
22
22
|
# decisive (ActiveAgent::Evals::Comparison)
|
|
23
23
|
# "_selection" — the scenarios and models this run covered, and
|
|
24
24
|
# the checkout sandbox it replayed against, if any
|
|
25
|
+
# "_pass_thresholds" — for an evaluation of a catalog set that declares
|
|
26
|
+
# `pass` bars: the bars, recorded as the run starts,
|
|
27
|
+
# so every replay and the completion gate by the same
|
|
28
|
+
# ones however the catalog changes meanwhile
|
|
29
|
+
# "_gates" — with them: each group's gate per model, met or not
|
|
25
30
|
#
|
|
26
31
|
# A selection's `sandbox_id` names a checkout sandbox whose app runtime
|
|
27
32
|
# every replay reaches beside the agent's own MCP servers, as if the agent
|
|
@@ -81,6 +86,7 @@ module ActionAgent
|
|
|
81
86
|
specs = model_specs
|
|
82
87
|
run = @run || @evaluation.evaluation_runs.create!(status: :pending)
|
|
83
88
|
run.update!(status: :running, selection: selection_summary(scenarios, specs))
|
|
89
|
+
record_pass_thresholds!(run)
|
|
84
90
|
ensure_sandbox_live!
|
|
85
91
|
|
|
86
92
|
if scenarios.empty?
|
|
@@ -125,7 +131,7 @@ module ActionAgent
|
|
|
125
131
|
adapter.call(evaluation: @evaluation, owner: owner, scenarios: tasks, models: specs, on_result: metered)
|
|
126
132
|
else
|
|
127
133
|
ensure_judge_defined_kpis! if @evaluation.judge_defined?
|
|
128
|
-
default_report(tasks, specs, on_result)
|
|
134
|
+
default_report(tasks, specs, on_result, run)
|
|
129
135
|
end
|
|
130
136
|
raise ArgumentError, "scenario evaluation adapter must return an ActiveAgent::Evals::Report" unless report.is_a?(Evals::Report)
|
|
131
137
|
reported = report.results.map { |result| [ result.scenario.key, result.label ] }
|
|
@@ -453,7 +459,7 @@ module ActionAgent
|
|
|
453
459
|
available_tools: fan_out_state(run).fetch("tools") { tool_roster },
|
|
454
460
|
instructions: @evaluation.agent.instructions,
|
|
455
461
|
agent_name: @evaluation.agent.name,
|
|
456
|
-
|
|
462
|
+
**gating(run.pass_thresholds, Evals::Runner),
|
|
457
463
|
replay: ->(*) { raise ArgumentError, "a fanned-out result is scored from its stored replay" },
|
|
458
464
|
refine_gate: -> { reserve_refinement!(run) }
|
|
459
465
|
)
|
|
@@ -513,7 +519,7 @@ module ActionAgent
|
|
|
513
519
|
)
|
|
514
520
|
end
|
|
515
521
|
report = Evals::Report.new(results: results, models: specs.values, judge: evals_judge,
|
|
516
|
-
instructions: @evaluation.agent.instructions,
|
|
522
|
+
instructions: @evaluation.agent.instructions, **gating(run.pass_thresholds, Evals::Report))
|
|
517
523
|
state = fan_out_state(run)
|
|
518
524
|
|
|
519
525
|
# The other jobs' judge calls go into this runner's meter before the
|
|
@@ -559,7 +565,7 @@ module ActionAgent
|
|
|
559
565
|
error.is_a?(Exception) ? "#{error.class}: #{error.message}" : error.to_s
|
|
560
566
|
end
|
|
561
567
|
|
|
562
|
-
def default_report(tasks, specs, on_result)
|
|
568
|
+
def default_report(tasks, specs, on_result, run)
|
|
563
569
|
Evals::Runner.new(
|
|
564
570
|
scenarios: tasks,
|
|
565
571
|
models: specs,
|
|
@@ -568,7 +574,7 @@ module ActionAgent
|
|
|
568
574
|
available_tools: tool_roster,
|
|
569
575
|
instructions: @evaluation.agent.instructions,
|
|
570
576
|
agent_name: @evaluation.agent.name,
|
|
571
|
-
|
|
577
|
+
**gating(run.pass_thresholds, Evals::Runner),
|
|
572
578
|
replay: ->(scenario, spec) { replay(scenario, spec) },
|
|
573
579
|
on_result: on_result
|
|
574
580
|
).call
|
|
@@ -827,6 +833,7 @@ module ActionAgent
|
|
|
827
833
|
comparison = report.comparison if report.respond_to?(:comparison)
|
|
828
834
|
scores["_comparison"] = comparison if comparison
|
|
829
835
|
scores["_selection"] = run.selection
|
|
836
|
+
record_gates(scores, report, run.pass_thresholds)
|
|
830
837
|
scores["_metadata"] = report.metadata
|
|
831
838
|
scores["_judge_label"] = report.judge_label || report.judge&.label
|
|
832
839
|
# The judge's own spend, apart from the replays' (which the results
|
|
@@ -839,6 +846,60 @@ module ActionAgent
|
|
|
839
846
|
scores
|
|
840
847
|
end
|
|
841
848
|
|
|
849
|
+
# The bars a host adapter's report did not carry are the run's own, so
|
|
850
|
+
# the run is gated however its replays ran.
|
|
851
|
+
def record_gates(scores, report, recorded)
|
|
852
|
+
thresholds = (report.pass_thresholds if report.respond_to?(:pass_thresholds)) || recorded
|
|
853
|
+
return if thresholds.nil?
|
|
854
|
+
|
|
855
|
+
scores["_pass_thresholds"] = thresholds.to_h
|
|
856
|
+
scores["_gates"] = report.gates(thresholds).map(&:to_h)
|
|
857
|
+
end
|
|
858
|
+
|
|
859
|
+
# --- pass bars ----------------------------------------------------------
|
|
860
|
+
|
|
861
|
+
# The score keyword, and the bars when the run has them and the
|
|
862
|
+
# installed framework's +klass+ takes them: the bars' threshold is the
|
|
863
|
+
# catalog's or PASS_THRESHOLD, the score the dashboard always used.
|
|
864
|
+
def gating(thresholds, klass)
|
|
865
|
+
return { threshold: PASS_THRESHOLD } if thresholds.nil? || !accepts_keyword?(klass, :pass_thresholds)
|
|
866
|
+
|
|
867
|
+
{ threshold: thresholds.threshold, pass_thresholds: thresholds }
|
|
868
|
+
end
|
|
869
|
+
|
|
870
|
+
def accepts_keyword?(klass, keyword)
|
|
871
|
+
klass.instance_method(:initialize).parameters.any? { |type, name| name == keyword && %i[key keyreq].include?(type) }
|
|
872
|
+
end
|
|
873
|
+
|
|
874
|
+
# Records the bars of the catalog set this evaluation runs as, once,
|
|
875
|
+
# as the run starts: a fanned-out replay reads them from the run.
|
|
876
|
+
def record_pass_thresholds!(run)
|
|
877
|
+
thresholds = catalog_pass_thresholds
|
|
878
|
+
return if thresholds.nil?
|
|
879
|
+
|
|
880
|
+
run.update!(scores: (run.scores.is_a?(Hash) ? run.scores : {}).merge("_pass_thresholds" => thresholds.to_h))
|
|
881
|
+
end
|
|
882
|
+
|
|
883
|
+
# The product's bars as the suite's and the set's as its group's, as
|
|
884
|
+
# Catalog#suite_for gives them, for an evaluation a catalog set
|
|
885
|
+
# materialized (ScenarioSet#catalog_reference) when the set, under the
|
|
886
|
+
# owner's catalogs, still has the keys the reference names; nil when
|
|
887
|
+
# it declares none, and on a framework without per-group bars.
|
|
888
|
+
def catalog_pass_thresholds
|
|
889
|
+
reference = @evaluation.config.to_h["catalog"]
|
|
890
|
+
return nil unless reference.is_a?(Hash) && defined?(Evals::PassThresholds)
|
|
891
|
+
|
|
892
|
+
set = ScenarioSet.joins(:product).includes(:product)
|
|
893
|
+
.where(ScenarioProduct.table_name => { scenario_catalog_id: ScenarioCatalog.for_owner(owner).select(:id) })
|
|
894
|
+
.find_by(id: reference["set_id"], key: reference["set_key"].to_s)
|
|
895
|
+
return nil if set.nil? || set.product.key != reference["product_key"].to_s
|
|
896
|
+
return nil if set.product.pass.empty? && set.pass.empty?
|
|
897
|
+
|
|
898
|
+
Evals::PassThresholds.new(suite: set.product.pass, groups: { set.key => set.pass })
|
|
899
|
+
rescue ArgumentError
|
|
900
|
+
nil
|
|
901
|
+
end
|
|
902
|
+
|
|
842
903
|
# Records who judged once the calls are in. A judge with no model pinned
|
|
843
904
|
# started out labelled by its provider; it is renamed after the model
|
|
844
905
|
# that answered, here and on the verdict it wrote.
|
data/lib/action_agent/version.rb
CHANGED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AddCatalogProductCriteria < ActiveRecord::Migration<%= migration_version %>
|
|
4
|
+
def up
|
|
5
|
+
return unless table_exists?(products_table)
|
|
6
|
+
return if column_exists?(products_table, :criteria)
|
|
7
|
+
|
|
8
|
+
type = connection.adapter_name.to_s.downcase.include?("postgres") ? :jsonb : :json
|
|
9
|
+
add_column products_table, :criteria, type
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def down
|
|
13
|
+
return unless table_exists?(products_table)
|
|
14
|
+
return unless column_exists?(products_table, :criteria)
|
|
15
|
+
|
|
16
|
+
remove_column products_table, :criteria
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def products_table
|
|
22
|
+
"#{ActionAgent.table_name_prefix}scenario_products"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AddCatalogPassBars < ActiveRecord::Migration<%= migration_version %>
|
|
4
|
+
def up
|
|
5
|
+
type = connection.adapter_name.to_s.downcase.include?("postgres") ? :jsonb : :json
|
|
6
|
+
catalog_tables.each do |table|
|
|
7
|
+
next unless table_exists?(table)
|
|
8
|
+
next if column_exists?(table, :pass)
|
|
9
|
+
|
|
10
|
+
add_column table, :pass, type
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def down
|
|
15
|
+
catalog_tables.each do |table|
|
|
16
|
+
next unless table_exists?(table)
|
|
17
|
+
next unless column_exists?(table, :pass)
|
|
18
|
+
|
|
19
|
+
remove_column table, :pass
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def catalog_tables
|
|
26
|
+
%w[scenario_products scenario_sets].map { |table| "#{ActionAgent.table_name_prefix}#{table}" }
|
|
27
|
+
end
|
|
28
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: actionagent
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.9.
|
|
4
|
+
version: 1.9.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Justin Bowen
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-11-
|
|
11
|
+
date: 2026-11-12 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activeagent
|
|
@@ -383,6 +383,8 @@ files:
|
|
|
383
383
|
- lib/generators/action_agent/templates/migrations/030_add_evaluation_scoring_leases.rb.erb
|
|
384
384
|
- lib/generators/action_agent/templates/migrations/031_add_catalog_scenario_metadata.rb.erb
|
|
385
385
|
- lib/generators/action_agent/templates/migrations/032_create_active_agent_mcp_server_configs.rb.erb
|
|
386
|
+
- lib/generators/action_agent/templates/migrations/034_add_catalog_product_criteria.rb.erb
|
|
387
|
+
- lib/generators/action_agent/templates/migrations/035_add_catalog_pass_bars.rb.erb
|
|
386
388
|
- lib/tasks/action_agent.rake
|
|
387
389
|
- lib/tasks/browser.rake
|
|
388
390
|
- lib/tasks/claude_code.rake
|