completion-kit 0.1.0 → 0.2.1
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 +16 -0
- data/app/controllers/completion_kit/api/v1/metric_groups_controller.rb +2 -11
- data/app/controllers/completion_kit/api/v1/runs_controller.rb +2 -10
- data/app/controllers/completion_kit/metric_groups_controller.rb +2 -10
- data/app/controllers/completion_kit/runs_controller.rb +3 -10
- data/app/helpers/completion_kit/application_helper.rb +1 -8
- data/app/models/completion_kit/application_record.rb +7 -0
- data/app/models/completion_kit/metric.rb +1 -1
- data/app/models/completion_kit/metric_group.rb +8 -0
- data/app/models/completion_kit/model.rb +1 -1
- data/app/models/completion_kit/provider_credential.rb +2 -1
- data/app/models/completion_kit/run.rb +11 -3
- data/app/services/completion_kit/anthropic_client.rb +4 -17
- data/app/services/completion_kit/llm_client.rb +15 -0
- data/app/services/completion_kit/mcp_tools/base.rb +23 -0
- data/app/services/completion_kit/mcp_tools/datasets.rb +2 -18
- data/app/services/completion_kit/mcp_tools/metric_groups.rb +4 -28
- data/app/services/completion_kit/mcp_tools/metrics.rb +2 -18
- data/app/services/completion_kit/mcp_tools/prompts.rb +2 -18
- data/app/services/completion_kit/mcp_tools/provider_credentials.rb +2 -18
- data/app/services/completion_kit/mcp_tools/responses.rb +2 -13
- data/app/services/completion_kit/mcp_tools/runs.rb +4 -28
- data/app/services/completion_kit/ollama_client.rb +2 -15
- data/app/services/completion_kit/open_ai_client.rb +1 -10
- data/app/services/completion_kit/open_router_client.rb +1 -12
- data/app/validators/completion_kit/tenant_scoped_uniqueness_validator.rb +15 -0
- data/db/migrate/20260403000003_add_applied_at_to_completion_kit_suggestions.rb +1 -0
- data/lib/completion_kit/version.rb +1 -1
- data/lib/completion_kit.rb +5 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c87481def48cfd6193ee591e9ac1ab1a2be6b7de63a3275c82f7f7175804abfc
|
|
4
|
+
data.tar.gz: 333b23de10b7e81daeac7c118f2c4ae13b2624304a1d04404b206aa5dffce092
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '0538aae286aeffe40644e580dc84a27ee1bc2a187fe0a060f33bc4dfb30e04a769f9daa2b8a59b19633d35b8f79172a40bcd03d75d136c5dc1d4a8af92e5aa4a'
|
|
7
|
+
data.tar.gz: 1843ea626685029288ec9533edc07a800bca18b8e435de22d1b6c32c59e4b2d52c08999833f7b123d983855e445a95ae814cb12a31aa21c041bac11d7a70c466
|
data/README.md
CHANGED
|
@@ -178,6 +178,22 @@ bin/rails db:migrate
|
|
|
178
178
|
git add db/migrate/ && git commit -m "install new engine migration"
|
|
179
179
|
```
|
|
180
180
|
|
|
181
|
+
## Multi-tenant host apps (advanced)
|
|
182
|
+
|
|
183
|
+
For hosts that mount CompletionKit in a multi-tenant app, two optional hooks scope engine records per tenant without forking the engine:
|
|
184
|
+
|
|
185
|
+
```ruby
|
|
186
|
+
CompletionKit.configure do |config|
|
|
187
|
+
config.tenant_scope = -> {
|
|
188
|
+
org = Current.organization&.id
|
|
189
|
+
org ? where(organization_id: org) : where("1=0")
|
|
190
|
+
}
|
|
191
|
+
config.tenant_scope_columns = [:organization_id]
|
|
192
|
+
end
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
`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.
|
|
196
|
+
|
|
181
197
|
## Contributing
|
|
182
198
|
|
|
183
199
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, testing, and pull request guidelines.
|
|
@@ -15,7 +15,7 @@ module CompletionKit
|
|
|
15
15
|
def create
|
|
16
16
|
metric_group = MetricGroup.new(metric_group_params.except(:metric_ids))
|
|
17
17
|
if metric_group.save
|
|
18
|
-
|
|
18
|
+
metric_group.replace_metrics!(params[:metric_ids]) if params.key?(:metric_ids)
|
|
19
19
|
render json: metric_group.reload, status: :created
|
|
20
20
|
else
|
|
21
21
|
render json: {errors: metric_group.errors}, status: :unprocessable_entity
|
|
@@ -24,7 +24,7 @@ module CompletionKit
|
|
|
24
24
|
|
|
25
25
|
def update
|
|
26
26
|
if @metric_group.update(metric_group_params.except(:metric_ids))
|
|
27
|
-
|
|
27
|
+
@metric_group.replace_metrics!(params[:metric_ids]) if params.key?(:metric_ids)
|
|
28
28
|
render json: @metric_group.reload
|
|
29
29
|
else
|
|
30
30
|
render json: {errors: @metric_group.errors}, status: :unprocessable_entity
|
|
@@ -47,15 +47,6 @@ module CompletionKit
|
|
|
47
47
|
def metric_group_params
|
|
48
48
|
params.permit(:name, :description, metric_ids: [])
|
|
49
49
|
end
|
|
50
|
-
|
|
51
|
-
def replace_metric_memberships(metric_group, metric_ids)
|
|
52
|
-
return unless metric_ids
|
|
53
|
-
|
|
54
|
-
metric_group.metric_group_memberships.delete_all
|
|
55
|
-
Array(metric_ids).reject(&:blank?).each_with_index do |metric_id, index|
|
|
56
|
-
metric_group.metric_group_memberships.create!(metric_id: metric_id, position: index + 1)
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
50
|
end
|
|
60
51
|
end
|
|
61
52
|
end
|
|
@@ -15,7 +15,7 @@ module CompletionKit
|
|
|
15
15
|
def create
|
|
16
16
|
run = Run.new(run_params.except(:metric_ids))
|
|
17
17
|
if run.save
|
|
18
|
-
|
|
18
|
+
run.replace_metrics!(params[:metric_ids])
|
|
19
19
|
render json: run.reload, status: :created
|
|
20
20
|
else
|
|
21
21
|
render json: {errors: run.errors}, status: :unprocessable_entity
|
|
@@ -24,7 +24,7 @@ module CompletionKit
|
|
|
24
24
|
|
|
25
25
|
def update
|
|
26
26
|
if @run.update(run_params.except(:metric_ids))
|
|
27
|
-
|
|
27
|
+
@run.replace_metrics!(params[:metric_ids]) if params.key?(:metric_ids)
|
|
28
28
|
render json: @run.reload
|
|
29
29
|
else
|
|
30
30
|
render json: {errors: @run.errors}, status: :unprocessable_entity
|
|
@@ -57,14 +57,6 @@ module CompletionKit
|
|
|
57
57
|
def run_params
|
|
58
58
|
params.permit(:name, :prompt_id, :dataset_id, :judge_model, :temperature, metric_ids: [])
|
|
59
59
|
end
|
|
60
|
-
|
|
61
|
-
def replace_run_metrics(run, metric_ids)
|
|
62
|
-
return unless metric_ids
|
|
63
|
-
run.run_metrics.delete_all
|
|
64
|
-
Array(metric_ids).reject(&:blank?).each_with_index do |metric_id, index|
|
|
65
|
-
run.run_metrics.create!(metric_id: metric_id, position: index + 1)
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
60
|
end
|
|
69
61
|
end
|
|
70
62
|
end
|
|
@@ -23,7 +23,7 @@ module CompletionKit
|
|
|
23
23
|
@metrics = Metric.order(:name)
|
|
24
24
|
|
|
25
25
|
if @metric_group.save
|
|
26
|
-
|
|
26
|
+
@metric_group.replace_metrics!(metric_group_params[:metric_ids])
|
|
27
27
|
redirect_to metric_group_path(@metric_group), notice: "Metric group was successfully created."
|
|
28
28
|
else
|
|
29
29
|
render :new, status: :unprocessable_entity
|
|
@@ -34,7 +34,7 @@ module CompletionKit
|
|
|
34
34
|
@metrics = Metric.order(:name)
|
|
35
35
|
|
|
36
36
|
if @metric_group.update(metric_group_params.except(:metric_ids))
|
|
37
|
-
|
|
37
|
+
@metric_group.replace_metrics!(metric_group_params[:metric_ids])
|
|
38
38
|
redirect_to metric_group_path(@metric_group), notice: "Metric group was successfully updated."
|
|
39
39
|
else
|
|
40
40
|
render :edit, status: :unprocessable_entity
|
|
@@ -55,13 +55,5 @@ module CompletionKit
|
|
|
55
55
|
def metric_group_params
|
|
56
56
|
params.require(:metric_group).permit(:name, :description, metric_ids: [])
|
|
57
57
|
end
|
|
58
|
-
|
|
59
|
-
def replace_metric_memberships
|
|
60
|
-
metric_ids = Array(metric_group_params[:metric_ids]).reject(&:blank?)
|
|
61
|
-
@metric_group.metric_group_memberships.delete_all
|
|
62
|
-
metric_ids.each_with_index do |metric_id, index|
|
|
63
|
-
@metric_group.metric_group_memberships.create!(metric_id: metric_id, position: index + 1)
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
58
|
end
|
|
67
59
|
end
|
|
@@ -35,7 +35,7 @@ module CompletionKit
|
|
|
35
35
|
def create
|
|
36
36
|
@run = Run.new(run_params.except(:metric_ids))
|
|
37
37
|
if @run.save
|
|
38
|
-
|
|
38
|
+
@run.replace_metrics!(params[:run][:metric_ids])
|
|
39
39
|
redirect_to run_path(@run), notice: "Run was successfully created."
|
|
40
40
|
else
|
|
41
41
|
load_form_collections
|
|
@@ -46,10 +46,10 @@ module CompletionKit
|
|
|
46
46
|
def update
|
|
47
47
|
if @run.responses.any?
|
|
48
48
|
new_run = Run.create!(run_params.except(:metric_ids).to_h.merge(status: "pending"))
|
|
49
|
-
|
|
49
|
+
new_run.replace_metrics!(params[:run][:metric_ids]) if params[:run].key?(:metric_ids)
|
|
50
50
|
redirect_to run_path(new_run), notice: "Saved as a new run. The previous run and its results are preserved."
|
|
51
51
|
elsif @run.update(run_params.except(:metric_ids))
|
|
52
|
-
|
|
52
|
+
@run.replace_metrics!(params[:run][:metric_ids]) if params[:run].key?(:metric_ids)
|
|
53
53
|
redirect_to run_path(@run), notice: "Run saved."
|
|
54
54
|
else
|
|
55
55
|
load_form_collections
|
|
@@ -120,12 +120,5 @@ module CompletionKit
|
|
|
120
120
|
params.require(:run).permit(:name, :prompt_id, :dataset_id, :judge_model, :temperature, metric_ids: [])
|
|
121
121
|
end
|
|
122
122
|
|
|
123
|
-
def replace_run_metrics(run, metric_ids)
|
|
124
|
-
return unless metric_ids
|
|
125
|
-
run.run_metrics.delete_all
|
|
126
|
-
Array(metric_ids).reject(&:blank?).each_with_index do |metric_id, index|
|
|
127
|
-
run.run_metrics.create!(metric_id: metric_id, position: index + 1)
|
|
128
|
-
end
|
|
129
|
-
end
|
|
130
123
|
end
|
|
131
124
|
end
|
|
@@ -76,15 +76,8 @@ module CompletionKit
|
|
|
76
76
|
end
|
|
77
77
|
end
|
|
78
78
|
|
|
79
|
-
PROVIDER_LABELS = {
|
|
80
|
-
"openai" => "OpenAI",
|
|
81
|
-
"anthropic" => "Anthropic",
|
|
82
|
-
"ollama" => "Ollama / local endpoint",
|
|
83
|
-
"openrouter" => "OpenRouter"
|
|
84
|
-
}.freeze
|
|
85
|
-
|
|
86
79
|
def ck_provider_label(provider)
|
|
87
|
-
PROVIDER_LABELS[provider.to_s] || provider.to_s.titleize
|
|
80
|
+
CompletionKit::ProviderCredential::PROVIDER_LABELS[provider.to_s] || provider.to_s.titleize
|
|
88
81
|
end
|
|
89
82
|
|
|
90
83
|
def ck_grouped_models(models, selected = nil)
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
module CompletionKit
|
|
2
2
|
class ApplicationRecord < ActiveRecord::Base
|
|
3
3
|
self.abstract_class = true
|
|
4
|
+
|
|
5
|
+
TenantScopedUniquenessValidator = CompletionKit::TenantScopedUniquenessValidator
|
|
6
|
+
|
|
7
|
+
default_scope do
|
|
8
|
+
scope_proc = CompletionKit.config.tenant_scope
|
|
9
|
+
scope_proc ? instance_exec(&scope_proc) : all
|
|
10
|
+
end
|
|
4
11
|
end
|
|
5
12
|
end
|
|
@@ -15,7 +15,7 @@ module CompletionKit
|
|
|
15
15
|
serialize :rubric_bands, coder: JSON
|
|
16
16
|
|
|
17
17
|
validates :name, presence: true
|
|
18
|
-
validates :key,
|
|
18
|
+
validates :key, tenant_scoped_uniqueness: { allow_nil: true }
|
|
19
19
|
|
|
20
20
|
before_validation :generate_key
|
|
21
21
|
before_validation :normalize_rubric_bands
|
|
@@ -11,6 +11,14 @@ module CompletionKit
|
|
|
11
11
|
metric_group_memberships.includes(:metric).map(&:metric).compact
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
+
def replace_metrics!(metric_ids)
|
|
15
|
+
return unless metric_ids
|
|
16
|
+
metric_group_memberships.delete_all
|
|
17
|
+
Array(metric_ids).reject(&:blank?).each_with_index do |metric_id, index|
|
|
18
|
+
metric_group_memberships.create!(metric_id: metric_id, position: index + 1)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
14
22
|
def as_json(options = {})
|
|
15
23
|
{
|
|
16
24
|
id: id, name: name, description: description,
|
|
@@ -3,7 +3,7 @@ module CompletionKit
|
|
|
3
3
|
STATUSES = %w[active retired failed].freeze
|
|
4
4
|
|
|
5
5
|
validates :provider, presence: true
|
|
6
|
-
validates :model_id, presence: true,
|
|
6
|
+
validates :model_id, presence: true, tenant_scoped_uniqueness: { scope: :provider }
|
|
7
7
|
validates :status, presence: true, inclusion: { in: STATUSES }
|
|
8
8
|
|
|
9
9
|
scope :active, -> { where(status: "active") }
|
|
@@ -22,7 +22,8 @@ module CompletionKit
|
|
|
22
22
|
PROVIDER_LABELS[provider] || provider.titleize
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
validates :provider, presence: true, inclusion: { in: PROVIDERS }
|
|
25
|
+
validates :provider, presence: true, inclusion: { in: PROVIDERS }
|
|
26
|
+
validates :provider, tenant_scoped_uniqueness: true
|
|
26
27
|
|
|
27
28
|
after_save :enqueue_discovery
|
|
28
29
|
|
|
@@ -21,6 +21,14 @@ module CompletionKit
|
|
|
21
21
|
judge_model.present? && metrics.any? && ApiConfig.valid_for_model?(judge_model)
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
+
def replace_metrics!(metric_ids)
|
|
25
|
+
return unless metric_ids
|
|
26
|
+
run_metrics.delete_all
|
|
27
|
+
Array(metric_ids).reject(&:blank?).each_with_index do |metric_id, index|
|
|
28
|
+
run_metrics.create!(metric_id: metric_id, position: index + 1)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
24
32
|
def avg_score
|
|
25
33
|
all_reviews = responses.flat_map(&:reviews)
|
|
26
34
|
scores = all_reviews.map(&:ai_score).compact.map(&:to_f)
|
|
@@ -113,15 +121,15 @@ module CompletionKit
|
|
|
113
121
|
response.response_text,
|
|
114
122
|
response.expected_output,
|
|
115
123
|
prompt.template,
|
|
116
|
-
criteria: metric.
|
|
117
|
-
rubric_text: metric.
|
|
124
|
+
criteria: metric.instruction.to_s,
|
|
125
|
+
rubric_text: metric.display_rubric_text,
|
|
118
126
|
input_data: response.input_data
|
|
119
127
|
)
|
|
120
128
|
|
|
121
129
|
response.reviews.find_or_initialize_by(metric_id: metric.id).tap do |review|
|
|
122
130
|
review.assign_attributes(
|
|
123
131
|
metric_name: metric.name,
|
|
124
|
-
instruction: metric.
|
|
132
|
+
instruction: metric.instruction.to_s,
|
|
125
133
|
status: "evaluated",
|
|
126
134
|
ai_score: evaluation[:score],
|
|
127
135
|
ai_feedback: evaluation[:feedback]
|
|
@@ -7,21 +7,12 @@ module CompletionKit
|
|
|
7
7
|
|
|
8
8
|
def generate_completion(prompt, options = {})
|
|
9
9
|
return "Error: API key not configured" unless configured?
|
|
10
|
-
|
|
11
|
-
require "faraday"
|
|
12
|
-
require "faraday/retry"
|
|
13
|
-
require "json"
|
|
14
|
-
|
|
10
|
+
|
|
15
11
|
model = options[:model] || "claude-3-7-sonnet-latest"
|
|
16
12
|
max_tokens = options[:max_tokens] || 1000
|
|
17
13
|
temperature = options[:temperature] || 0.7
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
f.request :retry, max: 2, interval: 0.5
|
|
21
|
-
f.adapter Faraday.default_adapter
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
response = conn.post do |req|
|
|
14
|
+
|
|
15
|
+
response = build_connection("https://api.anthropic.com").post do |req|
|
|
25
16
|
req.url "/v1/messages"
|
|
26
17
|
req.headers["Content-Type"] = "application/json"
|
|
27
18
|
req.headers["x-api-key"] = api_key
|
|
@@ -49,11 +40,7 @@ module CompletionKit
|
|
|
49
40
|
def available_models
|
|
50
41
|
return STATIC_MODELS unless configured?
|
|
51
42
|
|
|
52
|
-
|
|
53
|
-
require "faraday/retry"
|
|
54
|
-
require "json"
|
|
55
|
-
|
|
56
|
-
response = Faraday.get("https://api.anthropic.com/v1/models?limit=100") do |req|
|
|
43
|
+
response = build_connection("https://api.anthropic.com").get("/v1/models?limit=100") do |req|
|
|
57
44
|
req.headers["x-api-key"] = api_key
|
|
58
45
|
req.headers["anthropic-version"] = "2023-06-01"
|
|
59
46
|
end
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
require "faraday"
|
|
2
|
+
require "faraday/retry"
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
1
5
|
module CompletionKit
|
|
2
6
|
class LlmClient
|
|
3
7
|
def initialize(config = {})
|
|
@@ -41,5 +45,16 @@ module CompletionKit
|
|
|
41
45
|
|
|
42
46
|
for_provider(provider, config)
|
|
43
47
|
end
|
|
48
|
+
|
|
49
|
+
protected
|
|
50
|
+
|
|
51
|
+
def build_connection(url, timeout: nil, open_timeout: nil)
|
|
52
|
+
Faraday.new(url: url) do |f|
|
|
53
|
+
f.options.timeout = timeout if timeout
|
|
54
|
+
f.options.open_timeout = open_timeout if open_timeout
|
|
55
|
+
f.request :retry, max: 2, interval: 0.5
|
|
56
|
+
f.adapter Faraday.default_adapter
|
|
57
|
+
end
|
|
58
|
+
end
|
|
44
59
|
end
|
|
45
60
|
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module CompletionKit
|
|
2
|
+
module McpTools
|
|
3
|
+
module Base
|
|
4
|
+
def definitions
|
|
5
|
+
self::TOOLS.map { |name, config| {name: name, description: config[:description], inputSchema: config[:inputSchema]} }
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def call(name, arguments)
|
|
9
|
+
tool = self::TOOLS.fetch(name)
|
|
10
|
+
send(tool[:handler], arguments)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def text_result(data)
|
|
14
|
+
text = data.is_a?(String) ? data : data.to_json
|
|
15
|
+
{content: [{type: "text", text: text}]}
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def error_result(message)
|
|
19
|
+
{content: [{type: "text", text: message}], isError: true}
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
module CompletionKit
|
|
2
2
|
module McpTools
|
|
3
3
|
module Datasets
|
|
4
|
+
extend Base
|
|
5
|
+
|
|
4
6
|
TOOLS = {
|
|
5
7
|
"datasets_list" => {
|
|
6
8
|
description: "List all datasets",
|
|
@@ -37,15 +39,6 @@ module CompletionKit
|
|
|
37
39
|
}
|
|
38
40
|
}.freeze
|
|
39
41
|
|
|
40
|
-
def self.definitions
|
|
41
|
-
TOOLS.map { |name, config| {name: name, description: config[:description], inputSchema: config[:inputSchema]} }
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def self.call(name, arguments)
|
|
45
|
-
tool = TOOLS.fetch(name)
|
|
46
|
-
send(tool[:handler], arguments)
|
|
47
|
-
end
|
|
48
|
-
|
|
49
42
|
def self.list(_args)
|
|
50
43
|
text_result(Dataset.order(created_at: :desc).map(&:as_json))
|
|
51
44
|
end
|
|
@@ -76,15 +69,6 @@ module CompletionKit
|
|
|
76
69
|
Dataset.find(args["id"]).destroy!
|
|
77
70
|
text_result("Dataset #{args["id"]} deleted")
|
|
78
71
|
end
|
|
79
|
-
|
|
80
|
-
def self.text_result(data)
|
|
81
|
-
text = data.is_a?(String) ? data : data.to_json
|
|
82
|
-
{content: [{type: "text", text: text}]}
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def self.error_result(message)
|
|
86
|
-
{content: [{type: "text", text: message}], isError: true}
|
|
87
|
-
end
|
|
88
72
|
end
|
|
89
73
|
end
|
|
90
74
|
end
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
module CompletionKit
|
|
2
2
|
module McpTools
|
|
3
3
|
module MetricGroups
|
|
4
|
+
extend Base
|
|
5
|
+
|
|
4
6
|
TOOLS = {
|
|
5
7
|
"metric_groups_list" => {
|
|
6
8
|
description: "List all metric groups",
|
|
@@ -43,15 +45,6 @@ module CompletionKit
|
|
|
43
45
|
}
|
|
44
46
|
}.freeze
|
|
45
47
|
|
|
46
|
-
def self.definitions
|
|
47
|
-
TOOLS.map { |name, config| {name: name, description: config[:description], inputSchema: config[:inputSchema]} }
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def self.call(name, arguments)
|
|
51
|
-
tool = TOOLS.fetch(name)
|
|
52
|
-
send(tool[:handler], arguments)
|
|
53
|
-
end
|
|
54
|
-
|
|
55
48
|
def self.list(_args)
|
|
56
49
|
text_result(CompletionKit::MetricGroup.order(created_at: :desc).map(&:as_json))
|
|
57
50
|
end
|
|
@@ -63,7 +56,7 @@ module CompletionKit
|
|
|
63
56
|
def self.create(args)
|
|
64
57
|
metric_group = CompletionKit::MetricGroup.new(args.slice("name", "description"))
|
|
65
58
|
if metric_group.save
|
|
66
|
-
|
|
59
|
+
metric_group.replace_metrics!(args["metric_ids"])
|
|
67
60
|
text_result(metric_group.reload.as_json)
|
|
68
61
|
else
|
|
69
62
|
error_result(metric_group.errors.full_messages.join(", "))
|
|
@@ -73,7 +66,7 @@ module CompletionKit
|
|
|
73
66
|
def self.update(args)
|
|
74
67
|
metric_group = CompletionKit::MetricGroup.find(args["id"])
|
|
75
68
|
if metric_group.update(args.except("id", "metric_ids").slice("name", "description"))
|
|
76
|
-
|
|
69
|
+
metric_group.replace_metrics!(args["metric_ids"]) if args.key?("metric_ids")
|
|
77
70
|
text_result(metric_group.reload.as_json)
|
|
78
71
|
else
|
|
79
72
|
error_result(metric_group.errors.full_messages.join(", "))
|
|
@@ -84,23 +77,6 @@ module CompletionKit
|
|
|
84
77
|
CompletionKit::MetricGroup.find(args["id"]).destroy!
|
|
85
78
|
text_result("Metric group #{args["id"]} deleted")
|
|
86
79
|
end
|
|
87
|
-
|
|
88
|
-
def self.text_result(data)
|
|
89
|
-
text = data.is_a?(String) ? data : data.to_json
|
|
90
|
-
{content: [{type: "text", text: text}]}
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def self.error_result(message)
|
|
94
|
-
{content: [{type: "text", text: message}], isError: true}
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def self.replace_metric_memberships(metric_group, metric_ids)
|
|
98
|
-
return unless metric_ids
|
|
99
|
-
metric_group.metric_group_memberships.delete_all
|
|
100
|
-
Array(metric_ids).reject(&:blank?).each_with_index do |metric_id, index|
|
|
101
|
-
metric_group.metric_group_memberships.create!(metric_id: metric_id, position: index + 1)
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
80
|
end
|
|
105
81
|
end
|
|
106
82
|
end
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
module CompletionKit
|
|
2
2
|
module McpTools
|
|
3
3
|
module Metrics
|
|
4
|
+
extend Base
|
|
5
|
+
|
|
4
6
|
TOOLS = {
|
|
5
7
|
"metrics_list" => {
|
|
6
8
|
description: "List all metrics",
|
|
@@ -43,15 +45,6 @@ module CompletionKit
|
|
|
43
45
|
}
|
|
44
46
|
}.freeze
|
|
45
47
|
|
|
46
|
-
def self.definitions
|
|
47
|
-
TOOLS.map { |name, config| {name: name, description: config[:description], inputSchema: config[:inputSchema]} }
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def self.call(name, arguments)
|
|
51
|
-
tool = TOOLS.fetch(name)
|
|
52
|
-
send(tool[:handler], arguments)
|
|
53
|
-
end
|
|
54
|
-
|
|
55
48
|
def self.list(_args)
|
|
56
49
|
text_result(Metric.order(created_at: :desc).map(&:as_json))
|
|
57
50
|
end
|
|
@@ -82,15 +75,6 @@ module CompletionKit
|
|
|
82
75
|
Metric.find(args["id"]).destroy!
|
|
83
76
|
text_result("Metric #{args["id"]} deleted")
|
|
84
77
|
end
|
|
85
|
-
|
|
86
|
-
def self.text_result(data)
|
|
87
|
-
text = data.is_a?(String) ? data : data.to_json
|
|
88
|
-
{content: [{type: "text", text: text}]}
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
def self.error_result(message)
|
|
92
|
-
{content: [{type: "text", text: message}], isError: true}
|
|
93
|
-
end
|
|
94
78
|
end
|
|
95
79
|
end
|
|
96
80
|
end
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
module CompletionKit
|
|
2
2
|
module McpTools
|
|
3
3
|
module Prompts
|
|
4
|
+
extend Base
|
|
5
|
+
|
|
4
6
|
TOOLS = {
|
|
5
7
|
"prompts_list" => {
|
|
6
8
|
description: "List all prompts",
|
|
@@ -48,15 +50,6 @@ module CompletionKit
|
|
|
48
50
|
},
|
|
49
51
|
}.freeze
|
|
50
52
|
|
|
51
|
-
def self.definitions
|
|
52
|
-
TOOLS.map { |name, config| {name: name, description: config[:description], inputSchema: config[:inputSchema]} }
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def self.call(name, arguments)
|
|
56
|
-
tool = TOOLS.fetch(name)
|
|
57
|
-
send(tool[:handler], arguments)
|
|
58
|
-
end
|
|
59
|
-
|
|
60
53
|
def self.list(_args)
|
|
61
54
|
text_result(Prompt.order(created_at: :desc).map(&:as_json))
|
|
62
55
|
end
|
|
@@ -98,15 +91,6 @@ module CompletionKit
|
|
|
98
91
|
prompt.publish!
|
|
99
92
|
text_result(prompt.reload.as_json)
|
|
100
93
|
end
|
|
101
|
-
|
|
102
|
-
def self.text_result(data)
|
|
103
|
-
text = data.is_a?(String) ? data : data.to_json
|
|
104
|
-
{content: [{type: "text", text: text}]}
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
def self.error_result(message)
|
|
108
|
-
{content: [{type: "text", text: message}], isError: true}
|
|
109
|
-
end
|
|
110
94
|
end
|
|
111
95
|
end
|
|
112
96
|
end
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
module CompletionKit
|
|
2
2
|
module McpTools
|
|
3
3
|
module ProviderCredentials
|
|
4
|
+
extend Base
|
|
5
|
+
|
|
4
6
|
TOOLS = {
|
|
5
7
|
"provider_credentials_list" => {
|
|
6
8
|
description: "List all provider credentials (API keys are not exposed)",
|
|
@@ -44,15 +46,6 @@ module CompletionKit
|
|
|
44
46
|
}
|
|
45
47
|
}.freeze
|
|
46
48
|
|
|
47
|
-
def self.definitions
|
|
48
|
-
TOOLS.map { |name, config| {name: name, description: config[:description], inputSchema: config[:inputSchema]} }
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def self.call(name, arguments)
|
|
52
|
-
tool = TOOLS.fetch(name)
|
|
53
|
-
send(tool[:handler], arguments)
|
|
54
|
-
end
|
|
55
|
-
|
|
56
49
|
def self.list(_args)
|
|
57
50
|
text_result(ProviderCredential.order(created_at: :desc).map(&:as_json))
|
|
58
51
|
end
|
|
@@ -83,15 +76,6 @@ module CompletionKit
|
|
|
83
76
|
ProviderCredential.find(args["id"]).destroy!
|
|
84
77
|
text_result("Provider credential #{args["id"]} deleted")
|
|
85
78
|
end
|
|
86
|
-
|
|
87
|
-
def self.text_result(data)
|
|
88
|
-
text = data.is_a?(String) ? data : data.to_json
|
|
89
|
-
{content: [{type: "text", text: text}]}
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def self.error_result(message)
|
|
93
|
-
{content: [{type: "text", text: message}], isError: true}
|
|
94
|
-
end
|
|
95
79
|
end
|
|
96
80
|
end
|
|
97
81
|
end
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
module CompletionKit
|
|
2
2
|
module McpTools
|
|
3
3
|
module Responses
|
|
4
|
+
extend Base
|
|
5
|
+
|
|
4
6
|
TOOLS = {
|
|
5
7
|
"responses_list" => {
|
|
6
8
|
description: "List responses for a run",
|
|
@@ -18,15 +20,6 @@ module CompletionKit
|
|
|
18
20
|
}
|
|
19
21
|
}.freeze
|
|
20
22
|
|
|
21
|
-
def self.definitions
|
|
22
|
-
TOOLS.map { |name, config| {name: name, description: config[:description], inputSchema: config[:inputSchema]} }
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def self.call(name, arguments)
|
|
26
|
-
tool = TOOLS.fetch(name)
|
|
27
|
-
send(tool[:handler], arguments)
|
|
28
|
-
end
|
|
29
|
-
|
|
30
23
|
def self.list(args)
|
|
31
24
|
run = Run.find(args["run_id"])
|
|
32
25
|
text_result(run.responses.includes(:reviews).map(&:as_json))
|
|
@@ -36,10 +29,6 @@ module CompletionKit
|
|
|
36
29
|
run = Run.find(args["run_id"])
|
|
37
30
|
text_result(run.responses.find(args["id"]).as_json)
|
|
38
31
|
end
|
|
39
|
-
|
|
40
|
-
def self.text_result(data)
|
|
41
|
-
{content: [{type: "text", text: data.to_json}]}
|
|
42
|
-
end
|
|
43
32
|
end
|
|
44
33
|
end
|
|
45
34
|
end
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
module CompletionKit
|
|
2
2
|
module McpTools
|
|
3
3
|
module Runs
|
|
4
|
+
extend Base
|
|
5
|
+
|
|
4
6
|
TOOLS = {
|
|
5
7
|
"runs_list" => {
|
|
6
8
|
description: "List all runs",
|
|
@@ -55,15 +57,6 @@ module CompletionKit
|
|
|
55
57
|
}
|
|
56
58
|
}.freeze
|
|
57
59
|
|
|
58
|
-
def self.definitions
|
|
59
|
-
TOOLS.map { |name, config| {name: name, description: config[:description], inputSchema: config[:inputSchema]} }
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def self.call(name, arguments)
|
|
63
|
-
tool = TOOLS.fetch(name)
|
|
64
|
-
send(tool[:handler], arguments)
|
|
65
|
-
end
|
|
66
|
-
|
|
67
60
|
def self.list(_args)
|
|
68
61
|
text_result(Run.order(created_at: :desc).map(&:as_json))
|
|
69
62
|
end
|
|
@@ -75,7 +68,7 @@ module CompletionKit
|
|
|
75
68
|
def self.create(args)
|
|
76
69
|
run = Run.new(args.slice("name", "prompt_id", "dataset_id", "judge_model"))
|
|
77
70
|
if run.save
|
|
78
|
-
|
|
71
|
+
run.replace_metrics!(args["metric_ids"])
|
|
79
72
|
text_result(run.reload.as_json)
|
|
80
73
|
else
|
|
81
74
|
error_result(run.errors.full_messages.join(", "))
|
|
@@ -85,7 +78,7 @@ module CompletionKit
|
|
|
85
78
|
def self.update(args)
|
|
86
79
|
run = Run.find(args["id"])
|
|
87
80
|
if run.update(args.except("id", "metric_ids").slice("name", "dataset_id", "judge_model"))
|
|
88
|
-
|
|
81
|
+
run.replace_metrics!(args["metric_ids"]) if args.key?("metric_ids")
|
|
89
82
|
text_result(run.reload.as_json)
|
|
90
83
|
else
|
|
91
84
|
error_result(run.errors.full_messages.join(", "))
|
|
@@ -108,23 +101,6 @@ module CompletionKit
|
|
|
108
101
|
JudgeJob.perform_later(run.id)
|
|
109
102
|
text_result(run.reload.as_json)
|
|
110
103
|
end
|
|
111
|
-
|
|
112
|
-
def self.text_result(data)
|
|
113
|
-
text = data.is_a?(String) ? data : data.to_json
|
|
114
|
-
{content: [{type: "text", text: text}]}
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
def self.error_result(message)
|
|
118
|
-
{content: [{type: "text", text: message}], isError: true}
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
def self.replace_run_metrics(run, metric_ids)
|
|
122
|
-
return unless metric_ids
|
|
123
|
-
run.run_metrics.delete_all
|
|
124
|
-
Array(metric_ids).reject(&:blank?).each_with_index do |metric_id, index|
|
|
125
|
-
run.run_metrics.create!(metric_id: metric_id, position: index + 1)
|
|
126
|
-
end
|
|
127
|
-
end
|
|
128
104
|
end
|
|
129
105
|
end
|
|
130
106
|
end
|
|
@@ -3,20 +3,11 @@ module CompletionKit
|
|
|
3
3
|
def generate_completion(prompt, options = {})
|
|
4
4
|
return "Error: API endpoint not configured" unless configured?
|
|
5
5
|
|
|
6
|
-
require "faraday"
|
|
7
|
-
require "faraday/retry"
|
|
8
|
-
require "json"
|
|
9
|
-
|
|
10
6
|
model = options[:model]
|
|
11
7
|
max_tokens = options[:max_tokens] || 1000
|
|
12
8
|
temperature = options[:temperature] || 0.7
|
|
13
9
|
|
|
14
|
-
|
|
15
|
-
f.request :retry, max: 2, interval: 0.5
|
|
16
|
-
f.adapter Faraday.default_adapter
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
response = conn.post do |req|
|
|
10
|
+
response = build_connection(api_endpoint).post do |req|
|
|
20
11
|
req.url "/v1/completions"
|
|
21
12
|
req.headers["Content-Type"] = "application/json"
|
|
22
13
|
req.headers["Authorization"] = "Bearer #{api_key}" if api_key.present?
|
|
@@ -41,11 +32,7 @@ module CompletionKit
|
|
|
41
32
|
def available_models
|
|
42
33
|
return [] unless configured?
|
|
43
34
|
|
|
44
|
-
|
|
45
|
-
require "faraday/retry"
|
|
46
|
-
require "json"
|
|
47
|
-
|
|
48
|
-
response = Faraday.get("#{api_endpoint}/v1/models") do |req|
|
|
35
|
+
response = build_connection(api_endpoint).get("/v1/models") do |req|
|
|
49
36
|
req.headers["Authorization"] = "Bearer #{api_key}" if api_key.present?
|
|
50
37
|
end
|
|
51
38
|
|
|
@@ -9,20 +9,11 @@ module CompletionKit
|
|
|
9
9
|
def generate_completion(prompt, options = {})
|
|
10
10
|
return "Error: API key not configured" unless configured?
|
|
11
11
|
|
|
12
|
-
require "faraday"
|
|
13
|
-
require "faraday/retry"
|
|
14
|
-
require "json"
|
|
15
|
-
|
|
16
12
|
model = options[:model] || "gpt-4.1-mini"
|
|
17
13
|
max_tokens = options[:max_tokens] || 1000
|
|
18
14
|
temperature = options[:temperature] || 0.7
|
|
19
15
|
|
|
20
|
-
|
|
21
|
-
f.request :retry, max: 2, interval: 0.5
|
|
22
|
-
f.adapter Faraday.default_adapter
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
response = conn.post do |req|
|
|
16
|
+
response = build_connection("https://api.openai.com").post do |req|
|
|
26
17
|
req.url "/v1/responses"
|
|
27
18
|
req.headers["Content-Type"] = "application/json"
|
|
28
19
|
req.headers["Authorization"] = "Bearer #{api_key}"
|
|
@@ -7,22 +7,11 @@ module CompletionKit
|
|
|
7
7
|
def generate_completion(prompt, options = {})
|
|
8
8
|
return "Error: API key not configured" unless configured?
|
|
9
9
|
|
|
10
|
-
require "faraday"
|
|
11
|
-
require "faraday/retry"
|
|
12
|
-
require "json"
|
|
13
|
-
|
|
14
10
|
model = options[:model] || "openai/gpt-4o-mini"
|
|
15
11
|
max_tokens = options[:max_tokens] || 1000
|
|
16
12
|
temperature = options[:temperature] || 0.7
|
|
17
13
|
|
|
18
|
-
|
|
19
|
-
f.options.timeout = 30
|
|
20
|
-
f.options.open_timeout = 5
|
|
21
|
-
f.request :retry, max: 2, interval: 0.5
|
|
22
|
-
f.adapter Faraday.default_adapter
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
response = conn.post do |req|
|
|
14
|
+
response = build_connection(BASE_URL, timeout: 30, open_timeout: 5).post do |req|
|
|
26
15
|
req.url "/chat/completions"
|
|
27
16
|
req.headers["Content-Type"] = "application/json"
|
|
28
17
|
req.headers["Authorization"] = "Bearer #{api_key}"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module CompletionKit
|
|
2
|
+
class TenantScopedUniquenessValidator < ActiveRecord::Validations::UniquenessValidator
|
|
3
|
+
def validate_each(record, attribute, value)
|
|
4
|
+
extra = Array(CompletionKit.config.tenant_scope_columns)
|
|
5
|
+
return super if extra.empty? && options[:scope].nil?
|
|
6
|
+
|
|
7
|
+
merged = options.merge(
|
|
8
|
+
scope: Array(options[:scope]) + extra,
|
|
9
|
+
attributes: [attribute],
|
|
10
|
+
class: @klass
|
|
11
|
+
)
|
|
12
|
+
self.class.superclass.new(merged).validate(record)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
data/lib/completion_kit.rb
CHANGED
|
@@ -8,6 +8,7 @@ module CompletionKit
|
|
|
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
10
|
attr_accessor :username, :password, :auth_strategy, :api_token
|
|
11
|
+
attr_accessor :tenant_scope, :tenant_scope_columns
|
|
11
12
|
|
|
12
13
|
def initialize
|
|
13
14
|
@openai_api_key = ENV['OPENAI_API_KEY']
|
|
@@ -19,6 +20,10 @@ module CompletionKit
|
|
|
19
20
|
@high_quality_threshold = 4
|
|
20
21
|
@medium_quality_threshold = 3
|
|
21
22
|
end
|
|
23
|
+
|
|
24
|
+
def tenant_scope_columns
|
|
25
|
+
@tenant_scope_columns ||= []
|
|
26
|
+
end
|
|
22
27
|
end
|
|
23
28
|
|
|
24
29
|
class << self
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: completion-kit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1
|
|
4
|
+
version: 0.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Damien Bastin
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-04-
|
|
11
|
+
date: 2026-04-24 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -259,6 +259,7 @@ files:
|
|
|
259
259
|
- app/services/completion_kit/judge_service.rb
|
|
260
260
|
- app/services/completion_kit/llm_client.rb
|
|
261
261
|
- app/services/completion_kit/mcp_dispatcher.rb
|
|
262
|
+
- app/services/completion_kit/mcp_tools/base.rb
|
|
262
263
|
- app/services/completion_kit/mcp_tools/datasets.rb
|
|
263
264
|
- app/services/completion_kit/mcp_tools/metric_groups.rb
|
|
264
265
|
- app/services/completion_kit/mcp_tools/metrics.rb
|
|
@@ -271,6 +272,7 @@ files:
|
|
|
271
272
|
- app/services/completion_kit/open_ai_client.rb
|
|
272
273
|
- app/services/completion_kit/open_router_client.rb
|
|
273
274
|
- app/services/completion_kit/prompt_improvement_service.rb
|
|
275
|
+
- app/validators/completion_kit/tenant_scoped_uniqueness_validator.rb
|
|
274
276
|
- app/views/completion_kit/api_reference/_example.html.erb
|
|
275
277
|
- app/views/completion_kit/api_reference/index.html.erb
|
|
276
278
|
- app/views/completion_kit/datasets/_form.html.erb
|