completion-kit 0.28.44 → 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.
- checksums.yaml +4 -4
- data/README.md +31 -6
- data/app/assets/stylesheets/completion_kit/application.css +5 -0
- data/app/controllers/completion_kit/agreements_controller.rb +1 -5
- data/app/controllers/completion_kit/api/v1/base_controller.rb +5 -0
- data/app/controllers/completion_kit/api/v1/metric_groups_controller.rb +6 -4
- data/app/controllers/completion_kit/api/v1/metrics_controller.rb +1 -1
- data/app/controllers/completion_kit/api/v1/runs_controller.rb +9 -5
- data/app/controllers/completion_kit/application_controller.rb +13 -0
- data/app/controllers/completion_kit/mcp_controller.rb +12 -4
- data/app/controllers/completion_kit/metric_groups_controller.rb +6 -4
- data/app/controllers/completion_kit/provider_credentials_controller.rb +19 -1
- data/app/controllers/completion_kit/runs_controller.rb +13 -8
- data/app/jobs/completion_kit/generate_row_job.rb +2 -1
- data/app/jobs/completion_kit/judge_review_job.rb +1 -0
- data/app/jobs/completion_kit/metric_suggestion_job.rb +1 -2
- data/app/jobs/completion_kit/model_discovery_job.rb +28 -13
- data/app/models/completion_kit/metric_group.rb +4 -2
- data/app/models/completion_kit/metric_group_membership.rb +1 -1
- data/app/models/completion_kit/prompt.rb +7 -6
- data/app/models/completion_kit/provider_credential.rb +19 -2
- data/app/models/completion_kit/run.rb +77 -14
- data/app/models/completion_kit/run_metric.rb +1 -1
- data/app/models/concerns/completion_kit/metric_selection.rb +35 -0
- data/app/services/completion_kit/api_config.rb +23 -7
- data/app/services/completion_kit/azure_foundry_client.rb +4 -2
- data/app/services/completion_kit/csv_processor.rb +4 -6
- data/app/services/completion_kit/json_object.rb +14 -0
- data/app/services/completion_kit/llm_client.rb +1 -1
- data/app/services/completion_kit/mcp_dispatcher.rb +1 -1
- data/app/services/completion_kit/mcp_tools/base.rb +2 -0
- data/app/services/completion_kit/mcp_tools/datasets.rb +1 -1
- data/app/services/completion_kit/mcp_tools/judges.rb +2 -2
- data/app/services/completion_kit/mcp_tools/metric_groups.rb +2 -4
- data/app/services/completion_kit/mcp_tools/provider_credentials.rb +3 -2
- data/app/services/completion_kit/mcp_tools/runs.rb +6 -6
- data/app/services/completion_kit/model_discovery_service.rb +10 -10
- data/app/services/completion_kit/ollama_client.rb +4 -2
- data/app/services/completion_kit/promptfoo_importer.rb +1 -1
- data/app/services/completion_kit/provider_endpoint.rb +18 -9
- data/app/views/completion_kit/agreements/_buttons.html.erb +1 -1
- data/app/views/completion_kit/agreements/_trust_panel.html.erb +1 -2
- data/app/views/completion_kit/api_reference/_body.html.erb +4 -3
- data/app/views/completion_kit/dashboard/show.html.erb +1 -1
- data/app/views/completion_kit/datasets/show.html.erb +5 -0
- data/app/views/completion_kit/metric_groups/_form.html.erb +4 -1
- data/app/views/completion_kit/prompts/show.html.erb +5 -0
- data/app/views/completion_kit/provider_credentials/_form.html.erb +24 -1
- data/app/views/completion_kit/provider_credentials/edit.html.erb +1 -1
- data/app/views/completion_kit/provider_credentials/index.html.erb +1 -2
- data/app/views/completion_kit/responses/show.html.erb +1 -1
- data/app/views/completion_kit/runs/_form.html.erb +1 -1
- data/app/views/completion_kit/runs/index.html.erb +1 -1
- data/config/locales/en.yml +12 -0
- data/lib/completion_kit/errors.rb +30 -0
- data/lib/completion_kit/version.rb +1 -1
- data/lib/completion_kit.rb +1 -1
- data/lib/generators/completion_kit/templates/initializer.rb +9 -0
- data/lib/tasks/completion_kit_runs.rake +2 -2
- metadata +4 -2
- data/app/models/completion_kit/mcp_session.rb +0 -54
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7c6a9a433f596def84c665f6b6fbe2b5aa4ed1422ac923f20a2a2d246aad02c4
|
|
4
|
+
data.tar.gz: 3a0ad110587d51bad3b41f89439da3efa79c199887d58b76703b0d46d16d7f6d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5f17ea2b059df5b6e2656c9a80a60d87ccd5a55b7d1d2a2599ca3b913227eca49d19a983015765b52accc0287a8e07e60dedb3e1d92d2744e81f5803a32f5b19
|
|
7
|
+
data.tar.gz: 673b44c3f1eb7edaf963acb85a0451067ed1f6fa9ee73fee156518c031e8fb2b74786aa119c88d39281cc36e4a5e56da3ab8354a3c0411fe1eca800ed96b275e
|
data/README.md
CHANGED
|
@@ -133,6 +133,29 @@ end
|
|
|
133
133
|
|
|
134
134
|
Only one mode can be active.
|
|
135
135
|
|
|
136
|
+
### Reviewer name
|
|
137
|
+
|
|
138
|
+
When someone records a verdict on a judge's score in the web UI, CompletionKit saves it under a reviewer name. Each reviewer keeps one verdict per response and metric, so give every person their own name. Otherwise their verdicts overwrite each other. Set `current_reviewer` to a callable that receives the controller and returns the signed-in person's name:
|
|
139
|
+
|
|
140
|
+
```ruby
|
|
141
|
+
CompletionKit.configure do |c|
|
|
142
|
+
c.auth_strategy = ->(controller) { controller.authenticate_user! }
|
|
143
|
+
c.current_reviewer = ->(controller) { controller.current_user&.email }
|
|
144
|
+
end
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The callable must return a String or nil. Any other value, such as a user record, raises `CompletionKit::ConfigurationError`. If `current_reviewer` is unset or returns a blank value, verdicts are saved under `username`, or `"operator"` when no username is set. The web UI does not read the `X-Remote-User` header on its own. If a proxy in front of your app sets that header, pass it through:
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
CompletionKit.configure do |c|
|
|
151
|
+
c.current_reviewer = ->(controller) { controller.request.headers["X-Remote-User"] }
|
|
152
|
+
end
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Only use this header when a trusted proxy always overwrites it. Otherwise any client can send it and claim any reviewer name.
|
|
156
|
+
|
|
157
|
+
The REST API and the MCP server take the reviewer name from their own `created_by` parameter.
|
|
158
|
+
|
|
136
159
|
## Rate limiting
|
|
137
160
|
|
|
138
161
|
The REST API, the MCP endpoint, and the web UI are rate limited per IP, per minute. The defaults are generous; tune them in the initializer:
|
|
@@ -269,22 +292,24 @@ CompletionKit.configure do |config|
|
|
|
269
292
|
end
|
|
270
293
|
```
|
|
271
294
|
|
|
272
|
-
`tenant_scope` runs as each engine model's `default_scope` (use `unscoped` to bypass). `tenant_scope_columns` is appended to every engine uniqueness validation. Adding the tenant columns and composite unique indexes lives in your host migrations. Both defaults (`nil`, `[]`) are no-ops.
|
|
295
|
+
`tenant_scope` runs as each engine model's `default_scope` (use `unscoped` to bypass). `tenant_scope_columns` is appended to every engine uniqueness validation. Adding the tenant columns and composite unique indexes lives in your host migrations. Both defaults (`nil`, `[]`) are no-ops. The `completion_kit:mark_interrupted_runs_failed` rake task bypasses `tenant_scope` and marks in-flight runs in every tenant, so a host that also enforces tenancy in the database, for example with row-level security, must run it as a database role that can see every tenant's rows.
|
|
296
|
+
|
|
297
|
+
Custom model endpoints and CSV imports by URL may reach the server's own loopback interface by default, so a local Ollama works out of the box. If the people using the engine should not reach services on the app server itself, set `config.allow_loopback_endpoints = false`. Private, link-local and reserved addresses are always refused.
|
|
273
298
|
|
|
274
|
-
One hook lets a host apply
|
|
299
|
+
One hook lets a host apply its own display rule everywhere the engine lists, counts, or traverses runs, without overriding controllers or views:
|
|
275
300
|
|
|
276
301
|
```ruby
|
|
277
302
|
CompletionKit.configure do |config|
|
|
278
303
|
config.runs_display_scope = -> { where(created_at: 90.days.ago..) }
|
|
279
|
-
config.runs_display_footer_partial = "runs/
|
|
304
|
+
config.runs_display_footer_partial = "runs/display_notice"
|
|
280
305
|
end
|
|
281
306
|
```
|
|
282
307
|
|
|
283
|
-
`runs_display_scope` is a callable evaluated against a `Run` relation, in the same bare-`where` style as `tenant_scope` (it runs via `instance_exec`, so write it zero-arg with the relation as `self`, like a Rails `scope` lambda). It must return a relation: a callable that returns `nil` or anything non-chainable raises when a list renders. The engine applies it through `Run.display_scoped` at every run list and count it owns (the runs index, prompt and dataset show pages, the compare picker, new-run tag defaults, the v1 API index and its `X-Total-Count`, the MCP `runs_list` tool, the dashboard and API reference recent-runs panels, the dashboard "Runs" stat-card count, and provider-credential usage stats) and through `Run.visible_run_ids` for child records that traverse runs (the metric trust-panel sample and the agreement examples shown on a metric page). Use it for list-only
|
|
308
|
+
`runs_display_scope` is a callable evaluated against a `Run` relation, in the same bare-`where` style as `tenant_scope` (it runs via `instance_exec`, so write it zero-arg with the relation as `self`, like a Rails `scope` lambda). It must return a relation: a callable that returns `nil` or anything non-chainable raises when a list renders. The engine applies it through `Run.display_scoped` at every run list and count it owns (the runs index, prompt and dataset show pages, the compare picker, new-run tag defaults, the v1 API index and its `X-Total-Count`, the MCP `runs_list` tool, the dashboard and API reference recent-runs panels, the dashboard "Runs" stat-card count, and provider-credential usage stats) and through `Run.visible_run_ids` for child records that traverse runs (the metric trust-panel sample and the agreement examples shown on a metric page). Use it for a list-only display rule rather than a global `default_scope`, which would null `Run` associations everywhere they are traversed.
|
|
284
309
|
|
|
285
|
-
Deliberately exempt, because they must still see every run: id-addressed single-run lookups (`runs#show`, the MCP `runs_get` tool, the v1 API show), delete-confirmation cascade counts, the auto-generated run-name counter, the dashboard's stat-card gate (it keys off the unscoped run total so a
|
|
310
|
+
Deliberately exempt, because they must still see every run: id-addressed single-run lookups (`runs#show`, the MCP `runs_get` tool, the v1 API show), delete-confirmation cascade counts, the auto-generated run-name counter, the dashboard's stat-card gate (it keys off the unscoped run total so a workspace whose runs the display scope excludes entirely still sees its activity cards rather than a blank dashboard), and the judge few-shot seeding that learns from corrected examples even on runs the display scope excludes.
|
|
286
311
|
|
|
287
|
-
`runs_display_footer_partial` names a partial rendered below the runs list on the dashboard, the index, and the prompt and dataset show pages; it receives the shown runs as a `runs` local.
|
|
312
|
+
`runs_display_footer_partial` names a partial rendered below the runs list on the dashboard, the index, and the prompt and dataset show pages; it receives the shown runs as a `runs` local. When the display scope excludes every run one of those surfaces would list, the surface says "No runs to show." and the footer partial renders beneath it. The engine cannot know why your scope excludes runs, so use the partial to explain your display rule: your host owns that rule in `runs_display_scope` and is the only side that can describe or count what it left out. Both default to `nil` (no-ops), leaving standalone behaviour unchanged.
|
|
288
313
|
|
|
289
314
|
## Contributing
|
|
290
315
|
|
|
@@ -5,7 +5,7 @@ module CompletionKit
|
|
|
5
5
|
before_action :reject_check_metric, only: [:create]
|
|
6
6
|
|
|
7
7
|
def create
|
|
8
|
-
created_by =
|
|
8
|
+
created_by = ck_current_reviewer
|
|
9
9
|
existing = Agreement.find_by(
|
|
10
10
|
run_id: @run.id, response_id: @response.id, metric_id: @metric.id, created_by: created_by
|
|
11
11
|
)
|
|
@@ -74,9 +74,5 @@ module CompletionKit
|
|
|
74
74
|
def review_for_metric
|
|
75
75
|
@response.reviews.find_by(metric_id: @metric.id)
|
|
76
76
|
end
|
|
77
|
-
|
|
78
|
-
def agreement_creator
|
|
79
|
-
request.env["HTTP_X_REMOTE_USER"].presence || CompletionKit.config.username.presence || "operator"
|
|
80
|
-
end
|
|
81
77
|
end
|
|
82
78
|
end
|
|
@@ -5,9 +5,14 @@ module CompletionKit
|
|
|
5
5
|
rate_limit to: CompletionKit.config.api_rate_limit, within: 1.minute,
|
|
6
6
|
with: -> { render_error("Rate limit exceeded", status: :too_many_requests) }
|
|
7
7
|
before_action :authenticate_api!
|
|
8
|
+
rescue_from ConfigurationError, with: :render_configuration_error
|
|
8
9
|
|
|
9
10
|
private
|
|
10
11
|
|
|
12
|
+
def render_configuration_error(error)
|
|
13
|
+
render_error(error.message, status: :unprocessable_entity)
|
|
14
|
+
end
|
|
15
|
+
|
|
11
16
|
def render_error(message, status:, details: nil)
|
|
12
17
|
payload = { error: message }
|
|
13
18
|
payload[:details] = details if details.present?
|
|
@@ -15,9 +15,8 @@ module CompletionKit
|
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def create
|
|
18
|
-
metric_group = MetricGroup.new(
|
|
18
|
+
metric_group = MetricGroup.new(metric_group_attributes)
|
|
19
19
|
if metric_group.save
|
|
20
|
-
metric_group.replace_metrics!(params[:metric_ids]) if params.key?(:metric_ids)
|
|
21
20
|
render json: metric_group.reload, status: :created
|
|
22
21
|
else
|
|
23
22
|
render_validation_errors(metric_group)
|
|
@@ -25,8 +24,7 @@ module CompletionKit
|
|
|
25
24
|
end
|
|
26
25
|
|
|
27
26
|
def update
|
|
28
|
-
if @metric_group.update(
|
|
29
|
-
@metric_group.replace_metrics!(params[:metric_ids]) if params.key?(:metric_ids)
|
|
27
|
+
if @metric_group.update(metric_group_attributes)
|
|
30
28
|
render json: @metric_group.reload
|
|
31
29
|
else
|
|
32
30
|
render_validation_errors(@metric_group)
|
|
@@ -49,6 +47,10 @@ module CompletionKit
|
|
|
49
47
|
def metric_group_params
|
|
50
48
|
params.permit(:name, :description, metric_ids: [], tag_names: [])
|
|
51
49
|
end
|
|
50
|
+
|
|
51
|
+
def metric_group_attributes
|
|
52
|
+
metric_group_params.except(:metric_ids).merge(selected_metric_ids: metric_group_params[:metric_ids])
|
|
53
|
+
end
|
|
52
54
|
end
|
|
53
55
|
end
|
|
54
56
|
end
|
|
@@ -48,9 +48,9 @@ module CompletionKit
|
|
|
48
48
|
return
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
-
MetricVersion.drafts.where(metric_id: @metric.id, source: "suggestion").destroy_all
|
|
52
51
|
generator = MetricVariantGenerator.new(@metric, count: params[:count].to_i, model: params[:model])
|
|
53
52
|
variants = generator.call
|
|
53
|
+
MetricVersion.drafts.where(metric_id: @metric.id, source: "suggestion").destroy_all
|
|
54
54
|
if variants.empty?
|
|
55
55
|
render_error("The model returned no usable variants. Try again with a different model.", status: :unprocessable_entity)
|
|
56
56
|
return
|
|
@@ -18,9 +18,8 @@ module CompletionKit
|
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def create
|
|
21
|
-
run = Run.new(
|
|
21
|
+
run = Run.new(run_attributes)
|
|
22
22
|
if run.save
|
|
23
|
-
run.replace_metrics!(params[:metric_ids])
|
|
24
23
|
render json: run.reload, status: :created
|
|
25
24
|
else
|
|
26
25
|
render_validation_errors(run)
|
|
@@ -28,8 +27,7 @@ module CompletionKit
|
|
|
28
27
|
end
|
|
29
28
|
|
|
30
29
|
def update
|
|
31
|
-
if @run.update(
|
|
32
|
-
@run.replace_metrics!(params[:metric_ids]) if params.key?(:metric_ids)
|
|
30
|
+
if @run.update(run_attributes)
|
|
33
31
|
render json: @run.reload
|
|
34
32
|
else
|
|
35
33
|
render_validation_errors(@run)
|
|
@@ -65,13 +63,15 @@ module CompletionKit
|
|
|
65
63
|
else
|
|
66
64
|
render_error(new_run.failure_summary || "Could not start the new run.", status: :unprocessable_entity)
|
|
67
65
|
end
|
|
66
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
67
|
+
render_validation_errors(e.record)
|
|
68
68
|
end
|
|
69
69
|
|
|
70
70
|
def regrade
|
|
71
71
|
if @run.regrade!
|
|
72
72
|
render json: @run.reload, status: :accepted
|
|
73
73
|
else
|
|
74
|
-
render_error("Nothing to re-grade. The run has no succeeded responses or no metrics attached.", status: :unprocessable_entity)
|
|
74
|
+
render_error(@run.errors.full_messages.to_sentence.presence || "Nothing to re-grade. The run has no succeeded responses or no metrics attached.", status: :unprocessable_entity)
|
|
75
75
|
end
|
|
76
76
|
end
|
|
77
77
|
|
|
@@ -133,6 +133,10 @@ module CompletionKit
|
|
|
133
133
|
params.permit(:name, :prompt_id, :dataset_id, :judge_model, :temperature, :max_tokens, :judge_temperature, :output_column, :expected_column,
|
|
134
134
|
metric_ids: [], tag_names: [])
|
|
135
135
|
end
|
|
136
|
+
|
|
137
|
+
def run_attributes
|
|
138
|
+
run_params.except(:metric_ids).merge(selected_metric_ids: run_params[:metric_ids])
|
|
139
|
+
end
|
|
136
140
|
end
|
|
137
141
|
end
|
|
138
142
|
end
|
|
@@ -8,9 +8,22 @@ module CompletionKit
|
|
|
8
8
|
rate_limit to: CompletionKit.config.web_rate_limit, within: 1.minute,
|
|
9
9
|
with: -> { render plain: "Rate limit exceeded. Please slow down.", status: :too_many_requests }
|
|
10
10
|
before_action :authenticate_completion_kit!
|
|
11
|
+
helper_method :ck_current_reviewer
|
|
11
12
|
|
|
12
13
|
private
|
|
13
14
|
|
|
15
|
+
def ck_current_reviewer
|
|
16
|
+
@ck_current_reviewer ||= begin
|
|
17
|
+
cfg = CompletionKit.config
|
|
18
|
+
name = cfg.current_reviewer&.call(self)
|
|
19
|
+
unless name.nil? || name.is_a?(String)
|
|
20
|
+
raise CompletionKit::ConfigurationError,
|
|
21
|
+
"current_reviewer must return a String or nil, got #{name.class}. Return a name such as current_user.email."
|
|
22
|
+
end
|
|
23
|
+
name.presence || cfg.username.presence || "operator"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
14
27
|
def workspace_ready?
|
|
15
28
|
CompletionKit::Onboarding::Checklist.new.complete? ||
|
|
16
29
|
cookies[ONBOARDING_DISMISS_COOKIE].present?
|
|
@@ -2,6 +2,8 @@ module CompletionKit
|
|
|
2
2
|
class McpController < Api::V1::BaseController
|
|
3
3
|
PUBLIC_METHODS = %w[initialize notifications/initialized tools/list].freeze
|
|
4
4
|
|
|
5
|
+
FOREIGN_KEY_MESSAGE = "This change references a record that does not exist or is still in use.".freeze
|
|
6
|
+
|
|
5
7
|
skip_before_action :authenticate_api!, only: [:stream, :handle], raise: false
|
|
6
8
|
|
|
7
9
|
def stream
|
|
@@ -39,17 +41,18 @@ module CompletionKit
|
|
|
39
41
|
rescue McpDispatcher::InvalidParams => e
|
|
40
42
|
render json: jsonrpc_error(request_body.dig("id"), -32602, e.message), status: :ok
|
|
41
43
|
rescue ActiveRecord::RecordNotFound => e
|
|
44
|
+
render json: jsonrpc_error(request_body.dig("id"), -32602, not_found_message(e)), status: :ok
|
|
45
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
42
46
|
render json: jsonrpc_error(request_body.dig("id"), -32602, e.message), status: :ok
|
|
43
|
-
rescue ActiveRecord::
|
|
44
|
-
|
|
47
|
+
rescue ActiveRecord::InvalidForeignKey => e
|
|
48
|
+
Rails.error.report(e, handled: true, context: { controller: "CompletionKit::McpController" })
|
|
49
|
+
render json: jsonrpc_error(request_body.dig("id"), -32602, FOREIGN_KEY_MESSAGE), status: :ok
|
|
45
50
|
rescue StandardError => e
|
|
46
51
|
Rails.error.report(e, handled: true, context: { controller: "CompletionKit::McpController" })
|
|
47
52
|
render json: jsonrpc_error(request_body.dig("id"), -32603, "Internal error"), status: :ok
|
|
48
53
|
end
|
|
49
54
|
|
|
50
55
|
def destroy
|
|
51
|
-
session_id = request.headers["Mcp-Session-Id"]
|
|
52
|
-
McpSession.destroy_session(session_id) if session_id
|
|
53
56
|
head :ok
|
|
54
57
|
end
|
|
55
58
|
|
|
@@ -61,6 +64,11 @@ module CompletionKit
|
|
|
61
64
|
!performed?
|
|
62
65
|
end
|
|
63
66
|
|
|
67
|
+
def not_found_message(error)
|
|
68
|
+
return "Record not found" unless error.model.present? && error.id.present?
|
|
69
|
+
"#{error.model.demodulize.titleize} #{Array(error.id).join(", ")} not found"
|
|
70
|
+
end
|
|
71
|
+
|
|
64
72
|
def jsonrpc_response(id, result)
|
|
65
73
|
{jsonrpc: "2.0", id: id, result: result}
|
|
66
74
|
end
|
|
@@ -20,11 +20,10 @@ module CompletionKit
|
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def create
|
|
23
|
-
@metric_group = MetricGroup.new(
|
|
23
|
+
@metric_group = MetricGroup.new(metric_group_attributes)
|
|
24
24
|
@metrics = Metric.includes(:tags).order(:name)
|
|
25
25
|
|
|
26
26
|
if @metric_group.save
|
|
27
|
-
@metric_group.replace_metrics!(metric_group_params[:metric_ids])
|
|
28
27
|
redirect_to metric_group_path(@metric_group), notice: "Metric group was successfully created."
|
|
29
28
|
else
|
|
30
29
|
render :new, status: :unprocessable_entity
|
|
@@ -34,8 +33,7 @@ module CompletionKit
|
|
|
34
33
|
def update
|
|
35
34
|
@metrics = Metric.includes(:tags).order(:name)
|
|
36
35
|
|
|
37
|
-
if @metric_group.update(
|
|
38
|
-
@metric_group.replace_metrics!(metric_group_params[:metric_ids])
|
|
36
|
+
if @metric_group.update(metric_group_attributes)
|
|
39
37
|
redirect_to metric_group_path(@metric_group), notice: "Metric group was successfully updated."
|
|
40
38
|
else
|
|
41
39
|
render :edit, status: :unprocessable_entity
|
|
@@ -56,5 +54,9 @@ module CompletionKit
|
|
|
56
54
|
def metric_group_params
|
|
57
55
|
params.require(:metric_group).permit(:name, :description, metric_ids: [], tag_names: [])
|
|
58
56
|
end
|
|
57
|
+
|
|
58
|
+
def metric_group_attributes
|
|
59
|
+
metric_group_params.except(:metric_ids).merge(selected_metric_ids: metric_group_params[:metric_ids])
|
|
60
|
+
end
|
|
59
61
|
end
|
|
60
62
|
end
|
|
@@ -29,7 +29,11 @@ module CompletionKit
|
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
def update
|
|
32
|
-
|
|
32
|
+
attributes = provider_credential_update_params
|
|
33
|
+
remove = attributes[:remove_api_key] == "1"
|
|
34
|
+
@remove_api_key = remove && attributes[:api_key].blank?
|
|
35
|
+
|
|
36
|
+
if @provider_credential.update(with_api_key_choice(attributes.except(:remove_api_key), remove: remove))
|
|
33
37
|
redirect_to provider_credentials_path, notice: "Provider credential was successfully updated."
|
|
34
38
|
else
|
|
35
39
|
render :edit, status: :unprocessable_entity
|
|
@@ -72,5 +76,19 @@ module CompletionKit
|
|
|
72
76
|
def provider_credential_params
|
|
73
77
|
params.require(:provider_credential).permit(:provider, :api_key, :api_endpoint, :api_version)
|
|
74
78
|
end
|
|
79
|
+
|
|
80
|
+
def provider_credential_update_params
|
|
81
|
+
params.require(:provider_credential).permit(:provider, :api_key, :api_endpoint, :api_version, :remove_api_key)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def with_api_key_choice(attributes, remove:)
|
|
85
|
+
if attributes[:api_key].present?
|
|
86
|
+
attributes
|
|
87
|
+
elsif remove
|
|
88
|
+
attributes.merge(api_key: nil)
|
|
89
|
+
else
|
|
90
|
+
attributes.except(:api_key)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
75
93
|
end
|
|
76
94
|
end
|
|
@@ -40,9 +40,8 @@ module CompletionKit
|
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
def create
|
|
43
|
-
@run = Run.new(
|
|
43
|
+
@run = Run.new(run_attributes)
|
|
44
44
|
if @run.save
|
|
45
|
-
@run.replace_metrics!(params[:run][:metric_ids])
|
|
46
45
|
redirect_to run_path(@run), notice: "Run was successfully created."
|
|
47
46
|
else
|
|
48
47
|
load_form_collections
|
|
@@ -52,20 +51,18 @@ module CompletionKit
|
|
|
52
51
|
|
|
53
52
|
def update
|
|
54
53
|
if @run.responses.any? && run_generation_changed?
|
|
55
|
-
attrs =
|
|
54
|
+
attrs = run_attributes.to_h
|
|
56
55
|
attrs.delete("name") if attrs["name"].to_s == @run.name.to_s
|
|
57
56
|
new_run = Run.new(attrs.merge(status: "pending"))
|
|
58
57
|
if new_run.save
|
|
59
|
-
new_run.replace_metrics!(params[:run][:metric_ids]) if params[:run].key?(:metric_ids)
|
|
60
58
|
redirect_to run_path(new_run), notice: "Saved as a new run. The previous run and its results are preserved."
|
|
61
59
|
else
|
|
62
|
-
@run.assign_attributes(
|
|
60
|
+
@run.assign_attributes(run_attributes)
|
|
63
61
|
new_run.errors.each { |error| @run.errors.add(error.attribute, error.message) }
|
|
64
62
|
load_form_collections
|
|
65
63
|
render :edit, status: :unprocessable_entity
|
|
66
64
|
end
|
|
67
|
-
elsif @run.update(
|
|
68
|
-
@run.replace_metrics!(params[:run][:metric_ids]) if params[:run].key?(:metric_ids)
|
|
65
|
+
elsif @run.update(run_attributes)
|
|
69
66
|
redirect_to run_path(@run), notice: "Run saved."
|
|
70
67
|
else
|
|
71
68
|
load_form_collections
|
|
@@ -106,7 +103,7 @@ module CompletionKit
|
|
|
106
103
|
if @run.regrade!
|
|
107
104
|
redirect_to run_path(@run), notice: "Re-grading existing responses against the current metrics."
|
|
108
105
|
else
|
|
109
|
-
redirect_to run_path(@run), alert: "Nothing to re-grade. The run has no succeeded responses or no metrics attached."
|
|
106
|
+
redirect_to run_path(@run), alert: @run.errors.full_messages.to_sentence.presence || "Nothing to re-grade. The run has no succeeded responses or no metrics attached."
|
|
110
107
|
end
|
|
111
108
|
end
|
|
112
109
|
|
|
@@ -117,6 +114,10 @@ module CompletionKit
|
|
|
117
114
|
else
|
|
118
115
|
redirect_to run_path(new_run), alert: new_run.failure_summary || "Could not start the new run."
|
|
119
116
|
end
|
|
117
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
118
|
+
redirect_to run_path(@run), alert: "This run can't be rerun. #{e.record.errors.full_messages.to_sentence}."
|
|
119
|
+
rescue ConfigurationError => e
|
|
120
|
+
redirect_to run_path(@run), alert: e.message
|
|
120
121
|
end
|
|
121
122
|
|
|
122
123
|
# The web process drives the live run page, because worker-side Turbo
|
|
@@ -226,6 +227,10 @@ module CompletionKit
|
|
|
226
227
|
params.require(:run).permit(:name, :prompt_id, :dataset_id, :judge_model, :temperature, :max_tokens, :judge_temperature, :output_column, :expected_column, metric_ids: [], tag_names: [])
|
|
227
228
|
end
|
|
228
229
|
|
|
230
|
+
def run_attributes
|
|
231
|
+
run_params.except(:metric_ids).merge(selected_metric_ids: run_params[:metric_ids])
|
|
232
|
+
end
|
|
233
|
+
|
|
229
234
|
# Editing a run that already has results forks a new run — but only when a
|
|
230
235
|
# field that affects generation or judging changed. Renaming or retagging is
|
|
231
236
|
# pure metadata and updates the run in place.
|
|
@@ -76,7 +76,7 @@ module CompletionKit
|
|
|
76
76
|
error_provider: nil, error_class: nil, error_status: nil, error_message: nil
|
|
77
77
|
)
|
|
78
78
|
|
|
79
|
-
if run.
|
|
79
|
+
if run.judge_model.present?
|
|
80
80
|
run.llm_metrics.each do |metric|
|
|
81
81
|
JudgeReviewJob.perform_later(response.id, metric.id, run.id)
|
|
82
82
|
end
|
|
@@ -101,6 +101,7 @@ module CompletionKit
|
|
|
101
101
|
response = Response.find_by(id: @response_id)
|
|
102
102
|
return unless response
|
|
103
103
|
|
|
104
|
+
error = EndpointError.from(error)
|
|
104
105
|
response.update!(
|
|
105
106
|
status: "failed",
|
|
106
107
|
error_provider: provider_for(response),
|
|
@@ -117,6 +117,7 @@ module CompletionKit
|
|
|
117
117
|
response = Response.find_by(id: @response_id)
|
|
118
118
|
return unless response
|
|
119
119
|
|
|
120
|
+
error = EndpointError.from(error)
|
|
120
121
|
review = response.reviews.find_or_initialize_by(metric_id: @metric_id)
|
|
121
122
|
review.assign_attributes(
|
|
122
123
|
metric_name: review.metric_name || Metric.find_by(id: @metric_id)&.name || "(deleted metric)",
|
|
@@ -17,10 +17,9 @@ module CompletionKit
|
|
|
17
17
|
return unless @metric
|
|
18
18
|
return if @metric.check?
|
|
19
19
|
|
|
20
|
-
MetricVersion.drafts.where(metric_id: @metric.id, source: "suggestion").destroy_all
|
|
21
|
-
|
|
22
20
|
generator = MetricVariantGenerator.new(@metric, count: 1)
|
|
23
21
|
variants = generator.call
|
|
22
|
+
MetricVersion.drafts.where(metric_id: @metric.id, source: "suggestion").destroy_all
|
|
24
23
|
if variants.empty?
|
|
25
24
|
broadcast_status(@metric, partial: "completion_kit/metrics/suggestion_failed", locals: { metric: @metric })
|
|
26
25
|
return
|
|
@@ -8,26 +8,23 @@ module CompletionKit
|
|
|
8
8
|
30 * executions
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
+
rescue_from(StandardError) do |error|
|
|
12
|
+
report_and_record_failure!(error)
|
|
13
|
+
end
|
|
14
|
+
|
|
11
15
|
retry_on Faraday::TimeoutError,
|
|
12
16
|
Faraday::ConnectionFailed,
|
|
13
|
-
wait: :polynomially_longer, attempts: 5
|
|
17
|
+
wait: :polynomially_longer, attempts: 5 do |job, error|
|
|
18
|
+
job.send(:report_and_record_failure!, error)
|
|
19
|
+
end
|
|
14
20
|
|
|
15
21
|
retry_on CompletionKit::RateLimitError,
|
|
16
|
-
wait:
|
|
22
|
+
wait: ->(executions) { rate_limit_wait(executions) }, attempts: 5 do |job, error|
|
|
23
|
+
job.send(:report_and_record_failure!, error)
|
|
24
|
+
end
|
|
17
25
|
|
|
18
26
|
discard_on ActiveJob::DeserializationError
|
|
19
27
|
|
|
20
|
-
rescue_from(StandardError) do |error|
|
|
21
|
-
unless error.is_a?(CompletionKit::ModelDiscoveryService::DiscoveryError)
|
|
22
|
-
Rails.error.report(error, handled: true, context: { job: self.class.name, provider_credential_id: arguments.first })
|
|
23
|
-
end
|
|
24
|
-
credential = ProviderCredential.find_by(id: arguments.first)
|
|
25
|
-
next unless credential
|
|
26
|
-
credential.update_columns(discovery_status: "failed", discovery_error: error.message.to_s.truncate(500))
|
|
27
|
-
credential.reload
|
|
28
|
-
credential.broadcast_discovery_progress
|
|
29
|
-
end
|
|
30
|
-
|
|
31
28
|
def perform(provider_credential_id, force: false)
|
|
32
29
|
credential = ProviderCredential.find_by(id: provider_credential_id)
|
|
33
30
|
return unless credential
|
|
@@ -57,5 +54,23 @@ module CompletionKit
|
|
|
57
54
|
credential.reload
|
|
58
55
|
credential.broadcast_discovery_complete
|
|
59
56
|
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def report_and_record_failure!(error)
|
|
61
|
+
unless error.is_a?(CompletionKit::ModelDiscoveryService::DiscoveryError)
|
|
62
|
+
Rails.error.report(error, handled: true, context: { job: self.class.name, provider_credential_id: arguments.first })
|
|
63
|
+
end
|
|
64
|
+
record_failure!(error)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def record_failure!(error)
|
|
68
|
+
credential = ProviderCredential.find_by(id: arguments.first)
|
|
69
|
+
return unless credential
|
|
70
|
+
|
|
71
|
+
credential.update_columns(discovery_status: "failed", discovery_error: EndpointError.from(error).message.to_s.truncate(500))
|
|
72
|
+
credential.reload
|
|
73
|
+
credential.broadcast_discovery_progress
|
|
74
|
+
end
|
|
60
75
|
end
|
|
61
76
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
module CompletionKit
|
|
2
2
|
class MetricGroup < ApplicationRecord
|
|
3
3
|
include CompletionKit::Taggable
|
|
4
|
+
include CompletionKit::MetricSelection
|
|
4
5
|
|
|
5
6
|
self.table_name = "completion_kit_metric_groups"
|
|
6
7
|
|
|
@@ -14,9 +15,10 @@ module CompletionKit
|
|
|
14
15
|
end
|
|
15
16
|
|
|
16
17
|
def replace_metrics!(metric_ids)
|
|
17
|
-
return
|
|
18
|
+
return if metric_ids.nil?
|
|
19
|
+
|
|
18
20
|
metric_group_memberships.delete_all
|
|
19
|
-
|
|
21
|
+
normalize_metric_ids(metric_ids).each_with_index do |metric_id, index|
|
|
20
22
|
metric_group_memberships.create!(metric_id: metric_id, position: index + 1)
|
|
21
23
|
end
|
|
22
24
|
end
|
|
@@ -3,7 +3,7 @@ module CompletionKit
|
|
|
3
3
|
self.table_name = "completion_kit_metric_group_memberships"
|
|
4
4
|
|
|
5
5
|
belongs_to :metric_group, class_name: "CompletionKit::MetricGroup", foreign_key: "metric_group_id"
|
|
6
|
-
belongs_to :metric
|
|
6
|
+
belongs_to :metric, optional: false
|
|
7
7
|
|
|
8
8
|
validates :metric_id, uniqueness: { scope: :metric_group_id }
|
|
9
9
|
|
|
@@ -5,7 +5,7 @@ module CompletionKit
|
|
|
5
5
|
has_many :runs, dependent: :destroy
|
|
6
6
|
has_many :responses, through: :runs
|
|
7
7
|
|
|
8
|
-
attr_accessor :cloned_from_llm_model
|
|
8
|
+
attr_accessor :cloned_from_llm_model, :allow_unconfigured_provider
|
|
9
9
|
|
|
10
10
|
validates :name, presence: true
|
|
11
11
|
validates :template, presence: true
|
|
@@ -105,12 +105,13 @@ module CompletionKit
|
|
|
105
105
|
end
|
|
106
106
|
|
|
107
107
|
def llm_model_usable_for_generation
|
|
108
|
-
|
|
109
|
-
return if rows.empty?
|
|
110
|
-
return if rows.where(supports_generation: true, status: "active").exists?
|
|
111
|
-
return unless rows.where(supports_generation: false).exists?
|
|
108
|
+
return if Model.for_generation.exists?(model_id: llm_model)
|
|
112
109
|
|
|
113
|
-
|
|
110
|
+
if Model.exists?(model_id: llm_model, supports_generation: false)
|
|
111
|
+
errors.add(:llm_model, "is not available for generating responses")
|
|
112
|
+
elsif !allow_unconfigured_provider && !ApiConfig.servable_model?(llm_model)
|
|
113
|
+
errors.add(:llm_model, %("#{llm_model}" is not served by any configured provider))
|
|
114
|
+
end
|
|
114
115
|
end
|
|
115
116
|
|
|
116
117
|
def assign_family_key
|
|
@@ -27,6 +27,7 @@ module CompletionKit
|
|
|
27
27
|
validates :provider, tenant_scoped_uniqueness: true
|
|
28
28
|
validates :api_endpoint, presence: true, if: :azure_foundry?
|
|
29
29
|
validate :api_endpoint_not_internal
|
|
30
|
+
validate :api_key_reentered_for_new_destination, on: :update
|
|
30
31
|
|
|
31
32
|
after_save :enqueue_discovery
|
|
32
33
|
before_destroy :ensure_not_in_use, prepend: true
|
|
@@ -36,6 +37,15 @@ module CompletionKit
|
|
|
36
37
|
provider == "azure_foundry"
|
|
37
38
|
end
|
|
38
39
|
|
|
40
|
+
def api_key_stored?
|
|
41
|
+
api_key_in_database.present?
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def api_key_ending
|
|
45
|
+
key = api_key_in_database.to_s
|
|
46
|
+
key.last(4) if key.length > 8
|
|
47
|
+
end
|
|
48
|
+
|
|
39
49
|
def config_hash
|
|
40
50
|
{
|
|
41
51
|
provider: provider,
|
|
@@ -180,14 +190,21 @@ module CompletionKit
|
|
|
180
190
|
Rails.logger.error("[CompletionKit] discovery broadcast render failed: #{e.class}: #{e.message}")
|
|
181
191
|
end
|
|
182
192
|
|
|
193
|
+
def api_key_reentered_for_new_destination
|
|
194
|
+
return if api_key_came_from_user? || api_key.blank?
|
|
195
|
+
return unless provider_changed? || api_endpoint_in_database.presence != api_endpoint.presence
|
|
196
|
+
|
|
197
|
+
errors.add(:api_key, "must be entered again when the provider or endpoint changes")
|
|
198
|
+
end
|
|
199
|
+
|
|
183
200
|
def api_endpoint_not_internal
|
|
184
201
|
return if api_endpoint.blank?
|
|
185
202
|
|
|
186
203
|
issues = ProviderEndpoint.validate(api_endpoint)
|
|
187
204
|
if issues.include?(:invalid_url)
|
|
188
205
|
errors.add(:api_endpoint, "must be a valid http or https URL")
|
|
189
|
-
elsif issues.
|
|
190
|
-
errors.add(:api_endpoint, "must
|
|
206
|
+
elsif issues.any?
|
|
207
|
+
errors.add(:api_endpoint, "must resolve to a public address")
|
|
191
208
|
end
|
|
192
209
|
end
|
|
193
210
|
end
|