completion-kit 0.28.19 → 0.28.21
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/controllers/completion_kit/api/v1/runs_controller.rb +2 -30
- data/app/controllers/completion_kit/runs_controller.rb +5 -34
- data/app/models/completion_kit/run.rb +105 -0
- data/app/services/completion_kit/mcp_tools/runs.rb +66 -7
- data/app/views/completion_kit/runs/_row.html.erb +1 -1
- data/lib/completion_kit/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8242f60832e3c23d204132839a832e082eb5115c26afd46eb2082ca4e342f908
|
|
4
|
+
data.tar.gz: 5798c4e0f853e3aa5a2ae20291a7909cc5f70dd7baaa5e9bd2765cb5cbbe5f4f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ec13a779260638ccd8968c9c3be4bd250780fdda06f01ea13b9963553756329cb0c092603a7ee66118e854227bcdb68b2b360e66a1e7830c2cfeffc879bf35d3
|
|
7
|
+
data.tar.gz: 92dd2c71fd38a9cc31638c77927a21a9a53f6ba8a4f4b81dc876f652dbf91c3d601722b402dd71f108d7530ab96442d1d2c113aadf60684ab767902c1e3ec1c9
|
|
@@ -54,40 +54,12 @@ module CompletionKit
|
|
|
54
54
|
return render_error("Judge has changed since this run executed. Retry would mix versions in the same run; use POST /api/v1/runs/:id/rerun instead.", status: :conflict)
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
scope = scope.where(id: params[:only]) if params[:only].present?
|
|
59
|
-
|
|
60
|
-
ActiveRecord::Base.transaction do
|
|
61
|
-
failed_response_ids = scope.pluck(:id)
|
|
62
|
-
CompletionKit::Review.where(response_id: failed_response_ids, status: "failed").update_all(
|
|
63
|
-
status: "pending", attempts: 0,
|
|
64
|
-
error_provider: nil, error_class: nil, error_status: nil, error_message: nil,
|
|
65
|
-
ai_score: nil, passed: nil, ai_feedback: nil
|
|
66
|
-
)
|
|
67
|
-
scope.update_all(
|
|
68
|
-
status: "pending", attempts: 0,
|
|
69
|
-
error_provider: nil, error_class: nil, error_status: nil, error_message: nil,
|
|
70
|
-
response_text: nil
|
|
71
|
-
)
|
|
72
|
-
@run.update!(status: "running")
|
|
73
|
-
failed_response_ids.each { |rid| CompletionKit::GenerateRowJob.perform_later(@run.id, rid) }
|
|
74
|
-
end
|
|
75
|
-
|
|
57
|
+
@run.retry_failures!(only: params[:only])
|
|
76
58
|
render json: @run.reload, status: :accepted
|
|
77
59
|
end
|
|
78
60
|
|
|
79
61
|
def rerun
|
|
80
|
-
new_run =
|
|
81
|
-
prompt_id: @run.prompt_id,
|
|
82
|
-
dataset_id: @run.dataset_id,
|
|
83
|
-
judge_model: @run.judge_model,
|
|
84
|
-
temperature: @run.temperature,
|
|
85
|
-
output_column: @run.output_column,
|
|
86
|
-
expected_column: @run.expected_column,
|
|
87
|
-
tag_names: @run.tag_names,
|
|
88
|
-
status: "pending"
|
|
89
|
-
)
|
|
90
|
-
new_run.replace_metrics!(@run.metric_ids)
|
|
62
|
+
new_run = @run.rerun!
|
|
91
63
|
if new_run.start!
|
|
92
64
|
render json: new_run.reload, status: :accepted
|
|
93
65
|
else
|
|
@@ -6,8 +6,9 @@ module CompletionKit
|
|
|
6
6
|
before_action :load_form_collections, only: [:new, :edit, :create, :update]
|
|
7
7
|
|
|
8
8
|
def index
|
|
9
|
-
scope = Run.includes(:prompt, :dataset, :tags
|
|
10
|
-
@runs = apply_tag_filter(scope)
|
|
9
|
+
scope = Run.includes(:prompt, :dataset, :tags).order(created_at: :desc).display_scoped
|
|
10
|
+
@runs = apply_tag_filter(scope).load
|
|
11
|
+
Run.preload_summaries(@runs)
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
RESPONSES_PER_PAGE = 100
|
|
@@ -110,17 +111,7 @@ module CompletionKit
|
|
|
110
111
|
end
|
|
111
112
|
|
|
112
113
|
def rerun
|
|
113
|
-
new_run =
|
|
114
|
-
prompt_id: @run.prompt_id,
|
|
115
|
-
dataset_id: @run.dataset_id,
|
|
116
|
-
judge_model: @run.judge_model,
|
|
117
|
-
temperature: @run.temperature,
|
|
118
|
-
output_column: @run.output_column,
|
|
119
|
-
expected_column: @run.expected_column,
|
|
120
|
-
tag_names: @run.tag_names,
|
|
121
|
-
status: "pending"
|
|
122
|
-
)
|
|
123
|
-
new_run.replace_metrics!(@run.metric_ids)
|
|
114
|
+
new_run = @run.rerun!
|
|
124
115
|
if new_run.start!
|
|
125
116
|
redirect_to run_path(new_run), notice: "Re-running with the same configuration."
|
|
126
117
|
else
|
|
@@ -162,27 +153,7 @@ module CompletionKit
|
|
|
162
153
|
return
|
|
163
154
|
end
|
|
164
155
|
|
|
165
|
-
|
|
166
|
-
scope = scope.where(id: params[:only]) if params[:only].present?
|
|
167
|
-
|
|
168
|
-
ActiveRecord::Base.transaction do
|
|
169
|
-
failed_response_ids = scope.pluck(:id)
|
|
170
|
-
Review.where(response_id: failed_response_ids, status: "failed").update_all(
|
|
171
|
-
status: "pending",
|
|
172
|
-
attempts: 0,
|
|
173
|
-
error_provider: nil, error_class: nil, error_status: nil, error_message: nil,
|
|
174
|
-
ai_score: nil, passed: nil, ai_feedback: nil
|
|
175
|
-
)
|
|
176
|
-
scope.update_all(
|
|
177
|
-
status: "pending",
|
|
178
|
-
attempts: 0,
|
|
179
|
-
error_provider: nil, error_class: nil, error_status: nil, error_message: nil,
|
|
180
|
-
response_text: nil
|
|
181
|
-
)
|
|
182
|
-
@run.update!(status: "running")
|
|
183
|
-
failed_response_ids.each { |rid| GenerateRowJob.perform_later(@run.id, rid) }
|
|
184
|
-
end
|
|
185
|
-
|
|
156
|
+
@run.retry_failures!(only: params[:only])
|
|
186
157
|
@run.broadcast_ui
|
|
187
158
|
redirect_to run_path(@run)
|
|
188
159
|
end
|
|
@@ -14,6 +14,8 @@ module CompletionKit
|
|
|
14
14
|
has_many :suggestions, dependent: :destroy
|
|
15
15
|
has_many :dashboard_dismissals, as: :dismissable, dependent: :destroy
|
|
16
16
|
|
|
17
|
+
attr_writer :avg_score, :check_pass_rate, :metric_averages, :response_count
|
|
18
|
+
|
|
17
19
|
validates :name, presence: true
|
|
18
20
|
validates :status, inclusion: { in: STATUSES }
|
|
19
21
|
validate :dataset_supplies_prompt_variables
|
|
@@ -34,6 +36,58 @@ module CompletionKit
|
|
|
34
36
|
display_scoped.select(:id)
|
|
35
37
|
end
|
|
36
38
|
|
|
39
|
+
# Batch-compute the list-view summaries (response count, avg score, check
|
|
40
|
+
# pass rate, per-metric averages) for a set of runs in a constant number of
|
|
41
|
+
# grouped queries, injecting the results so the index never loads a single
|
|
42
|
+
# response or review object. Mirrors the per-run reader methods exactly.
|
|
43
|
+
def self.preload_summaries(runs)
|
|
44
|
+
runs = runs.to_a
|
|
45
|
+
return runs if runs.empty?
|
|
46
|
+
|
|
47
|
+
run_ids = runs.map(&:id)
|
|
48
|
+
counts = Response.where(run_id: run_ids).group(:run_id).count
|
|
49
|
+
|
|
50
|
+
run_col = Arel.sql("completion_kit_responses.run_id")
|
|
51
|
+
base = Review.joins(:response).where(completion_kit_responses: {run_id: run_ids})
|
|
52
|
+
|
|
53
|
+
run_rows = base.group(run_col).pluck(
|
|
54
|
+
run_col,
|
|
55
|
+
Arel.sql("AVG(ai_score)"),
|
|
56
|
+
Arel.sql("COUNT(passed)"),
|
|
57
|
+
Arel.sql("SUM(CASE WHEN passed THEN 1 ELSE 0 END)")
|
|
58
|
+
)
|
|
59
|
+
run_stats = run_rows.each_with_object({}) do |(rid, avg, resolved, passed), h|
|
|
60
|
+
h[rid] = {avg: avg, resolved: resolved.to_i, passed: passed.to_i}
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
metric_rows = base.group(run_col, :metric_name).pluck(
|
|
64
|
+
run_col,
|
|
65
|
+
:metric_name,
|
|
66
|
+
Arel.sql("AVG(ai_score)"),
|
|
67
|
+
Arel.sql("COUNT(ai_score)"),
|
|
68
|
+
Arel.sql("COUNT(passed)"),
|
|
69
|
+
Arel.sql("SUM(CASE WHEN passed THEN 1 ELSE 0 END)")
|
|
70
|
+
)
|
|
71
|
+
metrics_by_run = metric_rows.group_by(&:first)
|
|
72
|
+
|
|
73
|
+
runs.each do |run|
|
|
74
|
+
run.response_count = counts.fetch(run.id, 0)
|
|
75
|
+
|
|
76
|
+
stats = run_stats[run.id]
|
|
77
|
+
run.avg_score = stats && stats[:avg] ? stats[:avg].to_f.round(2) : nil
|
|
78
|
+
run.check_pass_rate = stats && stats[:resolved] > 0 ? (stats[:passed].to_f / stats[:resolved]).round(2) : nil
|
|
79
|
+
|
|
80
|
+
run.metric_averages = (metrics_by_run[run.id] || []).filter_map do |(_rid, name, avg, scored, resolved, passed)|
|
|
81
|
+
if scored.to_i > 0
|
|
82
|
+
{name: name, avg: avg.to_f.round(1)}
|
|
83
|
+
elsif resolved.to_i > 0
|
|
84
|
+
{name: name, kind: "check", pass_rate: (passed.to_i.to_f / resolved.to_i).round(2)}
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
runs
|
|
89
|
+
end
|
|
90
|
+
|
|
37
91
|
# A scoring-only run grades a pre-existing column on the dataset instead of
|
|
38
92
|
# generating new outputs. No prompt is attached; the response text is read
|
|
39
93
|
# from row[output_column]; no LLM generation happens.
|
|
@@ -126,7 +180,15 @@ module CompletionKit
|
|
|
126
180
|
end
|
|
127
181
|
end
|
|
128
182
|
|
|
183
|
+
def response_count
|
|
184
|
+
return @response_count if defined?(@response_count)
|
|
185
|
+
|
|
186
|
+
responses.size
|
|
187
|
+
end
|
|
188
|
+
|
|
129
189
|
def avg_score
|
|
190
|
+
return @avg_score if defined?(@avg_score)
|
|
191
|
+
|
|
130
192
|
scores = reviews_for_summary.map(&:ai_score).compact.map(&:to_f)
|
|
131
193
|
return nil if scores.empty?
|
|
132
194
|
|
|
@@ -134,6 +196,8 @@ module CompletionKit
|
|
|
134
196
|
end
|
|
135
197
|
|
|
136
198
|
def metric_averages
|
|
199
|
+
return @metric_averages if defined?(@metric_averages)
|
|
200
|
+
|
|
137
201
|
reviews_for_summary.group_by(&:metric_name).filter_map do |name, reviews|
|
|
138
202
|
scored = reviews.select { |r| r.ai_score.present? }
|
|
139
203
|
if scored.any?
|
|
@@ -150,6 +214,8 @@ module CompletionKit
|
|
|
150
214
|
end
|
|
151
215
|
|
|
152
216
|
def check_pass_rate
|
|
217
|
+
return @check_pass_rate if defined?(@check_pass_rate)
|
|
218
|
+
|
|
153
219
|
resolved = reviews_for_summary.reject { |r| r.passed.nil? }
|
|
154
220
|
return nil if resolved.empty?
|
|
155
221
|
|
|
@@ -307,6 +373,45 @@ module CompletionKit
|
|
|
307
373
|
true
|
|
308
374
|
end
|
|
309
375
|
|
|
376
|
+
def rerun!
|
|
377
|
+
new_run = Run.create!(
|
|
378
|
+
prompt_id: prompt_id,
|
|
379
|
+
dataset_id: dataset_id,
|
|
380
|
+
judge_model: judge_model,
|
|
381
|
+
temperature: temperature,
|
|
382
|
+
output_column: output_column,
|
|
383
|
+
expected_column: expected_column,
|
|
384
|
+
tag_names: tag_names,
|
|
385
|
+
status: "pending"
|
|
386
|
+
)
|
|
387
|
+
new_run.replace_metrics!(metric_ids)
|
|
388
|
+
new_run
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def retry_failures!(only: nil)
|
|
392
|
+
scope = responses.where(status: "failed")
|
|
393
|
+
scope = scope.where(id: only) if only.present?
|
|
394
|
+
|
|
395
|
+
transaction do
|
|
396
|
+
failed_response_ids = scope.pluck(:id)
|
|
397
|
+
Review.where(response_id: failed_response_ids, status: "failed").update_all(
|
|
398
|
+
status: "pending",
|
|
399
|
+
attempts: 0,
|
|
400
|
+
error_provider: nil, error_class: nil, error_status: nil, error_message: nil,
|
|
401
|
+
ai_score: nil, passed: nil, ai_feedback: nil
|
|
402
|
+
)
|
|
403
|
+
scope.update_all(
|
|
404
|
+
status: "pending",
|
|
405
|
+
attempts: 0,
|
|
406
|
+
error_provider: nil, error_class: nil, error_status: nil, error_message: nil,
|
|
407
|
+
response_text: nil
|
|
408
|
+
)
|
|
409
|
+
update!(status: "running")
|
|
410
|
+
failed_response_ids.each { |rid| GenerateRowJob.perform_later(id, rid) }
|
|
411
|
+
end
|
|
412
|
+
self
|
|
413
|
+
end
|
|
414
|
+
|
|
310
415
|
def progress_snapshot
|
|
311
416
|
generated_done = responses.where(status: "succeeded").count
|
|
312
417
|
generated_failed = responses.where(status: "failed").count
|
|
@@ -24,6 +24,7 @@ module CompletionKit
|
|
|
24
24
|
output_column: {type: "string", description: "Dataset column to grade when prompt_id is omitted; defaults to \"actual_output\"."},
|
|
25
25
|
expected_column: {type: "string", description: "Dataset column holding each row's answer key / ground truth, graded by checks with compare_to \"expected\" and passed to the judge; defaults to \"expected_output\"."},
|
|
26
26
|
metric_ids: {type: "array", items: {type: "integer"}},
|
|
27
|
+
metric_group_id: {type: "integer", description: "Attach the metrics belonging to this metric group (its current metric_ids). Ignored when metric_ids is also given."},
|
|
27
28
|
tag_names: {type: "array", items: {type: "string"}}
|
|
28
29
|
},
|
|
29
30
|
required: ["name"]
|
|
@@ -40,6 +41,7 @@ module CompletionKit
|
|
|
40
41
|
output_column: {type: "string"},
|
|
41
42
|
expected_column: {type: "string"},
|
|
42
43
|
metric_ids: {type: "array", items: {type: "integer"}},
|
|
44
|
+
metric_group_id: {type: "integer", description: "Replace the run's metrics with those belonging to this metric group. Ignored when metric_ids is also given."},
|
|
43
45
|
tag_names: {type: "array", items: {type: "string"}}
|
|
44
46
|
},
|
|
45
47
|
required: ["id"]
|
|
@@ -55,6 +57,21 @@ module CompletionKit
|
|
|
55
57
|
description: "Start a run. Required for every run, including score-only runs (no prompt): generates responses with the prompt when there is one, otherwise copies the graded dataset column and grades it.",
|
|
56
58
|
inputSchema: {type: "object", properties: {id: {type: "integer"}}, required: ["id"]},
|
|
57
59
|
handler: :generate
|
|
60
|
+
},
|
|
61
|
+
"runs_regrade" => {
|
|
62
|
+
description: "Re-grade a run's existing responses with its currently attached metrics, without regenerating. Use after attaching or editing metrics on an already-generated run.",
|
|
63
|
+
inputSchema: {type: "object", properties: {id: {type: "integer"}}, required: ["id"]},
|
|
64
|
+
handler: :regrade
|
|
65
|
+
},
|
|
66
|
+
"runs_rerun" => {
|
|
67
|
+
description: "Create and start a fresh copy of a run with the same prompt, dataset, metrics, and settings. Use when the judge changed and you want a clean run instead of mixing versions.",
|
|
68
|
+
inputSchema: {type: "object", properties: {id: {type: "integer"}}, required: ["id"]},
|
|
69
|
+
handler: :rerun
|
|
70
|
+
},
|
|
71
|
+
"runs_retry_failures" => {
|
|
72
|
+
description: "Re-run only the failed responses of a run, optionally limited to specific response ids via \"only\".",
|
|
73
|
+
inputSchema: {type: "object", properties: {id: {type: "integer"}, only: {type: "array", items: {type: "integer"}}}, required: ["id"]},
|
|
74
|
+
handler: :retry_failures
|
|
58
75
|
}
|
|
59
76
|
}.freeze
|
|
60
77
|
|
|
@@ -63,15 +80,15 @@ module CompletionKit
|
|
|
63
80
|
end
|
|
64
81
|
|
|
65
82
|
def self.get(args)
|
|
66
|
-
text_result(Run.find(args["id"])
|
|
83
|
+
text_result(run_payload(Run.find(args["id"])))
|
|
67
84
|
end
|
|
68
85
|
|
|
69
86
|
def self.create(args)
|
|
70
87
|
run = Run.new(args.slice("name", "prompt_id", "dataset_id", "judge_model", "output_column", "expected_column"))
|
|
71
88
|
if run.save
|
|
72
|
-
run.replace_metrics!(args
|
|
89
|
+
run.replace_metrics!(resolve_metric_ids(args))
|
|
73
90
|
run.update!(tag_names: args["tag_names"]) if args.key?("tag_names")
|
|
74
|
-
text_result(run.reload
|
|
91
|
+
text_result(run_payload(run.reload))
|
|
75
92
|
else
|
|
76
93
|
error_result(run.errors.full_messages.join(", "))
|
|
77
94
|
end
|
|
@@ -79,10 +96,10 @@ module CompletionKit
|
|
|
79
96
|
|
|
80
97
|
def self.update(args)
|
|
81
98
|
run = Run.find(args["id"])
|
|
82
|
-
if run.update(args.except("id", "metric_ids", "tag_names").slice("name", "dataset_id", "judge_model", "output_column", "expected_column"))
|
|
83
|
-
run.replace_metrics!(args
|
|
99
|
+
if run.update(args.except("id", "metric_ids", "metric_group_id", "tag_names").slice("name", "dataset_id", "judge_model", "output_column", "expected_column"))
|
|
100
|
+
run.replace_metrics!(resolve_metric_ids(args)) if args.key?("metric_ids") || args["metric_group_id"].present?
|
|
84
101
|
run.update!(tag_names: args["tag_names"]) if args.key?("tag_names")
|
|
85
|
-
text_result(run.reload
|
|
102
|
+
text_result(run_payload(run.reload))
|
|
86
103
|
else
|
|
87
104
|
error_result(run.errors.full_messages.join(", "))
|
|
88
105
|
end
|
|
@@ -96,11 +113,53 @@ module CompletionKit
|
|
|
96
113
|
def self.generate(args)
|
|
97
114
|
run = Run.find(args["id"])
|
|
98
115
|
if run.start!
|
|
99
|
-
text_result(run.reload
|
|
116
|
+
text_result(run_payload(run.reload))
|
|
100
117
|
else
|
|
101
118
|
text_result(run.failure_summary || run.errors.full_messages.to_sentence)
|
|
102
119
|
end
|
|
103
120
|
end
|
|
121
|
+
|
|
122
|
+
def self.regrade(args)
|
|
123
|
+
run = Run.find(args["id"])
|
|
124
|
+
if run.regrade!
|
|
125
|
+
text_result(run_payload(run.reload))
|
|
126
|
+
else
|
|
127
|
+
error_result("Nothing to re-grade. The run has no succeeded responses or no metrics attached.")
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def self.rerun(args)
|
|
132
|
+
new_run = Run.find(args["id"]).rerun!
|
|
133
|
+
if new_run.start!
|
|
134
|
+
text_result(run_payload(new_run.reload))
|
|
135
|
+
else
|
|
136
|
+
error_result(new_run.failure_summary || "Could not start the new run.")
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def self.retry_failures(args)
|
|
141
|
+
run = Run.find(args["id"])
|
|
142
|
+
if run.stale_review_summary.any?
|
|
143
|
+
return error_result("Judge has changed since this run executed. Retry would mix versions in the same run; use runs_rerun instead.")
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
run.retry_failures!(only: args["only"])
|
|
147
|
+
text_result(run_payload(run.reload))
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def self.resolve_metric_ids(args)
|
|
151
|
+
return args["metric_ids"] if args.key?("metric_ids")
|
|
152
|
+
return MetricGroup.find(args["metric_group_id"]).metric_ids if args["metric_group_id"].present?
|
|
153
|
+
|
|
154
|
+
nil
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def self.run_payload(run)
|
|
158
|
+
json = run.as_json
|
|
159
|
+
return json unless run.metric_ids.empty?
|
|
160
|
+
|
|
161
|
+
json.merge("warning" => "No metrics are attached, so this run judges nothing. Attach metric_ids or a metric_group_id before generating.")
|
|
162
|
+
end
|
|
104
163
|
end
|
|
105
164
|
end
|
|
106
165
|
end
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
</td>
|
|
28
28
|
<td>
|
|
29
29
|
<span class="ck-runs-table__count">
|
|
30
|
-
<%= run.
|
|
30
|
+
<%= run.response_count %><% if run.dataset %><span class="ck-runs-table__count-of">/<%= run.dataset.row_count %></span><% end %>
|
|
31
31
|
</span>
|
|
32
32
|
</td>
|
|
33
33
|
<td>
|