completion-kit 0.20.3 → 0.21.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: 71e8645d7a790e2afc35e4fb7c8ca4ec5a6d08f51f3f84a2c289a2b6c76a9051
4
- data.tar.gz: f5143766d938735ae5f2072516bc520c7d826894c02dc6a584bf62e40dd4b583
3
+ metadata.gz: 5db8eacad4c6487a87d970c4b942657ad5033b0647a846bdfba2a7c9548a54b0
4
+ data.tar.gz: 956456ed6c96d122283c6a437a91c062323153bee1e53ec32d10bae56bf2636c
5
5
  SHA512:
6
- metadata.gz: 0a6c71340315065125e6bc520f80453132de07d858acd4e5327d2cc35d3b3420c920a2f2ccf148c696c20afdad15371f6323d27d85dd7f3d9baa8dd03d3f5386
7
- data.tar.gz: 65c2978028e0897cb00e350f3a9e6af98f11e8d6fc6327da245acdbcc6de41bd783b8bbfe63b1cb76fc54e65debbd4488d71103f35d4222478c9611220d84a95
6
+ metadata.gz: e8b1f1e18e33a4875944bd779e107280b83acfc231cc4940b4f6fdcdd30d17d093c046fc0fb5836c2675c728681277006ccc54d9d0d72886ac09117d7c924c0d
7
+ data.tar.gz: c82ff9429c534d82ec3ca7b0acdcfa8ec1d50dfea091b4b04167a3dc65289b06bd3f4a5e2168654cbfb2a127fdd2fac69e77168847d899edd22b136e382e2560
@@ -210,6 +210,76 @@ document.addEventListener("click", function(e) {
210
210
  });
211
211
  });
212
212
 
213
+ var CK_CHECK_FIELDS = {
214
+ contains: ["value", "case_sensitive", "trim"],
215
+ not_contains: ["value", "case_sensitive", "trim"],
216
+ equals: ["value", "case_sensitive", "trim"],
217
+ regex: ["pattern", "case_sensitive", "multiline"],
218
+ valid_json: [],
219
+ json_path_equals: ["json_path", "expected"],
220
+ length_bounds: ["min", "max"],
221
+ no_refusal: []
222
+ };
223
+
224
+ function ckApplyCheckFields(scope) {
225
+ if (!scope) return;
226
+ var kindSelect = scope.querySelector('[name="metric[check_config][check_kind]"]');
227
+ if (!kindSelect) return;
228
+ var visible = CK_CHECK_FIELDS[kindSelect.value];
229
+ var targetSelect = scope.querySelector('[name="metric[check_config][target]"]');
230
+ var targetIsJsonPath = !!(targetSelect && targetSelect.value === "json_path");
231
+ scope.querySelectorAll("[data-ck-check-field]").forEach(function(field) {
232
+ var key = field.getAttribute("data-ck-check-field");
233
+ var show;
234
+ if (key === "target_path") {
235
+ show = targetIsJsonPath;
236
+ } else if (!visible) {
237
+ show = true;
238
+ } else {
239
+ show = visible.indexOf(key) !== -1;
240
+ }
241
+ field.hidden = !show;
242
+ });
243
+ }
244
+
245
+ function ckApplyMetricType(group) {
246
+ var checked = group.querySelector('input[type="radio"]:checked');
247
+ if (!checked) return;
248
+ var value = checked.value;
249
+ var scope = group.closest("form") || document;
250
+ scope.querySelectorAll("[data-ck-metric-editor]").forEach(function(editor) {
251
+ var active = editor.getAttribute("data-ck-metric-editor") === value;
252
+ editor.hidden = !active;
253
+ editor.querySelectorAll("input, select, textarea").forEach(function(field) {
254
+ field.disabled = !active;
255
+ });
256
+ });
257
+ ckApplyCheckFields(scope);
258
+ }
259
+
260
+ document.addEventListener("turbo:load", function() {
261
+ document.querySelectorAll("[data-ck-metric-type]").forEach(function(group) {
262
+ ckApplyMetricType(group);
263
+ });
264
+ document.querySelectorAll('[data-ck-metric-editor="check"]').forEach(function(editor) {
265
+ ckApplyCheckFields(editor);
266
+ });
267
+ });
268
+
269
+ document.addEventListener("change", function(e) {
270
+ var target = e.target;
271
+ if (!target || !target.closest) return;
272
+ var group = target.closest("[data-ck-metric-type]");
273
+ if (group && target.type === "radio") {
274
+ ckApplyMetricType(group);
275
+ return;
276
+ }
277
+ if (target.name === "metric[check_config][check_kind]" || target.name === "metric[check_config][target]") {
278
+ var scope = target.closest('[data-ck-metric-editor="check"]') || target.closest("form");
279
+ ckApplyCheckFields(scope);
280
+ }
281
+ });
282
+
213
283
  document.addEventListener("click", function(e) {
214
284
  var btn = e.target.closest("[data-ck-apply]");
215
285
  if (!btn) return;
@@ -226,3 +296,75 @@ document.addEventListener("click", function(e) {
226
296
  btn.textContent = "Applied ✓";
227
297
  field.focus({ preventScroll: true });
228
298
  });
299
+
300
+ function ckSelectClose(sel) {
301
+ if (!sel.classList.contains("is-open")) return;
302
+ sel.classList.remove("is-open");
303
+ var menu = sel.querySelector("[data-ck-select-menu]");
304
+ var trigger = sel.querySelector("[data-ck-select-trigger]");
305
+ if (menu) menu.hidden = true;
306
+ if (trigger) trigger.setAttribute("aria-expanded", "false");
307
+ }
308
+
309
+ function ckSelectOpen(sel) {
310
+ document.querySelectorAll("[data-ck-select].is-open").forEach(ckSelectClose);
311
+ sel.classList.add("is-open");
312
+ var menu = sel.querySelector("[data-ck-select-menu]");
313
+ var trigger = sel.querySelector("[data-ck-select-trigger]");
314
+ if (menu) menu.hidden = false;
315
+ if (trigger) trigger.setAttribute("aria-expanded", "true");
316
+ var current = menu.querySelector('[aria-selected="true"]') || menu.querySelector('[role="option"]');
317
+ if (current) current.focus();
318
+ }
319
+
320
+ function ckSelectChoose(sel, option) {
321
+ var input = sel.querySelector("[data-ck-select-value]");
322
+ var label = sel.querySelector("[data-ck-select-label]");
323
+ var text = option.querySelector("[data-ck-select-text]");
324
+ sel.querySelectorAll('[role="option"]').forEach(function(o) {
325
+ o.setAttribute("aria-selected", o === option ? "true" : "false");
326
+ });
327
+ input.value = option.getAttribute("data-value");
328
+ if (text) label.textContent = text.textContent;
329
+ input.dispatchEvent(new Event("change", { bubbles: true }));
330
+ ckSelectClose(sel);
331
+ var trigger = sel.querySelector("[data-ck-select-trigger]");
332
+ if (trigger) trigger.focus();
333
+ }
334
+
335
+ document.addEventListener("click", function(e) {
336
+ var trigger = e.target.closest("[data-ck-select-trigger]");
337
+ if (trigger) {
338
+ var sel = trigger.closest("[data-ck-select]");
339
+ if (sel.classList.contains("is-open")) { ckSelectClose(sel); } else { ckSelectOpen(sel); }
340
+ return;
341
+ }
342
+ var option = e.target.closest('[data-ck-select] [role="option"]');
343
+ if (option) {
344
+ ckSelectChoose(option.closest("[data-ck-select]"), option);
345
+ return;
346
+ }
347
+ document.querySelectorAll("[data-ck-select].is-open").forEach(function(sel) {
348
+ if (!sel.contains(e.target)) ckSelectClose(sel);
349
+ });
350
+ });
351
+
352
+ document.addEventListener("keydown", function(e) {
353
+ var sel = e.target.closest("[data-ck-select]");
354
+ if (!sel) return;
355
+ var trigger = sel.querySelector("[data-ck-select-trigger]");
356
+ var options = Array.prototype.slice.call(sel.querySelectorAll('[role="option"]'));
357
+ var open = sel.classList.contains("is-open");
358
+ if (e.target === trigger && !open) {
359
+ if (e.key === "ArrowDown" || e.key === "Enter" || e.key === " ") { e.preventDefault(); ckSelectOpen(sel); }
360
+ return;
361
+ }
362
+ if (!open) return;
363
+ var idx = options.indexOf(document.activeElement);
364
+ if (e.key === "ArrowDown") { e.preventDefault(); (options[idx + 1] || options[0]).focus(); }
365
+ else if (e.key === "ArrowUp") { e.preventDefault(); (options[idx - 1] || options[options.length - 1]).focus(); }
366
+ else if (e.key === "Home") { e.preventDefault(); options[0].focus(); }
367
+ else if (e.key === "End") { e.preventDefault(); options[options.length - 1].focus(); }
368
+ else if (e.key === "Enter" || e.key === " ") { e.preventDefault(); if (options[idx]) ckSelectChoose(sel, options[idx]); }
369
+ else if (e.key === "Escape") { e.preventDefault(); ckSelectClose(sel); trigger.focus(); }
370
+ });
@@ -1922,6 +1922,13 @@ label.ck-checkbox input {
1922
1922
  cursor: pointer;
1923
1923
  }
1924
1924
 
1925
+ .ck-radio-info {
1926
+ width: 16px;
1927
+ height: 16px;
1928
+ color: var(--ck-muted);
1929
+ cursor: help;
1930
+ }
1931
+
1925
1932
  .ck-field-row {
1926
1933
  display: flex;
1927
1934
  gap: 1rem;
@@ -1974,6 +1981,117 @@ label.ck-checkbox input {
1974
1981
  color: var(--ck-text);
1975
1982
  }
1976
1983
 
1984
+ .ck-select {
1985
+ position: relative;
1986
+ }
1987
+
1988
+ .ck-select__trigger {
1989
+ width: 100%;
1990
+ display: flex;
1991
+ align-items: center;
1992
+ justify-content: space-between;
1993
+ gap: 0.5rem;
1994
+ padding: 0.65rem 0.85rem;
1995
+ border: 1px solid var(--ck-line-strong);
1996
+ border-radius: var(--ck-radius);
1997
+ background: var(--ck-bg);
1998
+ color: var(--ck-text);
1999
+ font-family: var(--ck-mono);
2000
+ font-size: 0.9rem;
2001
+ text-align: left;
2002
+ cursor: pointer;
2003
+ transition: border-color 0.15s, box-shadow 0.15s;
2004
+ }
2005
+
2006
+ .ck-select__label {
2007
+ overflow: hidden;
2008
+ text-overflow: ellipsis;
2009
+ white-space: nowrap;
2010
+ }
2011
+
2012
+ .ck-select__trigger:focus-visible,
2013
+ .ck-select.is-open .ck-select__trigger {
2014
+ outline: none;
2015
+ border-color: var(--ck-accent);
2016
+ box-shadow: 0 0 0 3px var(--ck-accent-soft);
2017
+ }
2018
+
2019
+ .ck-select__chevron {
2020
+ display: inline-flex;
2021
+ flex-shrink: 0;
2022
+ color: var(--ck-muted);
2023
+ transition: transform 0.15s;
2024
+ }
2025
+
2026
+ .ck-select__chevron svg {
2027
+ width: 1rem;
2028
+ height: 1rem;
2029
+ }
2030
+
2031
+ .ck-select.is-open .ck-select__chevron {
2032
+ transform: rotate(180deg);
2033
+ }
2034
+
2035
+ .ck-select__menu {
2036
+ position: absolute;
2037
+ z-index: 30;
2038
+ left: 0;
2039
+ right: 0;
2040
+ top: calc(100% + 0.35rem);
2041
+ margin: 0;
2042
+ padding: 0.3rem;
2043
+ list-style: none;
2044
+ background: var(--ck-surface);
2045
+ border: 1px solid var(--ck-line-strong);
2046
+ border-radius: var(--ck-radius-lg);
2047
+ box-shadow: 0 16px 40px rgba(0, 0, 0, 0.45);
2048
+ max-height: 18rem;
2049
+ overflow-y: auto;
2050
+ font-family: var(--ck-mono);
2051
+ }
2052
+
2053
+ .ck-select__option {
2054
+ display: flex;
2055
+ align-items: center;
2056
+ gap: 0.5rem;
2057
+ padding: 0.5rem 0.6rem;
2058
+ border-radius: var(--ck-radius);
2059
+ color: var(--ck-text);
2060
+ font-size: 0.9rem;
2061
+ cursor: pointer;
2062
+ }
2063
+
2064
+ .ck-select__option:hover,
2065
+ .ck-select__option:focus-visible {
2066
+ outline: none;
2067
+ background: var(--ck-surface-hover);
2068
+ }
2069
+
2070
+ .ck-select__option[aria-selected="true"] {
2071
+ color: var(--ck-accent);
2072
+ }
2073
+
2074
+ .ck-select__tick {
2075
+ display: inline-flex;
2076
+ flex-shrink: 0;
2077
+ width: 1rem;
2078
+ color: var(--ck-accent);
2079
+ opacity: 0;
2080
+ }
2081
+
2082
+ .ck-select__tick svg {
2083
+ width: 1rem;
2084
+ height: 1rem;
2085
+ }
2086
+
2087
+ .ck-select__option[aria-selected="true"] .ck-select__tick {
2088
+ opacity: 1;
2089
+ }
2090
+
2091
+ .ck-select__text {
2092
+ flex: 1;
2093
+ }
2094
+
1977
2095
  .ck-section-title {
1978
2096
  font-size: 1.05rem;
1979
2097
  font-weight: 600;
@@ -3238,7 +3356,8 @@ select.ck-input {
3238
3356
  #ck-tab-metric-groups:checked ~ .ck-api-tabs__nav label[for="ck-tab-metric-groups"],
3239
3357
  #ck-tab-agreements:checked ~ .ck-api-tabs__nav label[for="ck-tab-agreements"],
3240
3358
  #ck-tab-tags:checked ~ .ck-api-tabs__nav label[for="ck-tab-tags"],
3241
- #ck-tab-providers:checked ~ .ck-api-tabs__nav label[for="ck-tab-providers"] {
3359
+ #ck-tab-providers:checked ~ .ck-api-tabs__nav label[for="ck-tab-providers"],
3360
+ #ck-tab-imports:checked ~ .ck-api-tabs__nav label[for="ck-tab-imports"] {
3242
3361
  color: var(--ck-accent);
3243
3362
  background: var(--ck-surface-soft);
3244
3363
  border-left-color: var(--ck-accent);
@@ -3253,7 +3372,8 @@ select.ck-input {
3253
3372
  #ck-tab-metric-groups:checked ~ .ck-api-tabs__panels .ck-api-tabs__panel:nth-child(7),
3254
3373
  #ck-tab-agreements:checked ~ .ck-api-tabs__panels .ck-api-tabs__panel:nth-child(8),
3255
3374
  #ck-tab-tags:checked ~ .ck-api-tabs__panels .ck-api-tabs__panel:nth-child(9),
3256
- #ck-tab-providers:checked ~ .ck-api-tabs__panels .ck-api-tabs__panel:nth-child(10) {
3375
+ #ck-tab-providers:checked ~ .ck-api-tabs__panels .ck-api-tabs__panel:nth-child(10),
3376
+ #ck-tab-imports:checked ~ .ck-api-tabs__panels .ck-api-tabs__panel:nth-child(11) {
3257
3377
  display: block;
3258
3378
  }
3259
3379
 
@@ -3295,7 +3415,8 @@ select.ck-input {
3295
3415
  #ck-tab-metric-groups:checked ~ .ck-api-tabs__nav label[for="ck-tab-metric-groups"],
3296
3416
  #ck-tab-agreements:checked ~ .ck-api-tabs__nav label[for="ck-tab-agreements"],
3297
3417
  #ck-tab-tags:checked ~ .ck-api-tabs__nav label[for="ck-tab-tags"],
3298
- #ck-tab-providers:checked ~ .ck-api-tabs__nav label[for="ck-tab-providers"] {
3418
+ #ck-tab-providers:checked ~ .ck-api-tabs__nav label[for="ck-tab-providers"],
3419
+ #ck-tab-imports:checked ~ .ck-api-tabs__nav label[for="ck-tab-imports"] {
3299
3420
  border-left-color: transparent;
3300
3421
  border-bottom-color: var(--ck-accent);
3301
3422
  }
@@ -68,7 +68,7 @@ module CompletionKit
68
68
  end
69
69
 
70
70
  def create
71
- @metric = Metric.new(metric_params)
71
+ @metric = Metric.new(create_metric_params)
72
72
 
73
73
  if @metric.save
74
74
  redirect_to metric_path(@metric), notice: "Metric was successfully created."
@@ -235,6 +235,15 @@ module CompletionKit
235
235
  @metric = Metric.find(params[:id])
236
236
  end
237
237
 
238
+ def create_metric_params
239
+ attrs = metric_params
240
+ if attrs[:metric_type] == "check"
241
+ attrs.except(:instruction, :rubric_bands)
242
+ else
243
+ attrs.except(:check_config)
244
+ end
245
+ end
246
+
238
247
  def metric_params
239
248
  permitted = params.require(:metric).permit(:name, :instruction, :metric_type,
240
249
  rubric_bands: [:stars, :description],
@@ -122,7 +122,7 @@ module CompletionKit
122
122
 
123
123
  def suggest
124
124
  if @run.prompt.nil?
125
- redirect_to run_path(@run), alert: "Judge-only runs don't have a prompt to improve."
125
+ redirect_to run_path(@run), alert: "A run that only scores existing outputs has no prompt to improve."
126
126
  return
127
127
  end
128
128
 
@@ -169,6 +169,48 @@ module CompletionKit
169
169
  end
170
170
  end
171
171
 
172
+ CHECK_KIND_LABELS = {
173
+ "contains" => "Contains a phrase",
174
+ "not_contains" => "Does not contain a phrase",
175
+ "equals" => "Equals exactly",
176
+ "regex" => "Matches a pattern",
177
+ "valid_json" => "Is valid JSON",
178
+ "json_path_equals" => "A JSON field equals a value",
179
+ "length_bounds" => "Length is within a range",
180
+ "no_refusal" => "Is not a refusal"
181
+ }.freeze
182
+
183
+ CHECK_TARGET_LABELS = {
184
+ "response_text" => "The response text",
185
+ "input_data" => "The input row",
186
+ "json_path" => "A value from the response JSON"
187
+ }.freeze
188
+
189
+ CHECK_FIELD_LABELS = {
190
+ "value" => "Text to look for",
191
+ "pattern" => "Pattern",
192
+ "json_path" => "JSON path",
193
+ "expected" => "Expected value",
194
+ "target_path" => "Path into the JSON",
195
+ "min" => "Shortest allowed",
196
+ "max" => "Longest allowed",
197
+ "case_sensitive" => "Case sensitive",
198
+ "multiline" => "Multiline",
199
+ "trim" => "Trim whitespace"
200
+ }.freeze
201
+
202
+ def ck_check_kind_label(kind)
203
+ CHECK_KIND_LABELS.fetch(kind.to_s) { kind.to_s.humanize }
204
+ end
205
+
206
+ def ck_check_target_label(target)
207
+ CHECK_TARGET_LABELS.fetch(target.to_s) { target.to_s.humanize }
208
+ end
209
+
210
+ def ck_check_field_label(key)
211
+ CHECK_FIELD_LABELS.fetch(key.to_s) { key.to_s.humanize }
212
+ end
213
+
172
214
  def ck_result_change_badge(change)
173
215
  case change
174
216
  when "broke"
@@ -30,7 +30,7 @@ module CompletionKit
30
30
  display_scoped.select(:id)
31
31
  end
32
32
 
33
- # A judge-only run grades a pre-existing column on the dataset instead of
33
+ # A scoring-only run grades a pre-existing column on the dataset instead of
34
34
  # generating new outputs. No prompt is attached; the response text is read
35
35
  # from row[output_column]; no LLM generation happens.
36
36
  def judge_only?
@@ -442,7 +442,7 @@ module CompletionKit
442
442
  self.name = "#{prompt.name} — v#{prompt.version_number} ##{count}"
443
443
  elsif dataset.present?
444
444
  count = Run.where(prompt_id: nil, dataset_id: dataset.id).count + 1
445
- self.name = "#{dataset.name} judge-only ##{count}"
445
+ self.name = "#{dataset.name} scoring ##{count}"
446
446
  end
447
447
  end
448
448
 
@@ -461,7 +461,7 @@ module CompletionKit
461
461
  return if prompt.present?
462
462
 
463
463
  if dataset.nil?
464
- errors.add(:dataset_id, "is required for a judge-only run (no prompt)")
464
+ errors.add(:dataset_id, "is required when scoring existing outputs (no prompt)")
465
465
  return
466
466
  end
467
467
 
@@ -5,7 +5,7 @@ module CompletionKit
5
5
 
6
6
  TOOLS = {
7
7
  "judges_replay" => {
8
- description: "Run the current judge against a dataset (judge-only run). Wraps runs_create with prompt_id omitted and output_column supplied. Re-judges existing dataset outputs so you can compare against human verdicts.",
8
+ description: "Run the current judge against a dataset (scores existing outputs). Wraps runs_create with prompt_id omitted and output_column supplied. Re-judges existing dataset outputs so you can compare against human verdicts.",
9
9
  inputSchema: {
10
10
  type: "object",
11
11
  properties: {
@@ -51,7 +51,7 @@ module CompletionKit
51
51
  handler: :publish
52
52
  },
53
53
  "prompts_suggest_improvement" => {
54
- description: "Suggest an improved version of a prompt, grounded in a run's test results and judge feedback. Analyzes the run's responses, scores, and reviews, then returns reasoning plus a rewritten template (preserving {{variables}}) and persists it as a Suggestion. Requires a run that has a prompt (not a judge-only run).",
54
+ description: "Suggest an improved version of a prompt, grounded in a run's test results and judge feedback. Analyzes the run's responses, scores, and reviews, then returns reasoning plus a rewritten template (preserving {{variables}}) and persists it as a Suggestion. Requires a run that has a prompt (not a scoring-only run).",
55
55
  inputSchema: {
56
56
  type: "object",
57
57
  properties: {run_id: {type: "integer", description: "The run whose results ground the improvement."}},
@@ -107,7 +107,7 @@ module CompletionKit
107
107
 
108
108
  def self.suggest_improvement(args)
109
109
  run = Run.find(args["run_id"])
110
- return error_result("Judge-only runs don't have a prompt to improve.") if run.prompt.nil?
110
+ return error_result("A run that only scores existing outputs has no prompt to improve.") if run.prompt.nil?
111
111
 
112
112
  result = PromptImprovementService.new(run).suggest
113
113
  return error_result("The model didn't return a usable rewrite.") if result["suggested_template"].blank?
@@ -15,7 +15,7 @@ module CompletionKit
15
15
  handler: :get
16
16
  },
17
17
  "runs_create" => {
18
- description: "Create a run. Omit prompt_id and provide output_column for a judge-only run that grades a pre-existing dataset column instead of generating new outputs.",
18
+ description: "Create a run. Omit prompt_id and provide output_column to score existing outputs by grading a pre-existing dataset column instead of generating new ones.",
19
19
  inputSchema: {
20
20
  type: "object",
21
21
  properties: {
@@ -20,6 +20,7 @@
20
20
  <input type="radio" name="ck-api-tab" id="ck-tab-agreements" class="ck-api-tabs__radio">
21
21
  <input type="radio" name="ck-api-tab" id="ck-tab-tags" class="ck-api-tabs__radio">
22
22
  <input type="radio" name="ck-api-tab" id="ck-tab-providers" class="ck-api-tabs__radio">
23
+ <input type="radio" name="ck-api-tab" id="ck-tab-imports" class="ck-api-tabs__radio">
23
24
 
24
25
  <nav class="ck-api-tabs__nav">
25
26
  <label for="ck-tab-mcp" class="ck-api-tabs__label">MCP <span class="ck-api-tabs__count"><%= CompletionKit::McpDispatcher.tool_definitions.size %></span></label>
@@ -32,6 +33,7 @@
32
33
  <label for="ck-tab-agreements" class="ck-api-tabs__label">Agreements <span class="ck-api-tabs__count">3</span></label>
33
34
  <label for="ck-tab-tags" class="ck-api-tabs__label">Tags <span class="ck-api-tabs__count">5</span></label>
34
35
  <label for="ck-tab-providers" class="ck-api-tabs__label">Providers <span class="ck-api-tabs__count">5</span></label>
36
+ <label for="ck-tab-imports" class="ck-api-tabs__label">Imports <span class="ck-api-tabs__count">1</span></label>
35
37
  </nav>
36
38
 
37
39
  <div class="ck-api-tabs__panels">
@@ -124,7 +126,7 @@
124
126
  <div class="ck-api-endpoint">
125
127
  <p class="ck-api-method"><span class="ck-chip ck-chip--soft">POST</span> /api/v1/runs</p>
126
128
  <p class="ck-meta-copy">Create a new run.</p>
127
- <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> (judge-only: 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>)</p>
128
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]}'" %>
129
131
  </div>
130
132
  <div class="ck-api-endpoint">
@@ -379,6 +381,18 @@
379
381
  } %>
380
382
  </div>
381
383
 
384
+ <div class="ck-api-tabs__panel">
385
+ <h2 class="ck-section-title">Imports</h2>
386
+ <p class="ck-copy">Bring an existing <a href="https://www.promptfoo.dev" class="ck-link">promptfoo</a> config into CompletionKit in one call. Prompts, the test dataset, assert-based metrics, and providers are created where they map cleanly and skipped with a reason where they don't.</p>
387
+ <div class="ck-api-endpoint">
388
+ <p class="ck-api-method"><span class="ck-chip ck-chip--soft">POST</span> /api/v1/imports/promptfoo</p>
389
+ <p class="ck-meta-copy">Import a promptfooconfig.yaml. Send the YAML as a <code>config</code> param, or POST the raw YAML as the request body. Returns 201 with a mapping summary, or 422 if the YAML cannot be parsed.</p>
390
+ <p class="ck-api-params"><strong>Request:</strong>&ensp;<code>config</code> (the YAML text) or a raw YAML request body</p>
391
+ <p class="ck-api-params"><strong>Response 201:</strong>&ensp;<code>prompts</code>, <code>dataset</code>, <code>metrics</code>, and <code>providers</code>, each listing what was <code>created</code> and what was <code>skipped</code> (with a reason)</p>
392
+ <%= render "completion_kit/api_reference/example", base_url: base_url, token: token, real_token: real_token, cmd: "curl -X POST #{base_url}/api/v1/imports/promptfoo \\\n -H \"Authorization: Bearer #{token}\" \\\n -H \"Content-Type: application/x-yaml\" \\\n --data-binary @promptfooconfig.yaml" %>
393
+ </div>
394
+ </div>
395
+
382
396
  </div>
383
397
  </div>
384
398
 
@@ -1,16 +1,16 @@
1
1
  <% config = local_assigns.fetch(:config) %>
2
2
  <dl class="ck-check-spec">
3
3
  <div class="ck-check-spec__row">
4
- <dt class="ck-check-spec__term">Kind</dt>
5
- <dd class="ck-check-spec__val"><code class="ck-check-spec__code"><%= config["check_kind"] %></code></dd>
4
+ <dt class="ck-check-spec__term">Rule</dt>
5
+ <dd class="ck-check-spec__val"><%= ck_check_kind_label(config["check_kind"]) %></dd>
6
6
  </div>
7
7
  <div class="ck-check-spec__row">
8
- <dt class="ck-check-spec__term">Target</dt>
9
- <dd class="ck-check-spec__val"><code class="ck-check-spec__code"><%= config["target"] %></code></dd>
8
+ <dt class="ck-check-spec__term">Checks</dt>
9
+ <dd class="ck-check-spec__val"><%= ck_check_target_label(config["target"]) %></dd>
10
10
  </div>
11
11
  <% config.except("check_kind", "target").each do |key, value| %>
12
12
  <div class="ck-check-spec__row">
13
- <dt class="ck-check-spec__term"><%= key.humanize %></dt>
13
+ <dt class="ck-check-spec__term"><%= ck_check_field_label(key) %></dt>
14
14
  <dd class="ck-check-spec__val"><code class="ck-check-spec__code"><%= value %></code></dd>
15
15
  </div>
16
16
  <% end %>
@@ -75,20 +75,22 @@
75
75
  <% else %>
76
76
  <div class="ck-field" data-ck-metric-type>
77
77
  <p class="ck-section-title">Metric type</p>
78
- <p class="ck-hint">An LLM judge scores each output on a 1-5 rubric. A deterministic check passes or fails with no model call.</p>
78
+ <p class="ck-hint">The judge gives each response 1 to 5 stars against your rubric. A check just passes or fails, with no AI.</p>
79
79
  <label class="ck-radio">
80
80
  <%= form.radio_button :metric_type, "llm_judge", checked: !metric.check? %>
81
81
  <span>LLM judge (1-5)</span>
82
+ <%= heroicon_tag "information-circle", variant: :outline, size: 16, class: "ck-radio-info", "aria-hidden": "true", title: "An AI reads each response and rates it 1 to 5 stars against your rubric, with a written reason. Best for subjective quality: tone, helpfulness, accuracy." %>
82
83
  </label>
83
84
  <label class="ck-radio">
84
85
  <%= form.radio_button :metric_type, "check", checked: metric.check? %>
85
86
  <span>Deterministic check</span>
87
+ <%= heroicon_tag "information-circle", variant: :outline, size: 16, class: "ck-radio-info", "aria-hidden": "true", title: "A rule that passes or fails instantly with no AI and no cost. Best for exact things: valid JSON, contains a phrase, no refusal." %>
86
88
  </label>
87
89
  </div>
88
90
  <% end %>
89
91
 
90
92
  <% if show_judge %>
91
- <div class="ck-field ck-field--spacious" data-ck-metric-editor="llm_judge">
93
+ <div class="ck-field ck-field--spacious" data-ck-metric-editor="llm_judge" <%= "hidden" if metric.check? %>>
92
94
  <p class="ck-section-title">Instruction</p>
93
95
  <p class="ck-hint">What should the judge assess? This instruction is sent to the LLM judge when scoring outputs.</p>
94
96
  <%= form.text_area :instruction, rows: 8, class: "ck-input ck-input--area", placeholder: "Evaluate whether the output...", **ck_field_aria(form, :instruction) %>
@@ -112,7 +114,7 @@
112
114
  <% end %>
113
115
  </div>
114
116
 
115
- <div class="ck-field ck-field--spacious">
117
+ <div class="ck-field ck-field--spacious" data-ck-metric-editor="llm_judge" <%= "hidden" if metric.check? %>>
116
118
  <p class="ck-section-title">Rubric<%= render "completion_kit/metrics/rubric_hint" %></p>
117
119
  <p class="ck-hint">What each star rating means for this metric.</p>
118
120
 
@@ -155,78 +157,77 @@
155
157
 
156
158
  <% if show_check %>
157
159
  <% check = metric.check_config || {} %>
158
- <div class="ck-field ck-field--spacious" data-ck-metric-editor="check">
160
+ <div class="ck-field ck-field--spacious" data-ck-metric-editor="check" <%= "hidden" unless metric.check? %>>
159
161
  <p class="ck-section-title">Check</p>
160
- <p class="ck-hint">A deterministic pass/fail rule. Fill only the fields the chosen kind needs.</p>
162
+ <p class="ck-hint">A pass or fail rule with no AI and no cost. You only fill in the fields the chosen rule needs.</p>
161
163
 
162
164
  <div class="ck-field">
163
- <label class="ck-label" for="metric_check_kind">Check kind</label>
164
- <select name="metric[check_config][check_kind]" id="metric_check_kind" class="ck-input">
165
- <% CompletionKit::Checks::Registry.kinds.each do |kind| %>
166
- <option value="<%= kind %>"<%= " selected" if check["check_kind"] == kind %>><%= kind %></option>
167
- <% end %>
168
- </select>
165
+ <p class="ck-label" id="metric_check_kind_label">Rule</p>
166
+ <%= render "completion_kit/shared/branded_select",
167
+ name: "metric[check_config][check_kind]",
168
+ options: CompletionKit::Checks::Registry.kinds.map { |kind| [kind, ck_check_kind_label(kind)] },
169
+ selected: check["check_kind"] || CompletionKit::Checks::Registry.kinds.first,
170
+ labelledby: "metric_check_kind_label" %>
169
171
  </div>
170
172
 
171
173
  <div class="ck-field">
172
- <label class="ck-label" for="metric_check_target">Target</label>
173
- <select name="metric[check_config][target]" id="metric_check_target" class="ck-input">
174
- <% CompletionKit::Checks::TargetResolver::TARGETS.each do |target| %>
175
- <option value="<%= target %>"<%= " selected" if check["target"] == target %>><%= target %></option>
176
- <% end %>
177
- </select>
174
+ <p class="ck-label" id="metric_check_target_label">What to check</p>
175
+ <%= render "completion_kit/shared/branded_select",
176
+ name: "metric[check_config][target]",
177
+ options: CompletionKit::Checks::TargetResolver::TARGETS.map { |target| [target, ck_check_target_label(target)] },
178
+ selected: check["target"] || CompletionKit::Checks::TargetResolver::TARGETS.first,
179
+ labelledby: "metric_check_target_label" %>
178
180
  </div>
179
181
 
180
- <div class="ck-field">
181
- <label class="ck-label" for="metric_check_target_path">Target path</label>
182
- <p class="ck-hint">Used when target is json_path, e.g. data.items.0.name.</p>
182
+ <div class="ck-field" data-ck-check-field="target_path">
183
+ <label class="ck-label" for="metric_check_target_path">Path into the JSON</label>
184
+ <p class="ck-hint">Where to look inside the response, like data.items.0.name.</p>
183
185
  <input type="text" name="metric[check_config][target_path]" id="metric_check_target_path" class="ck-input" value="<%= check["target_path"] %>">
184
186
  </div>
185
187
 
186
- <div class="ck-field">
187
- <label class="ck-label" for="metric_check_value">Value</label>
188
- <p class="ck-hint">The substring or exact string for contains, not_contains, or equals.</p>
188
+ <div class="ck-field" data-ck-check-field="value">
189
+ <label class="ck-label" for="metric_check_value">Text to look for</label>
189
190
  <input type="text" name="metric[check_config][value]" id="metric_check_value" class="ck-input" value="<%= check["value"] %>">
190
191
  </div>
191
192
 
192
- <div class="ck-field">
193
+ <div class="ck-field" data-ck-check-field="pattern">
193
194
  <label class="ck-label" for="metric_check_pattern">Pattern</label>
194
- <p class="ck-hint">A regular expression for the regex kind.</p>
195
+ <p class="ck-hint">A regular expression to match against.</p>
195
196
  <input type="text" name="metric[check_config][pattern]" id="metric_check_pattern" class="ck-input" value="<%= check["pattern"] %>">
196
197
  </div>
197
198
 
198
- <div class="ck-field">
199
+ <div class="ck-field" data-ck-check-field="json_path">
199
200
  <label class="ck-label" for="metric_check_json_path">JSON path</label>
200
- <p class="ck-hint">Dotted path into parsed JSON for json_path_equals.</p>
201
+ <p class="ck-hint">The path to the value inside the response, like data.status.</p>
201
202
  <input type="text" name="metric[check_config][json_path]" id="metric_check_json_path" class="ck-input" value="<%= check["json_path"] %>">
202
203
  </div>
203
204
 
204
- <div class="ck-field">
205
- <label class="ck-label" for="metric_check_expected">Expected</label>
206
- <p class="ck-hint">The value the JSON path must equal.</p>
205
+ <div class="ck-field" data-ck-check-field="expected">
206
+ <label class="ck-label" for="metric_check_expected">Expected value</label>
207
+ <p class="ck-hint">The value it should equal.</p>
207
208
  <input type="text" name="metric[check_config][expected]" id="metric_check_expected" class="ck-input" value="<%= check["expected"] %>">
208
209
  </div>
209
210
 
210
211
  <div class="ck-field-row">
211
- <div class="ck-field">
212
- <label class="ck-label" for="metric_check_min">Min length</label>
212
+ <div class="ck-field" data-ck-check-field="min">
213
+ <label class="ck-label" for="metric_check_min">Shortest allowed</label>
213
214
  <input type="number" name="metric[check_config][min]" id="metric_check_min" class="ck-input" value="<%= check["min"] %>">
214
215
  </div>
215
- <div class="ck-field">
216
- <label class="ck-label" for="metric_check_max">Max length</label>
216
+ <div class="ck-field" data-ck-check-field="max">
217
+ <label class="ck-label" for="metric_check_max">Longest allowed</label>
217
218
  <input type="number" name="metric[check_config][max]" id="metric_check_max" class="ck-input" value="<%= check["max"] %>">
218
219
  </div>
219
220
  </div>
220
221
 
221
- <label class="ck-checkbox">
222
+ <label class="ck-checkbox" data-ck-check-field="case_sensitive">
222
223
  <input type="checkbox" name="metric[check_config][case_sensitive]" value="true"<%= " checked" if check["case_sensitive"] %>>
223
224
  <span>Case sensitive</span>
224
225
  </label>
225
- <label class="ck-checkbox">
226
+ <label class="ck-checkbox" data-ck-check-field="multiline">
226
227
  <input type="checkbox" name="metric[check_config][multiline]" value="true"<%= " checked" if check["multiline"] %>>
227
228
  <span>Multiline</span>
228
229
  </label>
229
- <label class="ck-checkbox">
230
+ <label class="ck-checkbox" data-ck-check-field="trim">
230
231
  <input type="checkbox" name="metric[check_config][trim]" value="true"<%= " checked" if check["trim"] %>>
231
232
  <span>Trim whitespace</span>
232
233
  </label>
@@ -22,7 +22,7 @@
22
22
  <%= check_box_tag "run[judge_only]", "1", run.persisted? && run.judge_only?, id: "run_judge_only", class: "ck-checkbox" %>
23
23
  <span class="ck-checkbox-label__box" aria-hidden="true"></span>
24
24
  <span class="ck-checkbox-label__body">
25
- <span class="ck-checkbox-label__text">Judge-only run</span>
25
+ <span class="ck-checkbox-label__text">Score existing outputs</span>
26
26
  <span class="ck-checkbox-label__hint">Grade an existing column on the dataset instead of running a prompt. Roughly half the LLM calls per row.</span>
27
27
  </span>
28
28
  </label>
@@ -263,7 +263,7 @@ function updateRunForm() {
263
263
  }
264
264
  } else if (!dataset) {
265
265
  if (datasetField) datasetField.className = 'ck-field ck-field--info';
266
- if (datasetHint) datasetHint.textContent = 'Judge-only runs need a dataset that supplies the output column.';
266
+ if (datasetHint) datasetHint.textContent = 'Skip generation and score responses you already have from a dataset column. Works with rubric metrics or deterministic checks.';
267
267
  }
268
268
  } else {
269
269
  valid = prompt !== '';
@@ -10,7 +10,7 @@
10
10
  <%= link_to run.prompt.name, ck_prompt_path(run.prompt), class: "ck-runs-table__config-link", onclick: "event.stopPropagation();" %>
11
11
  <span class="ck-runs-table__version">v<%= run.prompt.version_number %></span>
12
12
  <% else %>
13
- <span class="ck-runs-table__version">Judge-only</span>
13
+ <span class="ck-runs-table__version">Scoring only</span>
14
14
  <% end %>
15
15
  <% if run.dataset %>
16
16
  <span class="ck-runs-table__sep">·</span>
@@ -22,7 +22,7 @@
22
22
  <% if run.prompt %>
23
23
  <p class="ck-meta-copy"><%= link_to run.prompt.display_name, prompt_path(run.prompt), class: "ck-link" %>&ensp;<span class="ck-chip" style="text-transform: none;"><%= run.prompt.llm_model %></span></p>
24
24
  <% else %>
25
- <p class="ck-meta-copy">Judge-only run grading column <code><%= run.output_column.presence || "actual_output" %></code><% if run.dataset %> on <%= link_to run.dataset.name, dataset_path(run.dataset), class: "ck-link" %><% end %></p>
25
+ <p class="ck-meta-copy">Scoring existing outputs, grading column <code><%= run.output_column.presence || "actual_output" %></code><% if run.dataset %> on <%= link_to run.dataset.name, dataset_path(run.dataset), class: "ck-link" %><% end %></p>
26
26
  <% end %>
27
27
  </div>
28
28
  <%= render "completion_kit/runs/actions", run: run %>
@@ -0,0 +1,18 @@
1
+ <% chosen = options.find { |value, _label| value.to_s == selected.to_s } || options.first %>
2
+ <div class="ck-select" data-ck-select>
3
+ <input type="hidden" name="<%= name %>" value="<%= chosen && chosen.first %>" data-ck-select-value>
4
+ <button type="button" class="ck-select__trigger" data-ck-select-trigger
5
+ aria-haspopup="listbox" aria-expanded="false" aria-labelledby="<%= labelledby %>">
6
+ <span class="ck-select__label" data-ck-select-label><%= chosen && chosen.last %></span>
7
+ <span class="ck-select__chevron" aria-hidden="true"><%= heroicon_tag "chevron-down" %></span>
8
+ </button>
9
+ <ul class="ck-select__menu" role="listbox" hidden data-ck-select-menu aria-labelledby="<%= labelledby %>">
10
+ <% options.each do |value, label| %>
11
+ <li class="ck-select__option" role="option" tabindex="-1" data-value="<%= value %>"
12
+ aria-selected="<%= chosen && chosen.first.to_s == value.to_s ? "true" : "false" %>">
13
+ <span class="ck-select__tick" aria-hidden="true"><%= heroicon_tag "check" %></span>
14
+ <span class="ck-select__text" data-ck-select-text><%= label %></span>
15
+ </li>
16
+ <% end %>
17
+ </ul>
18
+ </div>
@@ -1,3 +1,3 @@
1
1
  module CompletionKit
2
- VERSION = "0.20.3"
2
+ VERSION = "0.21.0"
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.20.3
4
+ version: 0.21.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Damien Bastin
@@ -419,6 +419,7 @@ files:
419
419
  - app/views/completion_kit/runs/index.html.erb
420
420
  - app/views/completion_kit/runs/new.html.erb
421
421
  - app/views/completion_kit/runs/show.html.erb
422
+ - app/views/completion_kit/shared/_branded_select.html.erb
422
423
  - app/views/completion_kit/shared/_settings_nav.html.erb
423
424
  - app/views/completion_kit/suggestions/_scoreboard.html.erb
424
425
  - app/views/completion_kit/suggestions/_state.html.erb