completion-kit 0.28.43 → 0.28.45

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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +31 -6
  3. data/app/assets/stylesheets/completion_kit/application.css +5 -0
  4. data/app/controllers/completion_kit/agreements_controller.rb +1 -5
  5. data/app/controllers/completion_kit/api/v1/base_controller.rb +5 -0
  6. data/app/controllers/completion_kit/api/v1/metric_groups_controller.rb +6 -4
  7. data/app/controllers/completion_kit/api/v1/metrics_controller.rb +1 -1
  8. data/app/controllers/completion_kit/api/v1/runs_controller.rb +9 -5
  9. data/app/controllers/completion_kit/application_controller.rb +13 -0
  10. data/app/controllers/completion_kit/mcp_controller.rb +16 -4
  11. data/app/controllers/completion_kit/metric_groups_controller.rb +6 -4
  12. data/app/controllers/completion_kit/provider_credentials_controller.rb +19 -1
  13. data/app/controllers/completion_kit/runs_controller.rb +13 -8
  14. data/app/jobs/completion_kit/generate_row_job.rb +2 -1
  15. data/app/jobs/completion_kit/judge_review_job.rb +1 -0
  16. data/app/jobs/completion_kit/metric_suggestion_job.rb +1 -2
  17. data/app/jobs/completion_kit/model_discovery_job.rb +28 -13
  18. data/app/models/completion_kit/metric_group.rb +4 -2
  19. data/app/models/completion_kit/metric_group_membership.rb +1 -1
  20. data/app/models/completion_kit/prompt.rb +7 -6
  21. data/app/models/completion_kit/provider_credential.rb +19 -2
  22. data/app/models/completion_kit/run.rb +77 -14
  23. data/app/models/completion_kit/run_metric.rb +1 -1
  24. data/app/models/concerns/completion_kit/metric_selection.rb +35 -0
  25. data/app/services/completion_kit/api_config.rb +23 -7
  26. data/app/services/completion_kit/azure_foundry_client.rb +4 -2
  27. data/app/services/completion_kit/csv_processor.rb +4 -6
  28. data/app/services/completion_kit/json_object.rb +14 -0
  29. data/app/services/completion_kit/llm_client.rb +1 -1
  30. data/app/services/completion_kit/mcp_dispatcher.rb +2 -2
  31. data/app/services/completion_kit/mcp_tools/base.rb +2 -0
  32. data/app/services/completion_kit/mcp_tools/datasets.rb +1 -1
  33. data/app/services/completion_kit/mcp_tools/judges.rb +2 -2
  34. data/app/services/completion_kit/mcp_tools/metric_groups.rb +2 -4
  35. data/app/services/completion_kit/mcp_tools/provider_credentials.rb +3 -2
  36. data/app/services/completion_kit/mcp_tools/runs.rb +6 -6
  37. data/app/services/completion_kit/model_discovery_service.rb +10 -10
  38. data/app/services/completion_kit/ollama_client.rb +4 -2
  39. data/app/services/completion_kit/promptfoo_importer.rb +1 -1
  40. data/app/services/completion_kit/provider_endpoint.rb +18 -9
  41. data/app/views/completion_kit/agreements/_buttons.html.erb +1 -1
  42. data/app/views/completion_kit/agreements/_trust_panel.html.erb +1 -2
  43. data/app/views/completion_kit/api_reference/_body.html.erb +4 -3
  44. data/app/views/completion_kit/dashboard/show.html.erb +1 -1
  45. data/app/views/completion_kit/datasets/show.html.erb +5 -0
  46. data/app/views/completion_kit/metric_groups/_form.html.erb +4 -1
  47. data/app/views/completion_kit/prompts/show.html.erb +5 -0
  48. data/app/views/completion_kit/provider_credentials/_form.html.erb +24 -1
  49. data/app/views/completion_kit/provider_credentials/edit.html.erb +1 -1
  50. data/app/views/completion_kit/provider_credentials/index.html.erb +1 -2
  51. data/app/views/completion_kit/responses/show.html.erb +1 -1
  52. data/app/views/completion_kit/runs/_form.html.erb +1 -1
  53. data/app/views/completion_kit/runs/index.html.erb +1 -1
  54. data/config/locales/en.yml +12 -0
  55. data/lib/completion_kit/errors.rb +30 -0
  56. data/lib/completion_kit/version.rb +1 -1
  57. data/lib/completion_kit.rb +1 -1
  58. data/lib/generators/completion_kit/templates/initializer.rb +9 -0
  59. data/lib/tasks/completion_kit_runs.rake +2 -2
  60. metadata +4 -2
  61. data/app/models/completion_kit/mcp_session.rb +0 -54
@@ -3,8 +3,7 @@
3
3
  <% anchor = metric&.name&.parameterize %>
4
4
  <% current_metric_version = metric && CompletionKit::MetricVersion.current.find_by(metric_id: metric.id) %>
5
5
  <% target_response = if (stats.sample_size.zero? || stats.counter_only?) && metric && current_metric_version
6
- created_by = CompletionKit.config.username.presence || "operator"
7
- verdicted_ids = CompletionKit::Agreement.where(metric_id: metric.id, created_by: created_by, metric_version_id: current_metric_version.id, run_id: CompletionKit::Run.visible_run_ids).pluck(:response_id)
6
+ verdicted_ids = CompletionKit::Agreement.where(metric_id: metric.id, created_by: ck_current_reviewer, metric_version_id: current_metric_version.id, run_id: CompletionKit::Run.visible_run_ids).pluck(:response_id)
8
7
  CompletionKit::Response.where(run_id: CompletionKit::Run.visible_run_ids).joins(:reviews)
9
8
  .where(reviews: { metric_id: metric.id, metric_version_id: current_metric_version.id })
10
9
  .where.not(reviews: { ai_score: nil })
@@ -145,7 +145,7 @@
145
145
  } %>
146
146
  <div class="ck-api-endpoint">
147
147
  <p class="ck-api-method"><span class="ck-chip ck-chip--soft">POST</span> /api/v1/runs/:id/generate</p>
148
- <p class="ck-meta-copy">Start generating responses. Returns 202 Accepted. Poll the run to check progress.</p>
148
+ <p class="ck-meta-copy">Start generating responses. Returns 202 Accepted. Poll the run to check progress. Returns 422 with the reason when the run cannot start, for example when no configured provider serves the prompt's model or the judge model.</p>
149
149
  <%= 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/1/generate \\\n -H \"Authorization: Bearer #{token}\"" %>
150
150
  </div>
151
151
  <div class="ck-api-endpoint">
@@ -161,7 +161,7 @@
161
161
  </div>
162
162
  <div class="ck-api-endpoint">
163
163
  <p class="ck-api-method"><span class="ck-chip ck-chip--soft">POST</span> /api/v1/runs/:id/regrade</p>
164
- <p class="ck-meta-copy">Re-judge the existing successful responses against the current metric versions without regenerating model output. Returns 202 Accepted, or 422 if no responses are eligible.</p>
164
+ <p class="ck-meta-copy">Re-judge the existing successful responses against the current metric versions without regenerating model output. Returns 202 Accepted, or 422 if no responses are eligible or no configured provider serves the judge model.</p>
165
165
  <%= 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/1/regrade \\\n -H \"Authorization: Bearer #{token}\"" %>
166
166
  </div>
167
167
  <div class="ck-api-endpoint">
@@ -258,7 +258,7 @@
258
258
  </div>
259
259
  <div class="ck-api-endpoint">
260
260
  <p class="ck-api-method"><span class="ck-chip ck-chip--soft">POST</span> /api/v1/metrics/:id/suggest_variants</p>
261
- <p class="ck-meta-copy">Generate draft metric versions from the current disagreements. Only rubric-style (llm_judge) metrics can suggest variants. Returns 201 with the new draft versions, or 422 if the metric is a check (checks are exact and have no variants to suggest), if no disagreements exist, or if the model produced nothing usable.</p>
261
+ <p class="ck-meta-copy">Generate draft metric versions from the current disagreements. Only rubric-style (llm_judge) metrics can suggest variants. Returns 201 with the new draft versions, or 422 if the metric is a check (checks are exact and have no variants to suggest), if no disagreements exist, if no configured provider serves the model, or if the model produced nothing usable.</p>
262
262
  <p class="ck-api-params"><strong>Optional:</strong>&ensp;<code>count</code>, <code>model</code></p>
263
263
  <%= render "completion_kit/api_reference/example", base_url: base_url, token: token, real_token: real_token, cmd: "curl -X POST #{base_url}/api/v1/metrics/1/suggest_variants \\\n -H \"Authorization: Bearer #{token}\"" %>
264
264
  </div>
@@ -395,6 +395,7 @@
395
395
  <div class="ck-api-endpoint">
396
396
  <p class="ck-api-method"><span class="ck-chip ck-chip--soft">GET</span>&ensp;<span class="ck-chip ck-chip--soft">PATCH</span>&ensp;<span class="ck-chip" style="color: var(--ck-danger);">DELETE</span> /api/v1/provider_credentials/:id</p>
397
397
  <p class="ck-meta-copy">Get, update, or delete a provider credential. DELETE returns 422 (not 204) when the provider is still in use by prompts or judge runs; remove those references first.</p>
398
+ <p class="ck-meta-copy">Changing <code>provider</code> or <code>api_endpoint</code> on a credential with a stored key requires <code>api_key</code> in the same request, otherwise PATCH returns 422 and nothing changes. Send <code>"api_key": null</code> to remove the key instead.</p>
398
399
  <%= render "completion_kit/api_reference/example", base_url: base_url, token: token, real_token: real_token, cmd: "curl -X PATCH #{base_url}/api/v1/provider_credentials/1 \\\n -H \"Authorization: Bearer #{token}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"api_key\": \"sk-new...\"}'" %>
399
400
  </div>
400
401
  <%= render "completion_kit/api_reference/resource_list", title: "Your providers",
@@ -130,7 +130,7 @@
130
130
  <% if @recent_runs.any? %>
131
131
  <%= render "completion_kit/runs/table", runs: @recent_runs %>
132
132
  <% elsif CompletionKit::Run.exists? %>
133
- <div class="ck-empty">Your older runs are hidden.</div>
133
+ <div class="ck-empty">No runs to show.</div>
134
134
  <% else %>
135
135
  <div class="ck-empty">No runs yet.&ensp;<%= link_to "Create your first run →", new_run_path, class: "ck-link" %></div>
136
136
  <% end %>
@@ -71,6 +71,11 @@
71
71
  <p class="ck-kicker">Runs</p>
72
72
  <%= render "completion_kit/runs/table", runs: @runs %>
73
73
  </section>
74
+ <% elsif @dataset.runs.exists? %>
75
+ <section class="ck-card--spaced">
76
+ <p class="ck-kicker">Runs</p>
77
+ <div class="ck-empty">No runs to show.</div>
78
+ </section>
74
79
  <% end %>
75
80
 
76
81
  <%= ck_runs_display_footer(@runs) %>
@@ -45,7 +45,7 @@
45
45
  <div class="ck-metric-checkboxes" data-metric-checkboxes role="group" aria-labelledby="mg_metrics_label">
46
46
  <% @metrics.each do |metric| %>
47
47
  <label class="ck-checkbox-label" data-metric-tags="<%= metric.tag_names.join(",") %>">
48
- <%= check_box_tag "metric_group[metric_ids][]", metric.id, metric_group.metrics.exists?(metric.id), class: "ck-checkbox", id: "metric_group_metric_#{metric.id}" %>
48
+ <%= check_box_tag "metric_group[metric_ids][]", metric.id, metric_group.selected_metric_ids.include?(metric.id), class: "ck-checkbox", id: "metric_group_metric_#{metric.id}" %>
49
49
  <span class="ck-checkbox-label__box" aria-hidden="true"></span>
50
50
  <span class="ck-checkbox-label__body">
51
51
  <span class="ck-checkbox-label__text"><%= metric.name %></span>
@@ -61,6 +61,9 @@
61
61
  </label>
62
62
  <% end %>
63
63
  </div>
64
+ <% if metric_group.errors[:metric_ids].any? %>
65
+ <p class="ck-field-error" id="<%= ck_field_error_id(form, :metric_ids) %>"><%= metric_group.errors.full_messages_for(:metric_ids).to_sentence %></p>
66
+ <% end %>
64
67
  <%= hidden_field_tag "metric_group[metric_ids][]", "" %>
65
68
  </div>
66
69
 
@@ -162,6 +162,11 @@
162
162
  <p class="ck-kicker">Runs</p>
163
163
  <%= render "completion_kit/runs/table", runs: @runs %>
164
164
  </section>
165
+ <% elsif CompletionKit::Run.where(prompt_id: @prompt.family_versions.select(:id)).exists? %>
166
+ <section class="ck-card--spaced">
167
+ <p class="ck-kicker">Runs</p>
168
+ <div class="ck-empty">No runs to show.</div>
169
+ </section>
165
170
  <% end %>
166
171
 
167
172
  <%= ck_runs_display_footer(@runs) %>
@@ -22,10 +22,33 @@
22
22
 
23
23
  <div class="ck-field">
24
24
  <%= form.label :api_key, "API key", class: "ck-label" %>
25
- <%= form.text_area :api_key, rows: 3, class: "ck-input ck-input--area ck-input--code", placeholder: "Paste the provider API key", **ck_field_aria(form, :api_key) %>
25
+ <%= form.text_area :api_key, value: (provider_credential.persisted? ? "" : provider_credential.api_key), rows: 3, class: "ck-input ck-input--area ck-input--code", placeholder: "Paste the provider API key", **ck_field_aria(form, :api_key) %>
26
26
  <%= ck_field_error(form, :api_key) %>
27
+ <% if provider_credential.api_key_stored? %>
28
+ <p class="ck-field-hint">
29
+ <% if provider_credential.api_key_ending %>
30
+ Leave blank to keep the current key ending in <code><%= provider_credential.api_key_ending %></code>.
31
+ <% else %>
32
+ Leave blank to keep the current key.
33
+ <% end %>
34
+ Enter it again if you change the provider or endpoint.
35
+ </p>
36
+ <% end %>
27
37
  </div>
28
38
 
39
+ <% if provider_credential.api_key_stored? %>
40
+ <div class="ck-field">
41
+ <label class="ck-checkbox-label">
42
+ <%= check_box_tag "provider_credential[remove_api_key]", "1", local_assigns[:remove_api_key], id: "provider_credential_remove_api_key", class: "ck-checkbox" %>
43
+ <span class="ck-checkbox-label__box" aria-hidden="true"></span>
44
+ <span class="ck-checkbox-label__body">
45
+ <span class="ck-checkbox-label__text">Remove the stored key</span>
46
+ <span class="ck-checkbox-label__hint">For endpoints that run without a key, such as a local Ollama server.</span>
47
+ </span>
48
+ </label>
49
+ </div>
50
+ <% end %>
51
+
29
52
  <div class="ck-field">
30
53
  <%= form.label :api_endpoint, "API endpoint", class: "ck-label" %>
31
54
  <%= form.text_field :api_endpoint, class: "ck-input", placeholder: "Needed for Ollama, Azure AI Foundry, or custom OpenAI-compatible endpoints", **ck_field_aria(form, :api_endpoint) %>
@@ -10,4 +10,4 @@
10
10
  </div>
11
11
  </section>
12
12
 
13
- <%= render "form", provider_credential: @provider_credential %>
13
+ <%= render "form", provider_credential: @provider_credential, remove_api_key: @remove_api_key %>
@@ -19,11 +19,10 @@
19
19
  <% default_endpoints = {"openai" => "https://api.openai.com", "anthropic" => "https://api.anthropic.com"} %>
20
20
  <div class="ck-provider-list" data-ck-statuses-url="<%= statuses_provider_credentials_path %>">
21
21
  <% @provider_credentials.each do |provider_credential| %>
22
- <% masked_key = provider_credential.api_key.present? ? "#{provider_credential.api_key[0..6]}#{'•' * 12}#{provider_credential.api_key[-4..]}" : "—" %>
23
22
  <a href="<%= edit_provider_credential_path(provider_credential) %>" class="ck-provider-card">
24
23
  <div class="ck-provider-card__header">
25
24
  <span class="ck-provider-card__name"><%= provider_credential.display_provider %></span>
26
- <span class="ck-provider-card__key"><%= masked_key %></span>
25
+ <span class="ck-provider-card__key"><%= provider_credential.api_key_stored? ? "#{'•' * 12}#{provider_credential.api_key_ending}" : "No key" %></span>
27
26
  </div>
28
27
 
29
28
  <div class="ck-provider-card__meta">
@@ -139,7 +139,7 @@
139
139
  <% if CompletionKit.config.judge_agreement_enabled && review.metric && review.ai_score && review.metric_version&.llm_judge? %>
140
140
  <% existing = CompletionKit::Agreement.find_by(
141
141
  response_id: @response.id, metric_id: review.metric_id,
142
- created_by: CompletionKit.config.username.presence || "operator"
142
+ created_by: ck_current_reviewer
143
143
  ) %>
144
144
  <%= render "completion_kit/agreements/buttons",
145
145
  review: review, agreement: existing, run: @run,
@@ -160,7 +160,7 @@
160
160
  <div class="ck-metric-checkboxes" data-metric-checkboxes role="group" aria-labelledby="run_metrics_label">
161
161
  <% @all_metrics.each do |metric| %>
162
162
  <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" %>">
163
- <%= check_box_tag "run[metric_ids][]", metric.id, run.metric_ids.include?(metric.id), class: "ck-checkbox", id: "run_metric_#{metric.id}" %>
163
+ <%= check_box_tag "run[metric_ids][]", metric.id, run.selected_metric_ids.include?(metric.id), class: "ck-checkbox", id: "run_metric_#{metric.id}" %>
164
164
  <span class="ck-checkbox-label__box" aria-hidden="true"></span>
165
165
  <span class="ck-checkbox-label__body">
166
166
  <span class="ck-checkbox-label__text"><%= metric.name %></span>
@@ -21,7 +21,7 @@
21
21
  <p>No runs match these tags. <%= link_to "Clear filters", runs_path, class: "ck-link" %>.</p>
22
22
  </div>
23
23
  <% elsif CompletionKit::Run.exists? %>
24
- <div class="ck-empty">Your older runs are hidden.</div>
24
+ <div class="ck-empty">No runs to show.</div>
25
25
  <% else %>
26
26
  <div class="ck-empty">No runs yet.&ensp;<%= link_to "Create your first run →", new_run_path, class: "ck-link" %></div>
27
27
  <% end %>
@@ -0,0 +1,12 @@
1
+ en:
2
+ activerecord:
3
+ attributes:
4
+ completion_kit/prompt:
5
+ llm_model: Model
6
+ completion_kit/run:
7
+ judge_model: Judge model
8
+ metric_ids: Metrics
9
+ completion_kit/metric_group:
10
+ metric_ids: Metrics
11
+ completion_kit/provider_credential:
12
+ api_key: API key
@@ -1,8 +1,38 @@
1
+ require "faraday"
2
+ require "json"
3
+
1
4
  module CompletionKit
2
5
  class Error < StandardError; end
3
6
 
4
7
  class ConfigurationError < Error; end
5
8
 
9
+ class UnknownModelError < ConfigurationError
10
+ attr_reader :model_name
11
+
12
+ def initialize(model_name = nil)
13
+ @model_name = model_name
14
+ super(%(No configured provider serves model "#{model_name}"))
15
+ end
16
+ end
17
+
18
+ class EndpointError < Error
19
+ CONNECTION_MESSAGE = "Could not connect to the endpoint."
20
+ TIMEOUT_MESSAGE = "The endpoint did not respond in time."
21
+ NOT_JSON_MESSAGE = "The endpoint did not return JSON."
22
+
23
+ def self.from(error)
24
+ message = case error
25
+ when Faraday::TimeoutError then TIMEOUT_MESSAGE
26
+ when Faraday::Error then CONNECTION_MESSAGE
27
+ when JSON::ParserError then NOT_JSON_MESSAGE
28
+ end
29
+ return error unless message
30
+
31
+ Rails.logger.warn("[CompletionKit] endpoint request failed: #{error.class}: #{error.message}")
32
+ new(message)
33
+ end
34
+ end
35
+
6
36
  class RateLimitError < Error
7
37
  attr_reader :provider, :status, :retry_after
8
38
 
@@ -1,3 +1,3 @@
1
1
  module CompletionKit
2
- VERSION = "0.28.43"
2
+ VERSION = "0.28.45"
3
3
  end
@@ -7,7 +7,7 @@ module CompletionKit
7
7
  class Configuration
8
8
  attr_accessor :openai_api_key, :anthropic_api_key, :ollama_api_key, :ollama_api_endpoint
9
9
  attr_accessor :judge_model, :high_quality_threshold, :medium_quality_threshold
10
- attr_accessor :username, :password, :auth_strategy, :api_token
10
+ attr_accessor :username, :password, :auth_strategy, :api_token, :current_reviewer
11
11
  attr_accessor :tenant_scope, :tenant_scope_columns
12
12
  attr_accessor :api_reference_authentication_partial
13
13
  attr_accessor :runs_display_scope, :runs_display_footer_partial
@@ -45,6 +45,15 @@ CompletionKit.configure do |config|
45
45
  # config.username = "admin"
46
46
  # config.password = ENV['COMPLETION_KIT_PASSWORD']
47
47
 
48
+ # Reviewer name for verdicts recorded in the web UI. The callable receives the
49
+ # controller. Each reviewer keeps one verdict per response and metric, so return
50
+ # a String unique to the signed-in person. Blank or nil falls back to username,
51
+ # then "operator". Any other return type raises CompletionKit::ConfigurationError.
52
+ # config.current_reviewer = ->(controller) { controller.current_user&.email }
53
+ # Behind a proxy that sets X-Remote-User. Only use this when a trusted proxy
54
+ # always overwrites the header, or any client can claim any name:
55
+ # config.current_reviewer = ->(controller) { controller.request.headers["X-Remote-User"] }
56
+
48
57
  # Rate limiting (requests per minute, per IP)
49
58
  # The REST API and MCP endpoint share api_rate_limit; the web UI uses
50
59
  # web_rate_limit. Both are enforced through Rails.cache.
@@ -1,7 +1,7 @@
1
1
  namespace :completion_kit do
2
- desc "Mark in-flight runs as failed (for use after the queue adapter cutover)"
2
+ desc "Mark in-flight runs in every tenant as failed (for use after the queue adapter cutover)"
3
3
  task mark_interrupted_runs_failed: :environment do
4
- scope = CompletionKit::Run.where(status: "running")
4
+ scope = CompletionKit::Run.unscoped.where(status: "running")
5
5
  count = scope.count
6
6
  scope.update_all(
7
7
  status: "failed",
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: completion-kit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.28.43
4
+ version: 0.28.45
5
5
  platform: ruby
6
6
  authors:
7
7
  - Damien Bastin
@@ -238,7 +238,6 @@ files:
238
238
  - app/models/completion_kit/application_record.rb
239
239
  - app/models/completion_kit/dashboard_dismissal.rb
240
240
  - app/models/completion_kit/dataset.rb
241
- - app/models/completion_kit/mcp_session.rb
242
241
  - app/models/completion_kit/metric.rb
243
242
  - app/models/completion_kit/metric_group.rb
244
243
  - app/models/completion_kit/metric_group_membership.rb
@@ -256,6 +255,7 @@ files:
256
255
  - app/models/completion_kit/tag.rb
257
256
  - app/models/completion_kit/tagging.rb
258
257
  - app/models/concerns/completion_kit/has_job_status.rb
258
+ - app/models/concerns/completion_kit/metric_selection.rb
259
259
  - app/models/concerns/completion_kit/taggable.rb
260
260
  - app/services/completion_kit/agreement_math.rb
261
261
  - app/services/completion_kit/anthropic_client.rb
@@ -274,6 +274,7 @@ files:
274
274
  - app/services/completion_kit/checks/valid_json.rb
275
275
  - app/services/completion_kit/csv_processor.rb
276
276
  - app/services/completion_kit/dashboard_stats.rb
277
+ - app/services/completion_kit/json_object.rb
277
278
  - app/services/completion_kit/judge_service.rb
278
279
  - app/services/completion_kit/llm_client.rb
279
280
  - app/services/completion_kit/mcp_dispatcher.rb
@@ -398,6 +399,7 @@ files:
398
399
  - app/views/completion_kit/tags/index.html.erb
399
400
  - app/views/completion_kit/tags/new.html.erb
400
401
  - app/views/layouts/completion_kit/application.html.erb
402
+ - config/locales/en.yml
401
403
  - config/routes.rb
402
404
  - db/migrate/20260311000001_create_completion_kit_tables.rb
403
405
  - db/migrate/20260326000001_rename_criteria_to_instruction_on_metrics_and_reviews.rb
@@ -1,54 +0,0 @@
1
- module CompletionKit
2
- # MCP session marker — one row per active client session, kept in the
3
- # database so sessions survive Puma restarts, deploys, and Rails.cache
4
- # eviction.
5
- #
6
- # Two things to know about why every query goes through `unscoped`:
7
- #
8
- # 1. CompletionKit::ApplicationRecord applies the host app's tenant_scope as
9
- # a default_scope. MCP sessions are per-CONNECTION, not per-tenant — the
10
- # table has no organization_id column. Letting the tenant_scope into a
11
- # session lookup turns "is this session live?" into either a SQL error
12
- # (no such column) or a false negative (`WHERE 1=0`), which surfaces to
13
- # the client as a spurious "Session not initialized" right after init.
14
- # 2. `active?` slides expires_at forward when a session is more than halfway
15
- # through its TTL, so an MCP connection that keeps making calls stays
16
- # alive instead of expiring on the original 1-hour wall clock.
17
- #
18
- # Expired rows are opportunistically pruned on every new session start, so
19
- # the table stays bounded by recent activity.
20
- class McpSession < ApplicationRecord
21
- self.table_name = "completion_kit_mcp_sessions"
22
-
23
- SESSION_TTL = 1.hour
24
-
25
- def self.start!
26
- prune_expired!
27
- unscoped.create!(session_id: SecureRandom.uuid, expires_at: SESSION_TTL.from_now).session_id
28
- end
29
-
30
- def self.active?(session_id)
31
- return false if session_id.blank?
32
-
33
- row = unscoped.where(session_id: session_id).where("expires_at > ?", Time.current).first
34
- return false unless row
35
-
36
- slide_expiry(row)
37
- true
38
- end
39
-
40
- def self.destroy_session(session_id)
41
- unscoped.where(session_id: session_id).delete_all
42
- end
43
-
44
- def self.prune_expired!
45
- unscoped.where("expires_at < ?", Time.current).delete_all
46
- end
47
-
48
- def self.slide_expiry(row)
49
- half_ttl_from_now = (SESSION_TTL / 2).from_now
50
- return if row.expires_at > half_ttl_from_now
51
- row.update_column(:expires_at, SESSION_TTL.from_now)
52
- end
53
- end
54
- end