completion-kit 0.23.0 → 0.24.0

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: a3952f21a923b362bd293e0cf32cc358b2e635e07124d9b4696e9c8a153b344e
4
- data.tar.gz: 2c669f474bbb38dc79b5320b8323e9ac3e2ab9ff89664d9b149b204ebad71c14
3
+ metadata.gz: 9f8caf00fc4eac2a56d69561c61e292b983626da4c25e9e96c2c25983e7e717f
4
+ data.tar.gz: 1ed507e15ea8a6c1fb17eb0c05d1c0f883cb19b49e8450ac3b6802e002d81104
5
5
  SHA512:
6
- metadata.gz: 5c22d5b53cb2146ffd2c8bb9daee2d99410dd40ffe03d7547a89fe9ba73277094c666d9a657e0be69407dc5a2fcb6f66b2b8d7fae0d23568ee172c0d1affaa65
7
- data.tar.gz: 95dc7384b03ecc1895ae908a0ced7b631d7fa124b22ae291bf02a1052547a63e18ebb6186db5449058eebee3ec9ef26ed20d163ce55891bf6dc98c1cfbb0b040
6
+ metadata.gz: dbdb44e7b030c7dcb6fe5438db01b44985acda2ba517001e89681471bf6f4e6199c70c47f5df8add056498f34c5a5965c86b90d812307174ca71a7382afe98a8
7
+ data.tar.gz: c67e0e52295a0219614ab0733d9dd35e1fcc3a835f4f3e3825c8edd2f32878c9b86e9260d835e28d9ab5ecd07ae899ff44c2dcc347e94753245c9dfa1cd5e03e
@@ -1745,6 +1745,38 @@ tr:hover .ck-chip--publish {
1745
1745
  color: var(--ck-muted);
1746
1746
  }
1747
1747
 
1748
+ .ck-header-preview {
1749
+ display: flex;
1750
+ flex-wrap: wrap;
1751
+ align-items: center;
1752
+ gap: 0.4rem;
1753
+ margin-top: 0.3rem;
1754
+ }
1755
+
1756
+ .ck-header-preview__label {
1757
+ font-family: var(--ck-mono);
1758
+ font-size: 0.66rem;
1759
+ letter-spacing: 0.08em;
1760
+ text-transform: uppercase;
1761
+ color: var(--ck-dim);
1762
+ }
1763
+
1764
+ .ck-header-chip {
1765
+ display: inline-flex;
1766
+ align-items: center;
1767
+ padding: 0.12rem 0.5rem;
1768
+ border: 1px solid var(--ck-line-strong);
1769
+ border-radius: 999px;
1770
+ font-family: var(--ck-mono);
1771
+ font-size: 0.72rem;
1772
+ color: var(--ck-muted);
1773
+ }
1774
+
1775
+ .ck-header-chip--special {
1776
+ border-color: var(--ck-accent);
1777
+ color: var(--ck-accent);
1778
+ }
1779
+
1748
1780
  .ck-field--info #refresh-status,
1749
1781
  .ck-field--warn #refresh-status,
1750
1782
  .ck-field--error #refresh-status {
@@ -83,6 +83,7 @@ module CompletionKit
83
83
  judge_model: @run.judge_model,
84
84
  temperature: @run.temperature,
85
85
  output_column: @run.output_column,
86
+ expected_column: @run.expected_column,
86
87
  tag_names: @run.tag_names,
87
88
  status: "pending"
88
89
  )
@@ -157,7 +158,7 @@ module CompletionKit
157
158
  end
158
159
 
159
160
  def run_params
160
- params.permit(:name, :prompt_id, :dataset_id, :judge_model, :temperature, :output_column,
161
+ params.permit(:name, :prompt_id, :dataset_id, :judge_model, :temperature, :output_column, :expected_column,
161
162
  metric_ids: [], tag_names: [])
162
163
  end
163
164
  end
@@ -41,9 +41,16 @@ module CompletionKit
41
41
  if @run.responses.any? && run_generation_changed?
42
42
  attrs = run_params.except(:metric_ids).to_h
43
43
  attrs.delete("name") if attrs["name"].to_s == @run.name.to_s
44
- new_run = Run.create!(attrs.merge(status: "pending"))
45
- new_run.replace_metrics!(params[:run][:metric_ids]) if params[:run].key?(:metric_ids)
46
- redirect_to run_path(new_run), notice: "Saved as a new run. The previous run and its results are preserved."
44
+ new_run = Run.new(attrs.merge(status: "pending"))
45
+ if new_run.save
46
+ new_run.replace_metrics!(params[:run][:metric_ids]) if params[:run].key?(:metric_ids)
47
+ redirect_to run_path(new_run), notice: "Saved as a new run. The previous run and its results are preserved."
48
+ else
49
+ @run.assign_attributes(run_params.except(:metric_ids))
50
+ new_run.errors.each { |error| @run.errors.add(error.attribute, error.message) }
51
+ load_form_collections
52
+ render :edit, status: :unprocessable_entity
53
+ end
47
54
  elsif @run.update(run_params.except(:metric_ids))
48
55
  @run.replace_metrics!(params[:run][:metric_ids]) if params[:run].key?(:metric_ids)
49
56
  redirect_to run_path(@run), notice: "Run saved."
@@ -97,6 +104,7 @@ module CompletionKit
97
104
  judge_model: @run.judge_model,
98
105
  temperature: @run.temperature,
99
106
  output_column: @run.output_column,
107
+ expected_column: @run.expected_column,
100
108
  tag_names: @run.tag_names,
101
109
  status: "pending"
102
110
  )
@@ -224,13 +232,13 @@ module CompletionKit
224
232
  end
225
233
 
226
234
  def run_params
227
- params.require(:run).permit(:name, :prompt_id, :dataset_id, :judge_model, :temperature, :output_column, metric_ids: [], tag_names: [])
235
+ params.require(:run).permit(:name, :prompt_id, :dataset_id, :judge_model, :temperature, :output_column, :expected_column, metric_ids: [], tag_names: [])
228
236
  end
229
237
 
230
238
  # Editing a run that already has results forks a new run — but only when a
231
239
  # field that affects generation or judging changed. Renaming or retagging is
232
240
  # pure metadata and updates the run in place.
233
- GENERATION_RUN_FIELDS = %i[prompt_id dataset_id judge_model temperature output_column].freeze
241
+ GENERATION_RUN_FIELDS = %i[prompt_id dataset_id judge_model temperature output_column expected_column].freeze
234
242
 
235
243
  def run_generation_changed?
236
244
  GENERATION_RUN_FIELDS.each do |field|
@@ -65,7 +65,7 @@ module CompletionKit
65
65
  def judge_count
66
66
  model_ids = Model.where(provider: provider).pluck(:model_id)
67
67
  return 0 if model_ids.empty?
68
- Run.where(judge_model: model_ids).display_scoped.distinct.count(:judge_model)
68
+ Run.where(judge_model: model_ids).distinct.count(:judge_model)
69
69
  end
70
70
 
71
71
  def last_used_at
@@ -74,7 +74,6 @@ module CompletionKit
74
74
  prompt_scope = Prompt.where(llm_model: model_ids).select(:id)
75
75
  Run.where("prompt_id IN (:prompts) OR judge_model IN (:models)",
76
76
  prompts: prompt_scope, models: model_ids)
77
- .display_scoped
78
77
  .where.not(status: "pending")
79
78
  .maximum(:created_at)
80
79
  end
@@ -59,7 +59,7 @@ module CompletionKit
59
59
  private
60
60
 
61
61
  def requires_response_text?
62
- succeeded? && !run&.judge_only_input_data_checks?
62
+ succeeded? && !run&.judge_only?
63
63
  end
64
64
 
65
65
  def broadcast_row_update
@@ -17,9 +17,11 @@ module CompletionKit
17
17
  validates :status, inclusion: { in: STATUSES }
18
18
  validate :dataset_supplies_prompt_variables
19
19
  validate :judge_only_run_supplies_output_column
20
+ validate :dataset_supplies_expected_column
20
21
 
21
22
  before_validation :set_default_status, on: :create
22
23
  before_validation :set_auto_name, on: :create
24
+ after_create_commit :notify_host_of_creation
23
25
 
24
26
  def self.display_scoped
25
27
  filter = CompletionKit.config.runs_display_scope
@@ -198,40 +200,48 @@ module CompletionKit
198
200
  end
199
201
  end
200
202
 
201
- transaction do
202
- responses.destroy_all
203
- update!(
204
- status: "running",
205
- progress_current: 0,
206
- progress_total: rows.length,
207
- failure_summary: nil,
208
- error_message: nil
209
- )
210
- rows.each_with_index do |row, index|
211
- input = row.empty? ? nil : row.to_json
212
- attrs = {
213
- status: "pending",
214
- row_index: index,
215
- input_data: input,
216
- expected_output: row["expected_output"]
217
- }
218
- if judge_only?
219
- attrs[:status] = "succeeded"
220
- column = output_column.presence || "actual_output"
221
- attrs[:response_text] = row[column].to_s if dataset && dataset.headers.include?(column)
203
+ failed_row = nil
204
+ begin
205
+ transaction do
206
+ responses.destroy_all
207
+ update!(
208
+ status: "running",
209
+ progress_current: 0,
210
+ progress_total: rows.length,
211
+ failure_summary: nil,
212
+ error_message: nil
213
+ )
214
+ rows.each_with_index do |row, index|
215
+ failed_row = index
216
+ input = row.empty? ? nil : row.to_json
217
+ attrs = {
218
+ status: "pending",
219
+ row_index: index,
220
+ input_data: input,
221
+ expected_output: row[expected_column.presence || "expected_output"]
222
+ }
223
+ if judge_only?
224
+ attrs[:status] = "succeeded"
225
+ column = output_column.presence || "actual_output"
226
+ attrs[:response_text] = row[column].to_s if dataset && dataset.headers.include?(column)
227
+ end
228
+
229
+ response = responses.create!(attrs)
230
+
231
+ if judge_only?
232
+ llm_metrics.each { |m| JudgeReviewJob.perform_later(response.id, m.id, id) } if llm_judge_configured?
233
+ check_metrics.each { |m| CheckReviewJob.perform_later(response.id, m.id, id) }
234
+ else
235
+ GenerateRowJob.perform_later(id, response.id)
236
+ end
222
237
  end
223
238
 
224
- response = responses.create!(attrs)
225
-
226
- if judge_only?
227
- llm_metrics.each { |m| JudgeReviewJob.perform_later(response.id, m.id, id) } if llm_judge_configured?
228
- check_metrics.each { |m| CheckReviewJob.perform_later(response.id, m.id, id) }
229
- else
230
- GenerateRowJob.perform_later(id, response.id)
231
- end
239
+ RunCompletionCheckJob.perform_later(id) if judge_only?
232
240
  end
233
-
234
- RunCompletionCheckJob.perform_later(id) if judge_only?
241
+ rescue ActiveRecord::RecordInvalid => e
242
+ reload
243
+ detail = e.record.errors.full_messages.to_sentence
244
+ return fail_with_summary!(failed_row ? "Row #{failed_row + 1}: #{detail}" : detail)
235
245
  end
236
246
 
237
247
  safely_broadcast do
@@ -324,6 +334,7 @@ module CompletionKit
324
334
  id: id, name: name, status: status, prompt_id: prompt_id,
325
335
  dataset_id: dataset_id, judge_model: judge_model, temperature: temperature,
326
336
  output_column: output_column,
337
+ expected_column: expected_column,
327
338
  created_at: created_at, updated_at: updated_at,
328
339
  responses_count: responses.count, avg_score: avg_score,
329
340
  check_pass_rate: check_pass_rate,
@@ -407,6 +418,12 @@ module CompletionKit
407
418
 
408
419
  private
409
420
 
421
+ def notify_host_of_creation
422
+ CompletionKit.config.on_run_created&.call(self)
423
+ rescue StandardError => e
424
+ Rails.error.report(e, handled: true, context: { hook: "on_run_created", run_id: id })
425
+ end
426
+
410
427
  def fail_with_summary!(message)
411
428
  errors.add(:base, message)
412
429
  if persisted?
@@ -457,6 +474,14 @@ module CompletionKit
457
474
  end
458
475
  end
459
476
 
477
+ def dataset_supplies_expected_column
478
+ return if expected_column.blank? || dataset.nil?
479
+
480
+ unless dataset.headers.include?(expected_column)
481
+ errors.add(:expected_column, "\"#{expected_column}\" is not a column on dataset \"#{dataset.name}\"")
482
+ end
483
+ end
484
+
460
485
  def judge_only_run_supplies_output_column
461
486
  return if prompt.present?
462
487
 
@@ -19,7 +19,7 @@ module CompletionKit
19
19
  handler: :get
20
20
  },
21
21
  "datasets_create" => {
22
- description: "Create a dataset with CSV data",
22
+ description: "Create a dataset with CSV data. First row is the header. Two column names are recognized specially: \"expected_output\" is each row's answer key (ground truth) given to the judge and to checks that compare against the row's expected value, and \"actual_output\" is a pre-made output to score in a prompt-less run. Both are overridable per run (expected_column / output_column). Every column is also available to the prompt as a variable.",
23
23
  inputSchema: {
24
24
  type: "object",
25
25
  properties: {name: {type: "string"}, csv_data: {type: "string"}, tag_names: {type: "array", items: {type: "string"}}},
@@ -42,7 +42,7 @@ module CompletionKit
42
42
  handler: :delete
43
43
  },
44
44
  "datasets_create_from_url" => {
45
- description: "Create a dataset by downloading CSV from a URL instead of inlining it. Use this for large datasets: pass a public http(s) URL and the server fetches the CSV directly, so the data never has to pass through the tool-call arguments. The URL is SSRF-checked and the download is capped at 10MB.",
45
+ description: "Create a dataset by downloading CSV from a URL instead of inlining it. Use this for large datasets: pass a public http(s) URL and the server fetches the CSV directly, so the data never has to pass through the tool-call arguments. The URL is SSRF-checked and the download is capped at 10MB. First row is the header; the \"expected_output\" (answer key) and \"actual_output\" (pre-made output) columns are recognized specially, overridable per run.",
46
46
  inputSchema: {
47
47
  type: "object",
48
48
  properties: {
@@ -22,6 +22,7 @@ module CompletionKit
22
22
  name: {type: "string"}, prompt_id: {type: "integer"},
23
23
  dataset_id: {type: "integer"}, judge_model: {type: "string"},
24
24
  output_column: {type: "string", description: "Dataset column to grade when prompt_id is omitted; defaults to \"actual_output\"."},
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\"."},
25
26
  metric_ids: {type: "array", items: {type: "integer"}},
26
27
  tag_names: {type: "array", items: {type: "string"}}
27
28
  },
@@ -37,6 +38,7 @@ module CompletionKit
37
38
  id: {type: "integer"}, name: {type: "string"},
38
39
  dataset_id: {type: "integer"}, judge_model: {type: "string"},
39
40
  output_column: {type: "string"},
41
+ expected_column: {type: "string"},
40
42
  metric_ids: {type: "array", items: {type: "integer"}},
41
43
  tag_names: {type: "array", items: {type: "string"}}
42
44
  },
@@ -50,7 +52,7 @@ module CompletionKit
50
52
  handler: :delete
51
53
  },
52
54
  "runs_generate" => {
53
- description: "Generate responses for a run using its prompt and dataset",
55
+ 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.",
54
56
  inputSchema: {type: "object", properties: {id: {type: "integer"}}, required: ["id"]},
55
57
  handler: :generate
56
58
  }
@@ -65,7 +67,7 @@ module CompletionKit
65
67
  end
66
68
 
67
69
  def self.create(args)
68
- run = Run.new(args.slice("name", "prompt_id", "dataset_id", "judge_model", "output_column"))
70
+ run = Run.new(args.slice("name", "prompt_id", "dataset_id", "judge_model", "output_column", "expected_column"))
69
71
  if run.save
70
72
  run.replace_metrics!(args["metric_ids"])
71
73
  run.update!(tag_names: args["tag_names"]) if args.key?("tag_names")
@@ -77,7 +79,7 @@ module CompletionKit
77
79
 
78
80
  def self.update(args)
79
81
  run = Run.find(args["id"])
80
- if run.update(args.except("id", "metric_ids", "tag_names").slice("name", "dataset_id", "judge_model", "output_column"))
82
+ if run.update(args.except("id", "metric_ids", "tag_names").slice("name", "dataset_id", "judge_model", "output_column", "expected_column"))
81
83
  run.replace_metrics!(args["metric_ids"]) if args.key?("metric_ids")
82
84
  run.update!(tag_names: args["tag_names"]) if args.key?("tag_names")
83
85
  text_result(run.reload.as_json)
@@ -126,7 +126,7 @@
126
126
  <div class="ck-api-endpoint">
127
127
  <p class="ck-api-method"><span class="ck-chip ck-chip--soft">POST</span> /api/v1/runs</p>
128
128
  <p class="ck-meta-copy">Create a new run.</p>
129
- <p class="ck-api-params"><strong>Optional:</strong>&ensp;<code>name</code>, <code>prompt_id</code>, <code>dataset_id</code>, <code>metric_ids</code>, <code>judge_model</code>, <code>output_column</code> (score existing outputs: omit <code>prompt_id</code> and grade a dataset column instead, default <code>actual_output</code>)</p>
129
+ <p class="ck-api-params"><strong>Optional:</strong>&ensp;<code>name</code>, <code>prompt_id</code>, <code>dataset_id</code>, <code>metric_ids</code>, <code>judge_model</code>, <code>output_column</code> (score existing outputs: omit <code>prompt_id</code> and grade a dataset column instead, default <code>actual_output</code>), <code>expected_column</code> (dataset column holding each row's answer key, given to the judge and to checks that compare against the row's expected value, default <code>expected_output</code>)</p>
130
130
  <%= render "completion_kit/api_reference/example", base_url: base_url, token: token, real_token: real_token, cmd: "curl -X POST #{base_url}/api/v1/runs \\\n -H \"Authorization: Bearer #{token}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"prompt_id\": 1, \"dataset_id\": 1, \"metric_ids\": [1, 2]}'" %>
131
131
  </div>
132
132
  <div class="ck-api-endpoint">
@@ -203,6 +203,7 @@
203
203
  <p class="ck-api-method"><span class="ck-chip ck-chip--soft">POST</span> /api/v1/datasets</p>
204
204
  <p class="ck-meta-copy">Create a dataset from inline CSV or an uploaded CSV file.</p>
205
205
  <p class="ck-api-params"><strong>Required:</strong>&ensp;<code>name</code>, and either <code>csv_data</code> (inline CSV) or a multipart <code>file</code> (CSV upload, preferred for large datasets)</p>
206
+ <p class="ck-meta-copy">First row is the header. Two column names are recognized specially: <code>expected_output</code> is each row's answer key (ground truth) given to the judge and to checks that compare against the row's expected value, and <code>actual_output</code> is a pre-made output to score in a prompt-less run. Both are overridable per run via <code>expected_column</code> and <code>output_column</code>.</p>
206
207
  <%= render "completion_kit/api_reference/example", base_url: base_url, token: token, real_token: real_token, cmd: "curl -X POST #{base_url}/api/v1/datasets \\\n -H \"Authorization: Bearer #{token}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"name\": \"tickets\", \"csv_data\": \"text,expected_output\\\\nHello,Hi\"}'" %>
207
208
  <%= render "completion_kit/api_reference/example", base_url: base_url, token: token, real_token: real_token, cmd: "curl -X POST #{base_url}/api/v1/datasets \\\n -H \"Authorization: Bearer #{token}\" \\\n -F \"name=tickets\" \\\n -F \"file=@tickets.csv\"" %>
208
209
  </div>
@@ -19,8 +19,13 @@
19
19
 
20
20
  <div class="ck-field">
21
21
  <%= form.label :csv_data, "CSV data", class: "ck-label" %>
22
- <%= form.text_area :csv_data, rows: 12, class: "ck-input ck-input--area ck-input--code", placeholder: "content,audience\nFirst ticket text,internal\nSecond ticket text,customer", **ck_field_aria(form, :csv_data) %>
22
+ <%= form.text_area :csv_data, rows: 12, class: "ck-input ck-input--area ck-input--code", placeholder: "content,audience\nFirst ticket text,internal\nSecond ticket text,customer", id: "dataset_csv_data", **ck_field_aria(form, :csv_data) %>
23
23
  <%= ck_field_error(form, :csv_data) %>
24
+ <p class="ck-field-hint">First row is the header. Two column names are recognized specially: <code>expected_output</code> is each row's answer key (ground truth) given to the judge and to checks that compare against the row's expected value, and <code>actual_output</code> is a pre-made output to score when you run without a prompt. Both are overridable per run. Every column is also available to the prompt as a variable.</p>
25
+ <div class="ck-header-preview" id="dataset-header-preview" hidden>
26
+ <span class="ck-header-preview__label">Detected columns</span>
27
+ <span id="dataset-header-chips"></span>
28
+ </div>
24
29
  </div>
25
30
 
26
31
  <%= render "completion_kit/tags/picker", record: dataset, param_namespace: :dataset %>
@@ -47,4 +52,32 @@
47
52
  <%= form.submit(dataset.persisted? ? "Save dataset" : "Create dataset", class: ck_button_classes(:dark)) %>
48
53
  </div>
49
54
  </div>
55
+
56
+ <script>
57
+ function ckDatasetHeaderPreview() {
58
+ var area = document.getElementById('dataset_csv_data');
59
+ var box = document.getElementById('dataset-header-preview');
60
+ var chips = document.getElementById('dataset-header-chips');
61
+ if (!area || !box || !chips) return;
62
+ var recognized = { expected_output: 'answer key', actual_output: 'pre-made output' };
63
+ var firstLine = (area.value.split(/\r?\n/).find(function(l) { return l.trim() !== ''; }) || '');
64
+ var headers = firstLine.split(',').map(function(h) { return h.trim().replace(/^"|"$/g, ''); }).filter(Boolean);
65
+ if (headers.length === 0) { box.hidden = true; chips.textContent = ''; return; }
66
+ box.hidden = false;
67
+ chips.textContent = '';
68
+ headers.forEach(function(h) {
69
+ var role = recognized[h];
70
+ var chip = document.createElement('span');
71
+ chip.className = 'ck-header-chip' + (role ? ' ck-header-chip--special' : '');
72
+ chip.textContent = role ? h + ' · ' + role : h;
73
+ chips.appendChild(chip);
74
+ });
75
+ }
76
+ document.addEventListener('turbo:load', ckDatasetHeaderPreview);
77
+ (function() {
78
+ var area = document.getElementById('dataset_csv_data');
79
+ if (area) area.addEventListener('input', ckDatasetHeaderPreview);
80
+ })();
81
+ ckDatasetHeaderPreview();
82
+ </script>
50
83
  <% end %>
@@ -75,6 +75,14 @@
75
75
  <p class="ck-field-hint" id="dataset-hint"></p>
76
76
  </div>
77
77
 
78
+ <div class="ck-field" id="expected-column-field">
79
+ <%= form.label :expected_column, "Answer-key column", class: "ck-label" %>
80
+ <%= form.text_field :expected_column, class: "ck-input", id: "run_expected_column", placeholder: "expected_output", **ck_field_aria(form, :expected_column) %>
81
+ <%= ck_field_error(form, :expected_column) %>
82
+ <p class="ck-field-hint" id="expected-column-hint">Optional. The dataset column holding each row's correct answer, used by the judge and by checks set to compare against the row's expected value. Defaults to <code>expected_output</code>.</p>
83
+ <p class="ck-field-hint" id="expected-column-warn"></p>
84
+ </div>
85
+
78
86
  <div class="ck-field">
79
87
  <label class="ck-label" for="run_temperature" style="position: relative;">
80
88
  Temperature<span class="ck-info-toggle" tabindex="0">?</span><span class="ck-info-popup">Controls how random the model's output is. Lower values are more focused and deterministic — the model picks the most likely words. Higher values are more varied and creative, with more risk of odd phrasing. Most LLMs default to 1.0; for evaluation, try a few values and see how your prompt holds up. Newer reasoning models (Claude Opus 4.7, GPT-5 family, etc.) ignore temperature entirely — CompletionKit detects this and re-sends without the parameter.</span>
@@ -152,7 +160,7 @@
152
160
 
153
161
  <div class="ck-metric-checkboxes" data-metric-checkboxes>
154
162
  <% @all_metrics.each do |metric| %>
155
- <label class="ck-checkbox-label" data-metric-tags="<%= metric.tag_names.join(",") %>">
163
+ <label class="ck-checkbox-label" data-metric-tags="<%= metric.tag_names.join(",") %>" data-compare-expected="<%= metric.check? && metric.check_config.to_h["compare_to"] == "expected" ? "1" : "0" %>">
156
164
  <%= check_box_tag "run[metric_ids][]", metric.id, run.metric_ids.include?(metric.id), class: "ck-checkbox", id: "run_metric_#{metric.id}" %>
157
165
  <span class="ck-checkbox-label__box" aria-hidden="true"></span>
158
166
  <span class="ck-checkbox-label__body">
@@ -235,6 +243,26 @@ function updateRunForm() {
235
243
  if (datasetHint) datasetHint.textContent = 'This prompt uses variables. Select a dataset to provide values.';
236
244
  }
237
245
 
246
+ var expectedWarn = document.getElementById('expected-column-warn');
247
+ var expectedField = document.getElementById('expected-column-field');
248
+ var expectedColEl = document.getElementById('run_expected_column');
249
+ if (expectedField) expectedField.className = 'ck-field';
250
+ if (expectedWarn) expectedWarn.textContent = '';
251
+ var wantsExpected = false;
252
+ document.querySelectorAll('input[name="run[metric_ids][]"]:checked').forEach(function(cb) {
253
+ var lbl = cb.closest('[data-compare-expected]');
254
+ if (lbl && lbl.dataset.compareExpected === '1') wantsExpected = true;
255
+ });
256
+ if (wantsExpected && dataset && datasetEl) {
257
+ var expOption = datasetEl.options[datasetEl.selectedIndex];
258
+ var expHeaders = (expOption && expOption.dataset.headers ? expOption.dataset.headers.split(/,\s*/) : []).filter(Boolean);
259
+ var expCol = ((expectedColEl && expectedColEl.value) || 'expected_output').trim();
260
+ if (expHeaders.indexOf(expCol) === -1) {
261
+ if (expectedField) expectedField.className = 'ck-field ck-field--error';
262
+ if (expectedWarn) expectedWarn.textContent = "A selected check grades against each row's expected value, but this dataset has no \"" + expCol + "\" column, so every row would fail. Add the column or point this at the right one.";
263
+ }
264
+ }
265
+
238
266
  var summary = document.getElementById('prompt-summary');
239
267
  if (summary) {
240
268
  if (selectedOption && selectedOption.value) {
@@ -0,0 +1,5 @@
1
+ class AddExpectedColumnToCompletionKitRuns < ActiveRecord::Migration[8.1]
2
+ def change
3
+ add_column :completion_kit_runs, :expected_column, :string
4
+ end
5
+ end
@@ -1,3 +1,3 @@
1
1
  module CompletionKit
2
- VERSION = "0.23.0"
2
+ VERSION = "0.24.0"
3
3
  end
@@ -15,6 +15,7 @@ module CompletionKit
15
15
  attr_accessor :allow_loopback_endpoints
16
16
  attr_accessor :judge_agreement_enabled
17
17
  attr_accessor :judge_examples_from_reviews
18
+ attr_accessor :on_run_created
18
19
 
19
20
  def initialize
20
21
  @openai_api_key = ENV['OPENAI_API_KEY']
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.23.0
4
+ version: 0.24.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Damien Bastin
@@ -161,14 +161,14 @@ dependencies:
161
161
  requirements:
162
162
  - - "~>"
163
163
  - !ruby/object:Gem::Version
164
- version: '6.0'
164
+ version: '8.0'
165
165
  type: :development
166
166
  prerelease: false
167
167
  version_requirements: !ruby/object:Gem::Requirement
168
168
  requirements:
169
169
  - - "~>"
170
170
  - !ruby/object:Gem::Version
171
- version: '6.0'
171
+ version: '8.0'
172
172
  - !ruby/object:Gem::Dependency
173
173
  name: factory_bot_rails
174
174
  requirement: !ruby/object:Gem::Requirement
@@ -475,6 +475,7 @@ files:
475
475
  - db/migrate/20260629000001_add_check_type_to_completion_kit_metrics.rb
476
476
  - db/migrate/20260629000002_add_check_type_to_completion_kit_metric_versions.rb
477
477
  - db/migrate/20260629000003_add_passed_to_completion_kit_reviews.rb
478
+ - db/migrate/20260706000001_add_expected_column_to_completion_kit_runs.rb
478
479
  - lib/completion-kit.rb
479
480
  - lib/completion_kit.rb
480
481
  - lib/completion_kit/concurrency_check.rb