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 +4 -4
- data/app/assets/stylesheets/completion_kit/application.css +32 -0
- data/app/controllers/completion_kit/api/v1/runs_controller.rb +2 -1
- data/app/controllers/completion_kit/runs_controller.rb +13 -5
- data/app/models/completion_kit/provider_credential.rb +1 -2
- data/app/models/completion_kit/response.rb +1 -1
- data/app/models/completion_kit/run.rb +56 -31
- data/app/services/completion_kit/mcp_tools/datasets.rb +2 -2
- data/app/services/completion_kit/mcp_tools/runs.rb +5 -3
- data/app/views/completion_kit/api_reference/_body.html.erb +2 -1
- data/app/views/completion_kit/datasets/_form.html.erb +34 -1
- data/app/views/completion_kit/runs/_form.html.erb +29 -1
- data/db/migrate/20260706000001_add_expected_column_to_completion_kit_runs.rb +5 -0
- data/lib/completion_kit/version.rb +1 -1
- data/lib/completion_kit.rb +1 -0
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9f8caf00fc4eac2a56d69561c61e292b983626da4c25e9e96c2c25983e7e717f
|
|
4
|
+
data.tar.gz: 1ed507e15ea8a6c1fb17eb0c05d1c0f883cb19b49e8450ac3b6802e002d81104
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
45
|
-
|
|
46
|
-
|
|
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).
|
|
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
|
|
@@ -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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: "
|
|
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> <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> <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> <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) {
|
data/lib/completion_kit.rb
CHANGED
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.
|
|
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: '
|
|
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: '
|
|
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
|