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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e30a6a27a48e1a23c1947477068eec74f8ff2ff869bab4592f68c612a728dc76
4
- data.tar.gz: c1cdf115062d9ddd903296574420bd75128995460ce26e64e44681cdd6655ed9
3
+ metadata.gz: 8242f60832e3c23d204132839a832e082eb5115c26afd46eb2082ca4e342f908
4
+ data.tar.gz: 5798c4e0f853e3aa5a2ae20291a7909cc5f70dd7baaa5e9bd2765cb5cbbe5f4f
5
5
  SHA512:
6
- metadata.gz: ef4c4ddecf48be58b43ff9d02b1e2743d75b28eabdccf13bc35db32c0225b4a880318a4e35faa7ba8c235a29f77c934ad323f5099a355edd0d29ba18f8293566
7
- data.tar.gz: cd6b8331a0ad697ca16e71c7d6eb623d70e050c1fe50332672bd797c01a42509cef2c2ba6738bbebf3dfdff6d1feb07a29ea2f205408390259ea8d809622103a
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
- scope = @run.responses.where(status: "failed")
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 = Run.create!(
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, responses: :reviews).order(created_at: :desc).display_scoped
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 = Run.create!(
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
- scope = @run.responses.where(status: "failed")
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"]).as_json)
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["metric_ids"])
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.as_json)
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["metric_ids"]) if args.key?("metric_ids")
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.as_json)
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.as_json)
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.responses.size %><% if run.dataset %><span class="ck-runs-table__count-of">/<%= run.dataset.row_count %></span><% end %>
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>
@@ -1,3 +1,3 @@
1
1
  module CompletionKit
2
- VERSION = "0.28.19"
2
+ VERSION = "0.28.21"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: completion-kit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.28.19
4
+ version: 0.28.21
5
5
  platform: ruby
6
6
  authors:
7
7
  - Damien Bastin