completion-kit 0.26.8 → 0.27.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/app/assets/stylesheets/completion_kit/application.css +20 -8
- data/app/controllers/completion_kit/api/v1/prompts_controller.rb +11 -4
- data/app/controllers/completion_kit/prompts_controller.rb +13 -4
- data/app/jobs/completion_kit/model_discovery_job.rb +6 -1
- data/app/models/completion_kit/prompt.rb +29 -2
- data/app/models/completion_kit/provider_credential.rb +4 -0
- data/app/services/completion_kit/azure_foundry_client.rb +14 -23
- data/app/services/completion_kit/model_discovery_service.rb +18 -1
- data/app/services/completion_kit/promptfoo_importer.rb +2 -0
- data/app/views/completion_kit/provider_credentials/_models_card.html.erb +12 -5
- data/app/views/completion_kit/responses/show.html.erb +3 -1
- data/db/migrate/20260713000001_add_catalog_model_count_to_completion_kit_provider_credentials.rb +5 -0
- data/lib/completion_kit/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 38c7acaadacc7cb636ee99e3eb7e4c52f68b89ed57122487eb1591be99435716
|
|
4
|
+
data.tar.gz: 3e5f9c36019c6635ae41b913eea74607f3b8c9f21a511c5ccc3f9ff75422282d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 80adc55bde36fa3ac782c213c4786e677cd27d1b85bef81c14fbd89e05a82e229681866390c34d7f17cd9d6a153bcb93da705d63a87f7aede39dcb26929e3629
|
|
7
|
+
data.tar.gz: 533ef4f6c4b29791a66ce9169ab4c980a38838f5e3a5184d0b07cf0a4276ad0fd5990fae69616a2ea43421daebab1ca1ed2f246b4a310a506403db7548935981
|
|
@@ -961,14 +961,6 @@ tr:hover .ck-chip--publish {
|
|
|
961
961
|
max-width: 36rem;
|
|
962
962
|
}
|
|
963
963
|
|
|
964
|
-
.ck-model-list__empty {
|
|
965
|
-
display: flex;
|
|
966
|
-
align-items: center;
|
|
967
|
-
gap: 1rem;
|
|
968
|
-
flex-wrap: wrap;
|
|
969
|
-
padding: 0.35rem 0.25rem;
|
|
970
|
-
}
|
|
971
|
-
|
|
972
964
|
.ck-form-card__footer-header {
|
|
973
965
|
display: flex;
|
|
974
966
|
align-items: center;
|
|
@@ -1504,6 +1496,26 @@ tr:hover .ck-chip--publish {
|
|
|
1504
1496
|
gap: 0.6rem;
|
|
1505
1497
|
}
|
|
1506
1498
|
|
|
1499
|
+
.ck-model-list__catalog {
|
|
1500
|
+
margin-top: 0.75rem;
|
|
1501
|
+
padding-top: 0.6rem;
|
|
1502
|
+
border-top: 1px solid var(--ck-line);
|
|
1503
|
+
font-family: var(--ck-mono);
|
|
1504
|
+
font-size: 0.72rem;
|
|
1505
|
+
letter-spacing: 0.03em;
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
.ck-model-list__catalog-link {
|
|
1509
|
+
color: var(--ck-accent);
|
|
1510
|
+
font-weight: 500;
|
|
1511
|
+
text-decoration: none;
|
|
1512
|
+
white-space: nowrap;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
.ck-model-list__catalog-link:hover {
|
|
1516
|
+
color: var(--ck-accent-hover);
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1507
1519
|
.ck-model-list__summary-stamp {
|
|
1508
1520
|
font-family: var(--ck-mono);
|
|
1509
1521
|
font-size: 0.7rem;
|
|
@@ -25,10 +25,17 @@ module CompletionKit
|
|
|
25
25
|
|
|
26
26
|
def update
|
|
27
27
|
if @prompt.runs.exists?
|
|
28
|
-
new_prompt = @prompt.
|
|
29
|
-
new_prompt.
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
new_prompt = @prompt.build_next_version(prompt_params.except(:tag_names).to_h)
|
|
29
|
+
if new_prompt.valid?
|
|
30
|
+
CompletionKit::ApplicationRecord.transaction do
|
|
31
|
+
new_prompt.save!
|
|
32
|
+
new_prompt.publish!
|
|
33
|
+
new_prompt.update!(tag_names: prompt_params[:tag_names]) if prompt_params.key?(:tag_names)
|
|
34
|
+
end
|
|
35
|
+
render json: new_prompt.reload
|
|
36
|
+
else
|
|
37
|
+
render_validation_errors(new_prompt)
|
|
38
|
+
end
|
|
32
39
|
elsif @prompt.update(prompt_params)
|
|
33
40
|
render json: @prompt
|
|
34
41
|
else
|
|
@@ -33,10 +33,19 @@ module CompletionKit
|
|
|
33
33
|
|
|
34
34
|
def update
|
|
35
35
|
if @prompt.runs.exists?
|
|
36
|
-
new_prompt = @prompt.
|
|
37
|
-
new_prompt.
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
new_prompt = @prompt.build_next_version(prompt_params.except(:tag_names).to_h)
|
|
37
|
+
if new_prompt.valid?
|
|
38
|
+
CompletionKit::ApplicationRecord.transaction do
|
|
39
|
+
new_prompt.save!
|
|
40
|
+
new_prompt.publish!
|
|
41
|
+
new_prompt.update!(tag_names: prompt_params[:tag_names]) if prompt_params.key?(:tag_names)
|
|
42
|
+
end
|
|
43
|
+
redirect_to prompt_path(new_prompt), notice: "Saved as #{new_prompt.version_label}."
|
|
44
|
+
else
|
|
45
|
+
@prompt.assign_attributes(prompt_params.except(:tag_names).to_h)
|
|
46
|
+
@prompt.errors.merge!(new_prompt.errors)
|
|
47
|
+
render :edit, status: :unprocessable_entity
|
|
48
|
+
end
|
|
40
49
|
elsif @prompt.update(prompt_params)
|
|
41
50
|
redirect_to prompt_path(@prompt), notice: "Prompt saved."
|
|
42
51
|
else
|
|
@@ -48,7 +48,12 @@ module CompletionKit
|
|
|
48
48
|
credential.broadcast_discovery_progress
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
-
credential.update_columns(
|
|
51
|
+
credential.update_columns(
|
|
52
|
+
discovery_status: "completed",
|
|
53
|
+
discovery_error: nil,
|
|
54
|
+
catalog_model_count: service.catalog_model_count,
|
|
55
|
+
updated_at: Time.current
|
|
56
|
+
)
|
|
52
57
|
credential.reload
|
|
53
58
|
credential.broadcast_discovery_complete
|
|
54
59
|
end
|
|
@@ -5,9 +5,12 @@ 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
|
|
9
|
+
|
|
8
10
|
validates :name, presence: true
|
|
9
11
|
validates :template, presence: true
|
|
10
12
|
validates :llm_model, presence: true
|
|
13
|
+
validate :llm_model_usable_for_generation, if: :llm_model_newly_selected?
|
|
11
14
|
validates :family_key, presence: true
|
|
12
15
|
validates :version_number, presence: true, numericality: { only_integer: true, greater_than: 0 }
|
|
13
16
|
|
|
@@ -52,8 +55,8 @@ module CompletionKit
|
|
|
52
55
|
self.class.where(family_key: family_key).order(version_number: :desc, created_at: :desc)
|
|
53
56
|
end
|
|
54
57
|
|
|
55
|
-
def
|
|
56
|
-
self.class.
|
|
58
|
+
def build_next_version(overrides = {})
|
|
59
|
+
version = self.class.new(
|
|
57
60
|
{
|
|
58
61
|
name: name,
|
|
59
62
|
description: description,
|
|
@@ -65,6 +68,14 @@ module CompletionKit
|
|
|
65
68
|
published_at: nil
|
|
66
69
|
}.merge(overrides.compact)
|
|
67
70
|
)
|
|
71
|
+
version.cloned_from_llm_model = llm_model
|
|
72
|
+
version
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def clone_as_new_version(overrides = {})
|
|
76
|
+
version = build_next_version(overrides)
|
|
77
|
+
version.save!
|
|
78
|
+
version
|
|
68
79
|
end
|
|
69
80
|
|
|
70
81
|
def publish!
|
|
@@ -86,6 +97,22 @@ module CompletionKit
|
|
|
86
97
|
|
|
87
98
|
private
|
|
88
99
|
|
|
100
|
+
def llm_model_newly_selected?
|
|
101
|
+
return false if llm_model.blank?
|
|
102
|
+
return llm_model != cloned_from_llm_model if new_record? && cloned_from_llm_model.present?
|
|
103
|
+
|
|
104
|
+
llm_model_changed?
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def llm_model_usable_for_generation
|
|
108
|
+
rows = Model.where(model_id: llm_model)
|
|
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?
|
|
112
|
+
|
|
113
|
+
errors.add(:llm_model, "is not available for generating responses")
|
|
114
|
+
end
|
|
115
|
+
|
|
89
116
|
def assign_family_key
|
|
90
117
|
self.family_key ||= SecureRandom.uuid
|
|
91
118
|
end
|
|
@@ -61,6 +61,10 @@ module CompletionKit
|
|
|
61
61
|
Model.where(provider: provider).active.count
|
|
62
62
|
end
|
|
63
63
|
|
|
64
|
+
def foundry_catalog_available?
|
|
65
|
+
azure_foundry? && catalog_model_count.to_i.positive?
|
|
66
|
+
end
|
|
67
|
+
|
|
64
68
|
def self.discovery_in_progress?
|
|
65
69
|
where(discovery_status: "discovering").exists?
|
|
66
70
|
end
|
|
@@ -58,16 +58,26 @@ module CompletionKit
|
|
|
58
58
|
return [] unless configured?
|
|
59
59
|
return [] unless ProviderEndpoint.safe?(api_endpoint)
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
path = foundry_project? ? "#{azure_base_url}/deployments?api-version=v1" : models_path
|
|
62
|
+
response = build_connection(azure_base_url).get(path) do |req|
|
|
62
63
|
req.headers["api-key"] = api_key
|
|
63
64
|
end
|
|
64
65
|
return [] unless response.success?
|
|
65
66
|
|
|
66
|
-
|
|
67
|
+
body = JSON.parse(response.body)
|
|
68
|
+
if foundry_project?
|
|
69
|
+
body.fetch("value", []).map { |d| { id: d["name"], name: d["name"] } }
|
|
70
|
+
else
|
|
71
|
+
body.fetch("data", []).map { |entry| { id: entry["id"], name: entry["id"] } }
|
|
72
|
+
end
|
|
67
73
|
rescue StandardError
|
|
68
74
|
[]
|
|
69
75
|
end
|
|
70
76
|
|
|
77
|
+
def foundry_project?
|
|
78
|
+
api_endpoint.to_s.include?("/api/projects/")
|
|
79
|
+
end
|
|
80
|
+
|
|
71
81
|
def configured?
|
|
72
82
|
configuration_errors.empty?
|
|
73
83
|
end
|
|
@@ -101,27 +111,8 @@ module CompletionKit
|
|
|
101
111
|
api_version.blank?
|
|
102
112
|
end
|
|
103
113
|
|
|
104
|
-
def
|
|
105
|
-
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
def models_url
|
|
109
|
-
if foundry_project?
|
|
110
|
-
"#{azure_base_url}/deployments?api-version=v1"
|
|
111
|
-
elsif v1_mode?
|
|
112
|
-
"/openai/v1/models"
|
|
113
|
-
else
|
|
114
|
-
"/openai/deployments?api-version=#{api_version}"
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
def parse_models(body)
|
|
119
|
-
json = JSON.parse(body)
|
|
120
|
-
if foundry_project?
|
|
121
|
-
json.fetch("value", []).map { |d| { id: d["name"], name: d["name"] } }
|
|
122
|
-
else
|
|
123
|
-
json.fetch("data", []).map { |e| { id: e["id"], name: e["id"] } }
|
|
124
|
-
end
|
|
114
|
+
def models_path
|
|
115
|
+
v1_mode? ? "/openai/v1/models" : "/openai/deployments?api-version=#{api_version}"
|
|
125
116
|
end
|
|
126
117
|
|
|
127
118
|
def post_chat(model:, prompt:, max_tokens:, temperature:, max_completion: false)
|
|
@@ -8,11 +8,14 @@ module CompletionKit
|
|
|
8
8
|
|
|
9
9
|
AZURE_HOST_SUFFIXES = [".openai.azure.com", ".services.ai.azure.com"].freeze
|
|
10
10
|
|
|
11
|
+
attr_reader :catalog_model_count
|
|
12
|
+
|
|
11
13
|
def initialize(config:)
|
|
12
14
|
@provider = config[:provider]
|
|
13
15
|
@api_key = config[:api_key]
|
|
14
16
|
@api_endpoint = config[:api_endpoint]
|
|
15
17
|
@api_version = config[:api_version]
|
|
18
|
+
@catalog_model_count = nil
|
|
16
19
|
end
|
|
17
20
|
|
|
18
21
|
def refresh!(force: false, &on_progress)
|
|
@@ -156,18 +159,32 @@ module CompletionKit
|
|
|
156
159
|
def fetch_azure_foundry_models
|
|
157
160
|
raise DiscoveryError, "An Azure endpoint URL is required." if @api_endpoint.blank?
|
|
158
161
|
|
|
159
|
-
|
|
162
|
+
path = azure_foundry_project? ? "#{azure_base_url}/deployments?api-version=v1" : azure_models_path
|
|
163
|
+
response = fetch_connection(azure_base_url).get(path) do |req|
|
|
160
164
|
req.headers["api-key"] = @api_key
|
|
161
165
|
end
|
|
162
166
|
raise DiscoveryError, azure_error_message(response) unless response.success?
|
|
167
|
+
|
|
163
168
|
body = JSON.parse(response.body)
|
|
164
169
|
if azure_foundry_project?
|
|
170
|
+
@catalog_model_count = fetch_azure_catalog_count
|
|
165
171
|
body.fetch("value", []).map { |d| { id: d["name"], display_name: d["name"] } }
|
|
166
172
|
else
|
|
167
173
|
body.fetch("data", []).map { |e| { id: e["id"], display_name: e["id"] } }
|
|
168
174
|
end
|
|
169
175
|
end
|
|
170
176
|
|
|
177
|
+
def fetch_azure_catalog_count
|
|
178
|
+
response = fetch_connection(azure_base_url).get("/openai/v1/models") do |req|
|
|
179
|
+
req.headers["api-key"] = @api_key
|
|
180
|
+
end
|
|
181
|
+
return nil unless response.success?
|
|
182
|
+
|
|
183
|
+
JSON.parse(response.body).fetch("data", []).length
|
|
184
|
+
rescue StandardError
|
|
185
|
+
nil
|
|
186
|
+
end
|
|
187
|
+
|
|
171
188
|
def azure_v1_mode?
|
|
172
189
|
@api_version.blank?
|
|
173
190
|
end
|
|
@@ -27,6 +27,8 @@ module CompletionKit
|
|
|
27
27
|
metrics = import_metrics(config)
|
|
28
28
|
Result.new(ok: true, error: nil, prompts: prompts, dataset: dataset, metrics: metrics, providers: providers)
|
|
29
29
|
end
|
|
30
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
31
|
+
failure(e.record.errors.full_messages.to_sentence.presence || e.message)
|
|
30
32
|
end
|
|
31
33
|
|
|
32
34
|
private
|
|
@@ -71,12 +71,19 @@
|
|
|
71
71
|
</div>
|
|
72
72
|
</details>
|
|
73
73
|
<% elsif provider_credential.discovery_status != "discovering" %>
|
|
74
|
-
<div class="ck-model-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
74
|
+
<div class="ck-model-list__summary">
|
|
75
|
+
<span class="ck-model-list__summary-label">Available models <span class="ck-model-list__summary-count">0</span></span>
|
|
76
|
+
<span class="ck-model-list__summary-meta">
|
|
77
|
+
<button type="button" class="ck-icon-btn ck-model-list__refresh" title="Refresh models" aria-label="Refresh available models" onclick="event.preventDefault();fetch('<%= refresh_provider_credential_path(provider_credential) %>', {method:'POST',headers:{'X-CSRF-Token':document.querySelector('meta[name=csrf-token]').content}});if(window.ckStartDiscoveryPolling)ckStartDiscoveryPolling(8000)"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" width="13" height="13" aria-hidden="true"><path fill-rule="evenodd" d="M13.836 2.477a.75.75 0 0 1 .75.75v3.182a.75.75 0 0 1-.75.75h-3.182a.75.75 0 0 1 0-1.5h1.37l-.84-.841a4.5 4.5 0 0 0-7.08.681.75.75 0 0 1-1.264-.808 6 6 0 0 1 9.44-.908l.84.84V3.227a.75.75 0 0 1 .75-.75Zm-.911 7.5A.75.75 0 0 1 13.199 11a6 6 0 0 1-9.44.908l-.84-.84v1.68a.75.75 0 0 1-1.5 0V9.567a.75.75 0 0 1 .75-.75h3.182a.75.75 0 0 1 0 1.5h-1.37l.84.841a4.5 4.5 0 0 0 7.08-.681.75.75 0 0 1 1.024-.274Z" clip-rule="evenodd"/></svg></button>
|
|
78
|
+
</span>
|
|
79
79
|
</div>
|
|
80
80
|
<% end %>
|
|
81
|
+
|
|
82
|
+
<% if provider_credential.foundry_catalog_available? %>
|
|
83
|
+
<p class="ck-meta-copy ck-model-list__catalog">
|
|
84
|
+
<%= pluralize(provider_credential.catalog_model_count, "model") %> available to deploy in
|
|
85
|
+
<a href="https://ai.azure.com/" target="_blank" rel="noopener" class="ck-model-list__catalog-link">Azure AI Foundry <span aria-hidden="true">→</span></a>
|
|
86
|
+
</p>
|
|
87
|
+
<% end %>
|
|
81
88
|
</div>
|
|
82
89
|
</div>
|
|
@@ -79,7 +79,9 @@
|
|
|
79
79
|
</div>
|
|
80
80
|
<% if @response.status == "failed" %>
|
|
81
81
|
<% err = @response.error_payload || {} %>
|
|
82
|
-
|
|
82
|
+
<% head = [err[:provider]&.titleize, err[:status].presence].compact.join(" ").presence %>
|
|
83
|
+
<% error_text = [head, @response.error_message.presence].compact.join("\n\n").presence || "The provider returned no error detail." %>
|
|
84
|
+
<div class="ck-code-scroll-wrap"><pre class="ck-code ck-code--error"><%= error_text %></pre></div>
|
|
83
85
|
<% else %>
|
|
84
86
|
<div class="ck-code-scroll-wrap"><pre class="ck-code"><%= ck_format_maybe_json(@response.response_text) %></pre></div>
|
|
85
87
|
<% end %>
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: completion-kit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.27.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Damien Bastin
|
|
@@ -479,6 +479,7 @@ files:
|
|
|
479
479
|
- db/migrate/20260629000003_add_passed_to_completion_kit_reviews.rb
|
|
480
480
|
- db/migrate/20260706000001_add_expected_column_to_completion_kit_runs.rb
|
|
481
481
|
- db/migrate/20260708000001_add_api_version_to_completion_kit_provider_credentials.rb
|
|
482
|
+
- db/migrate/20260713000001_add_catalog_model_count_to_completion_kit_provider_credentials.rb
|
|
482
483
|
- lib/completion-kit.rb
|
|
483
484
|
- lib/completion_kit.rb
|
|
484
485
|
- lib/completion_kit/concurrency_check.rb
|