completion-kit 0.4.8 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -0
  3. data/app/assets/stylesheets/completion_kit/application.css +375 -0
  4. data/app/controllers/completion_kit/api/v1/datasets_controller.rb +2 -2
  5. data/app/controllers/completion_kit/api/v1/metric_groups_controller.rb +2 -2
  6. data/app/controllers/completion_kit/api/v1/metrics_controller.rb +3 -2
  7. data/app/controllers/completion_kit/api/v1/prompts_controller.rb +5 -4
  8. data/app/controllers/completion_kit/api/v1/runs_controller.rb +3 -2
  9. data/app/controllers/completion_kit/api/v1/tags_controller.rb +51 -0
  10. data/app/controllers/completion_kit/datasets_controller.rb +3 -2
  11. data/app/controllers/completion_kit/metric_groups_controller.rb +7 -6
  12. data/app/controllers/completion_kit/metrics_controller.rb +4 -2
  13. data/app/controllers/completion_kit/prompts_controller.rb +7 -4
  14. data/app/controllers/completion_kit/runs_controller.rb +4 -3
  15. data/app/controllers/completion_kit/tags_controller.rb +50 -0
  16. data/app/controllers/concerns/completion_kit/tag_filtering.rb +22 -0
  17. data/app/helpers/completion_kit/application_helper.rb +11 -0
  18. data/app/models/completion_kit/dataset.rb +5 -2
  19. data/app/models/completion_kit/metric.rb +4 -1
  20. data/app/models/completion_kit/metric_group.rb +4 -1
  21. data/app/models/completion_kit/prompt.rb +4 -1
  22. data/app/models/completion_kit/run.rb +3 -1
  23. data/app/models/completion_kit/tag.rb +39 -0
  24. data/app/models/completion_kit/tagging.rb +12 -0
  25. data/app/models/concerns/completion_kit/taggable.rb +24 -0
  26. data/app/services/completion_kit/mcp_dispatcher.rb +3 -1
  27. data/app/services/completion_kit/mcp_tools/datasets.rb +6 -4
  28. data/app/services/completion_kit/mcp_tools/metric_groups.rb +6 -2
  29. data/app/services/completion_kit/mcp_tools/metrics.rb +8 -4
  30. data/app/services/completion_kit/mcp_tools/prompts.rb +10 -5
  31. data/app/services/completion_kit/mcp_tools/runs.rb +7 -3
  32. data/app/services/completion_kit/mcp_tools/tags.rb +74 -0
  33. data/app/views/completion_kit/api_reference/index.html.erb +38 -0
  34. data/app/views/completion_kit/datasets/_form.html.erb +20 -1
  35. data/app/views/completion_kit/datasets/index.html.erb +17 -1
  36. data/app/views/completion_kit/datasets/show.html.erb +6 -0
  37. data/app/views/completion_kit/metric_groups/_form.html.erb +74 -19
  38. data/app/views/completion_kit/metric_groups/index.html.erb +30 -4
  39. data/app/views/completion_kit/metrics/_form.html.erb +19 -1
  40. data/app/views/completion_kit/metrics/index.html.erb +18 -2
  41. data/app/views/completion_kit/metrics/show.html.erb +6 -0
  42. data/app/views/completion_kit/prompts/_form.html.erb +20 -1
  43. data/app/views/completion_kit/prompts/index.html.erb +17 -1
  44. data/app/views/completion_kit/prompts/show.html.erb +6 -0
  45. data/app/views/completion_kit/provider_credentials/_form.html.erb +1 -1
  46. data/app/views/completion_kit/provider_credentials/index.html.erb +3 -1
  47. data/app/views/completion_kit/runs/_form.html.erb +25 -3
  48. data/app/views/completion_kit/runs/_row.html.erb +5 -0
  49. data/app/views/completion_kit/runs/index.html.erb +9 -0
  50. data/app/views/completion_kit/runs/show.html.erb +6 -0
  51. data/app/views/completion_kit/shared/_settings_nav.html.erb +9 -0
  52. data/app/views/completion_kit/tags/_filter_bar.html.erb +15 -0
  53. data/app/views/completion_kit/tags/_form.html.erb +39 -0
  54. data/app/views/completion_kit/tags/_marks.html.erb +3 -0
  55. data/app/views/completion_kit/tags/_picker.html.erb +20 -0
  56. data/app/views/completion_kit/tags/edit.html.erb +20 -0
  57. data/app/views/completion_kit/tags/index.html.erb +45 -0
  58. data/app/views/completion_kit/tags/new.html.erb +20 -0
  59. data/app/views/layouts/completion_kit/application.html.erb +38 -1
  60. data/config/routes.rb +2 -0
  61. data/db/migrate/20260509000001_create_completion_kit_tags.rb +10 -0
  62. data/db/migrate/20260509000002_create_completion_kit_taggings.rb +16 -0
  63. data/lib/completion_kit/version.rb +1 -1
  64. metadata +18 -1
@@ -1,9 +1,10 @@
1
1
  module CompletionKit
2
2
  class PromptsController < ApplicationController
3
+ include CompletionKit::TagFiltering
3
4
  before_action :set_prompt, only: [:show, :edit, :update, :destroy, :publish]
4
-
5
+
5
6
  def index
6
- @prompts = Prompt.current_versions.includes(:runs).order(created_at: :desc)
7
+ @prompts = apply_tag_filter(Prompt.current_versions.includes(:runs, :tags).order(created_at: :desc))
7
8
  end
8
9
 
9
10
  def show
@@ -31,8 +32,9 @@ module CompletionKit
31
32
 
32
33
  def update
33
34
  if @prompt.runs.exists?
34
- new_prompt = @prompt.clone_as_new_version(prompt_params.to_h)
35
+ new_prompt = @prompt.clone_as_new_version(prompt_params.except(:tag_names).to_h)
35
36
  new_prompt.publish!
37
+ new_prompt.update!(tag_names: prompt_params[:tag_names]) if prompt_params.key?(:tag_names)
36
38
  redirect_to prompt_path(new_prompt), notice: "Saved as #{new_prompt.version_label}."
37
39
  elsif @prompt.update(prompt_params)
38
40
  redirect_to prompt_path(@prompt), notice: "Prompt saved."
@@ -62,7 +64,8 @@ module CompletionKit
62
64
  :name,
63
65
  :description,
64
66
  :template,
65
- :llm_model
67
+ :llm_model,
68
+ tag_names: []
66
69
  )
67
70
  end
68
71
  end
@@ -1,10 +1,11 @@
1
1
  module CompletionKit
2
2
  class RunsController < ApplicationController
3
+ include CompletionKit::TagFiltering
3
4
  before_action :set_run, only: [:show, :edit, :update, :destroy, :generate, :suggest, :retry_failures, :rerun, :refresh_status]
4
5
  before_action :load_form_collections, only: [:new, :edit, :create, :update]
5
6
 
6
7
  def index
7
- @runs = Run.includes(:prompt, :dataset, responses: :reviews).order(created_at: :desc)
8
+ @runs = apply_tag_filter(Run.includes(:prompt, :dataset, :tags, responses: :reviews).order(created_at: :desc))
8
9
  end
9
10
 
10
11
  def show
@@ -148,11 +149,11 @@ module CompletionKit
148
149
  @prompts = Prompt.order(:name)
149
150
  @datasets = Dataset.order(:name)
150
151
  @metric_groups = MetricGroup.includes(:metrics).order(:name)
151
- @all_metrics = Metric.order(:name)
152
+ @all_metrics = Metric.includes(:tags).order(:name)
152
153
  end
153
154
 
154
155
  def run_params
155
- params.require(:run).permit(:name, :prompt_id, :dataset_id, :judge_model, :temperature, metric_ids: [])
156
+ params.require(:run).permit(:name, :prompt_id, :dataset_id, :judge_model, :temperature, metric_ids: [], tag_names: [])
156
157
  end
157
158
 
158
159
  end
@@ -0,0 +1,50 @@
1
+ module CompletionKit
2
+ class TagsController < ApplicationController
3
+ before_action :set_tag, only: [:edit, :update, :destroy]
4
+
5
+ def index
6
+ @tags = Tag.order(:name)
7
+ @tagging_counts = Tagging.group(:tag_id).count
8
+ @tagging_by_type = Tagging.group(:tag_id, :taggable_type).count
9
+ end
10
+
11
+ def new
12
+ @tag = Tag.new(color: Tag::COLORS.sample)
13
+ end
14
+
15
+ def edit
16
+ end
17
+
18
+ def create
19
+ @tag = Tag.new(tag_params)
20
+ if @tag.save
21
+ redirect_to tags_path, notice: "Tag was successfully created."
22
+ else
23
+ render :new, status: :unprocessable_entity
24
+ end
25
+ end
26
+
27
+ def update
28
+ if @tag.update(tag_params)
29
+ redirect_to tags_path, notice: "Tag was successfully updated."
30
+ else
31
+ render :edit, status: :unprocessable_entity
32
+ end
33
+ end
34
+
35
+ def destroy
36
+ @tag.destroy
37
+ redirect_to tags_path, notice: "Tag was successfully destroyed."
38
+ end
39
+
40
+ private
41
+
42
+ def set_tag
43
+ @tag = Tag.find(params[:id])
44
+ end
45
+
46
+ def tag_params
47
+ params.require(:tag).permit(:name)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,22 @@
1
+ module CompletionKit
2
+ module TagFiltering
3
+ extend ActiveSupport::Concern
4
+
5
+ private
6
+
7
+ def apply_tag_filter(scope)
8
+ @available_tags = CompletionKit::Tag.order(:name)
9
+ @selected_tags = filter_tags_from_params
10
+ return scope if @selected_tags.empty?
11
+ scope.joins(:tags).where(tags: { id: @selected_tags.map(&:id) }).distinct
12
+ end
13
+
14
+ def filter_tags_from_params
15
+ names = Array(params[:tag])
16
+ .map { |n| n.to_s.strip.downcase }
17
+ .reject(&:blank?)
18
+ return [] if names.empty?
19
+ CompletionKit::Tag.where(name: names).to_a
20
+ end
21
+ end
22
+ end
@@ -116,6 +116,17 @@ module CompletionKit
116
116
  diff_tokens(old_text, new_text, :new)
117
117
  end
118
118
 
119
+ def tag_pill_class(tag, outline: false)
120
+ ["tag", "tag-#{tag.color}", ("tag-outline" if outline)].compact.join(" ")
121
+ end
122
+
123
+ def tag_filter_url(base_path, selected, toggling)
124
+ remaining = selected.reject { |t| t.id == toggling.id }
125
+ next_set = selected.include?(toggling) ? remaining : remaining + [toggling]
126
+ return base_path if next_set.empty?
127
+ "#{base_path}?#{{ tag: next_set.map(&:name) }.to_query}"
128
+ end
129
+
119
130
  private
120
131
 
121
132
  def diff_tokens(old_text, new_text, side)
@@ -1,6 +1,8 @@
1
1
  module CompletionKit
2
2
  class Dataset < ApplicationRecord
3
- has_many :runs, dependent: :restrict_with_error
3
+ include CompletionKit::Taggable
4
+
5
+ has_many :runs, dependent: :destroy
4
6
 
5
7
  validates :name, presence: true
6
8
  validates :csv_data, presence: true
@@ -8,7 +10,8 @@ module CompletionKit
8
10
  def as_json(options = {})
9
11
  {
10
12
  id: id, name: name, csv_data: csv_data,
11
- created_at: created_at, updated_at: updated_at
13
+ created_at: created_at, updated_at: updated_at,
14
+ tags: tags.as_json
12
15
  }
13
16
  end
14
17
 
@@ -1,5 +1,7 @@
1
1
  module CompletionKit
2
2
  class Metric < ApplicationRecord
3
+ include CompletionKit::Taggable
4
+
3
5
  DEFAULT_RUBRIC_BANDS = [
4
6
  { "stars" => 5, "description" => "Fully meets or exceeds all criteria. No meaningful issues." },
5
7
  { "stars" => 4, "description" => "Meets criteria well. Minor issues only." },
@@ -74,7 +76,8 @@ module CompletionKit
74
76
  {
75
77
  id: id, name: name, key: key, instruction: instruction,
76
78
  rubric_bands: rubric_bands,
77
- created_at: created_at, updated_at: updated_at
79
+ created_at: created_at, updated_at: updated_at,
80
+ tags: tags.as_json
78
81
  }
79
82
  end
80
83
 
@@ -1,5 +1,7 @@
1
1
  module CompletionKit
2
2
  class MetricGroup < ApplicationRecord
3
+ include CompletionKit::Taggable
4
+
3
5
  self.table_name = "completion_kit_metric_groups"
4
6
 
5
7
  has_many :metric_group_memberships, -> { order(:position, :id) }, dependent: :destroy
@@ -23,7 +25,8 @@ module CompletionKit
23
25
  {
24
26
  id: id, name: name, description: description,
25
27
  created_at: created_at, updated_at: updated_at,
26
- metric_ids: metric_ids
28
+ metric_ids: metric_ids,
29
+ tags: tags.as_json
27
30
  }
28
31
  end
29
32
  end
@@ -1,5 +1,7 @@
1
1
  module CompletionKit
2
2
  class Prompt < ApplicationRecord
3
+ include CompletionKit::Taggable
4
+
3
5
  has_many :runs, dependent: :destroy
4
6
  has_many :responses, through: :runs
5
7
 
@@ -77,7 +79,8 @@ module CompletionKit
77
79
  {
78
80
  id: id, name: name, description: description, template: template,
79
81
  llm_model: llm_model, family_key: family_key, version_number: version_number,
80
- current: current, created_at: created_at, updated_at: updated_at
82
+ current: current, created_at: created_at, updated_at: updated_at,
83
+ tags: tags.as_json
81
84
  }
82
85
  end
83
86
 
@@ -1,6 +1,7 @@
1
1
  module CompletionKit
2
2
  class Run < ApplicationRecord
3
3
  include Turbo::Broadcastable
4
+ include CompletionKit::Taggable
4
5
 
5
6
  STATUSES = %w[pending running completed failed].freeze
6
7
 
@@ -178,7 +179,8 @@ module CompletionKit
178
179
  failed_response_ids: responses.where(status: "failed").pluck(:id),
179
180
  failure_summary: failure_summary,
180
181
  error_message: error_message,
181
- metric_ids: metric_ids
182
+ metric_ids: metric_ids,
183
+ tags: tags.as_json
182
184
  }
183
185
  end
184
186
 
@@ -0,0 +1,39 @@
1
+ module CompletionKit
2
+ class Tag < ApplicationRecord
3
+ self.table_name = "completion_kit_tags"
4
+
5
+ COLORS = %w[
6
+ crimson burnt-orange amber mint deep-emerald
7
+ electric-cyan cobalt-blue deep-indigo amethyst rose
8
+ ].freeze
9
+
10
+ has_many :taggings, dependent: :destroy
11
+
12
+ before_validation :normalize_name
13
+ before_validation :assign_color, on: :create
14
+
15
+ validates :name, presence: true,
16
+ length: { maximum: 64 },
17
+ format: { with: /\A[\w\s\-]+\z/ },
18
+ tenant_scoped_uniqueness: true
19
+ validates :color, inclusion: { in: COLORS }
20
+
21
+ def as_json(options = {})
22
+ {
23
+ id: id, name: name, color: color,
24
+ created_at: created_at, updated_at: updated_at
25
+ }
26
+ end
27
+
28
+ private
29
+
30
+ def normalize_name
31
+ self.name = name.to_s.strip.downcase if name.present?
32
+ end
33
+
34
+ def assign_color
35
+ return if color.present?
36
+ self.color = COLORS[CompletionKit::Tag.count % COLORS.size]
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,12 @@
1
+ module CompletionKit
2
+ class Tagging < ApplicationRecord
3
+ self.table_name = "completion_kit_taggings"
4
+
5
+ belongs_to :tag, class_name: "CompletionKit::Tag"
6
+ belongs_to :taggable, polymorphic: true
7
+
8
+ validates :tag, presence: true
9
+ validates :taggable, presence: true
10
+ validates :tag_id, uniqueness: { scope: [:taggable_type, :taggable_id] }
11
+ end
12
+ end
@@ -0,0 +1,24 @@
1
+ module CompletionKit
2
+ module Taggable
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ has_many :taggings, as: :taggable,
7
+ class_name: "CompletionKit::Tagging",
8
+ dependent: :destroy
9
+ has_many :tags, through: :taggings, class_name: "CompletionKit::Tag"
10
+ end
11
+
12
+ def tag_names
13
+ tags.pluck(:name)
14
+ end
15
+
16
+ def tag_names=(names)
17
+ resolved = Array(names)
18
+ .map { |n| n.to_s.strip.downcase }
19
+ .reject(&:blank?)
20
+ .uniq
21
+ self.tags = resolved.map { |name| CompletionKit::Tag.find_or_create_by!(name: name) }
22
+ end
23
+ end
24
+ end
@@ -34,7 +34,8 @@ module CompletionKit
34
34
  McpTools::Datasets.definitions +
35
35
  McpTools::Metrics.definitions +
36
36
  McpTools::MetricGroups.definitions +
37
- McpTools::ProviderCredentials.definitions
37
+ McpTools::ProviderCredentials.definitions +
38
+ McpTools::Tags.definitions
38
39
  end
39
40
 
40
41
  def self.call_tool(name, arguments)
@@ -46,6 +47,7 @@ module CompletionKit
46
47
  when /\Ametrics_/ then McpTools::Metrics.call(name, arguments)
47
48
  when /\Ametric_groups_/ then McpTools::MetricGroups.call(name, arguments)
48
49
  when /\Aprovider_credentials_/ then McpTools::ProviderCredentials.call(name, arguments)
50
+ when /\Atags_/ then McpTools::Tags.call(name, arguments)
49
51
  else raise MethodNotFound, "Unknown tool: #{name}"
50
52
  end
51
53
  end
@@ -18,7 +18,7 @@ module CompletionKit
18
18
  description: "Create a dataset with CSV data",
19
19
  inputSchema: {
20
20
  type: "object",
21
- properties: {name: {type: "string"}, csv_data: {type: "string"}},
21
+ properties: {name: {type: "string"}, csv_data: {type: "string"}, tag_names: {type: "array", items: {type: "string"}}},
22
22
  required: ["name", "csv_data"]
23
23
  },
24
24
  handler: :create
@@ -27,7 +27,7 @@ module CompletionKit
27
27
  description: "Update a dataset",
28
28
  inputSchema: {
29
29
  type: "object",
30
- properties: {id: {type: "integer"}, name: {type: "string"}, csv_data: {type: "string"}},
30
+ properties: {id: {type: "integer"}, name: {type: "string"}, csv_data: {type: "string"}, tag_names: {type: "array", items: {type: "string"}}},
31
31
  required: ["id"]
32
32
  },
33
33
  handler: :update
@@ -49,8 +49,9 @@ module CompletionKit
49
49
 
50
50
  def self.create(args)
51
51
  dataset = Dataset.new(args.slice("name", "csv_data"))
52
+ dataset.tag_names = args["tag_names"] if args.key?("tag_names")
52
53
  if dataset.save
53
- text_result(dataset.as_json)
54
+ text_result(dataset.reload.as_json)
54
55
  else
55
56
  error_result(dataset.errors.full_messages.join(", "))
56
57
  end
@@ -59,7 +60,8 @@ module CompletionKit
59
60
  def self.update(args)
60
61
  dataset = Dataset.find(args["id"])
61
62
  if dataset.update(args.except("id").slice("name", "csv_data"))
62
- text_result(dataset.as_json)
63
+ dataset.update!(tag_names: args["tag_names"]) if args.key?("tag_names")
64
+ text_result(dataset.reload.as_json)
63
65
  else
64
66
  error_result(dataset.errors.full_messages.join(", "))
65
67
  end
@@ -20,7 +20,8 @@ module CompletionKit
20
20
  type: "object",
21
21
  properties: {
22
22
  name: {type: "string"}, description: {type: "string"},
23
- metric_ids: {type: "array", items: {type: "integer"}}
23
+ metric_ids: {type: "array", items: {type: "integer"}},
24
+ tag_names: {type: "array", items: {type: "string"}}
24
25
  },
25
26
  required: ["name"]
26
27
  },
@@ -32,7 +33,8 @@ module CompletionKit
32
33
  type: "object",
33
34
  properties: {
34
35
  id: {type: "integer"}, name: {type: "string"}, description: {type: "string"},
35
- metric_ids: {type: "array", items: {type: "integer"}}
36
+ metric_ids: {type: "array", items: {type: "integer"}},
37
+ tag_names: {type: "array", items: {type: "string"}}
36
38
  },
37
39
  required: ["id"]
38
40
  },
@@ -55,6 +57,7 @@ module CompletionKit
55
57
 
56
58
  def self.create(args)
57
59
  metric_group = CompletionKit::MetricGroup.new(args.slice("name", "description"))
60
+ metric_group.tag_names = args["tag_names"] if args.key?("tag_names")
58
61
  if metric_group.save
59
62
  metric_group.replace_metrics!(args["metric_ids"])
60
63
  text_result(metric_group.reload.as_json)
@@ -67,6 +70,7 @@ module CompletionKit
67
70
  metric_group = CompletionKit::MetricGroup.find(args["id"])
68
71
  if metric_group.update(args.except("id", "metric_ids").slice("name", "description"))
69
72
  metric_group.replace_metrics!(args["metric_ids"]) if args.key?("metric_ids")
73
+ metric_group.update!(tag_names: args["tag_names"]) if args.key?("tag_names")
70
74
  text_result(metric_group.reload.as_json)
71
75
  else
72
76
  error_result(metric_group.errors.full_messages.join(", "))
@@ -20,7 +20,8 @@ module CompletionKit
20
20
  type: "object",
21
21
  properties: {
22
22
  name: {type: "string"}, instruction: {type: "string"},
23
- rubric_bands: {type: "array", items: {type: "object", properties: {stars: {type: "integer"}, description: {type: "string"}}}}
23
+ rubric_bands: {type: "array", items: {type: "object", properties: {stars: {type: "integer"}, description: {type: "string"}}}},
24
+ tag_names: {type: "array", items: {type: "string"}}
24
25
  },
25
26
  required: ["name"]
26
27
  },
@@ -32,7 +33,8 @@ module CompletionKit
32
33
  type: "object",
33
34
  properties: {
34
35
  id: {type: "integer"}, name: {type: "string"}, instruction: {type: "string"},
35
- rubric_bands: {type: "array", items: {type: "object", properties: {stars: {type: "integer"}, description: {type: "string"}}}}
36
+ rubric_bands: {type: "array", items: {type: "object", properties: {stars: {type: "integer"}, description: {type: "string"}}}},
37
+ tag_names: {type: "array", items: {type: "string"}}
36
38
  },
37
39
  required: ["id"]
38
40
  },
@@ -55,8 +57,9 @@ module CompletionKit
55
57
 
56
58
  def self.create(args)
57
59
  metric = Metric.new(args.slice("name", "instruction", "rubric_bands"))
60
+ metric.tag_names = args["tag_names"] if args.key?("tag_names")
58
61
  if metric.save
59
- text_result(metric.as_json)
62
+ text_result(metric.reload.as_json)
60
63
  else
61
64
  error_result(metric.errors.full_messages.join(", "))
62
65
  end
@@ -65,7 +68,8 @@ module CompletionKit
65
68
  def self.update(args)
66
69
  metric = Metric.find(args["id"])
67
70
  if metric.update(args.except("id").slice("name", "instruction", "rubric_bands"))
68
- text_result(metric.as_json)
71
+ metric.update!(tag_names: args["tag_names"]) if args.key?("tag_names")
72
+ text_result(metric.reload.as_json)
69
73
  else
70
74
  error_result(metric.errors.full_messages.join(", "))
71
75
  end
@@ -20,7 +20,8 @@ module CompletionKit
20
20
  type: "object",
21
21
  properties: {
22
22
  name: {type: "string"}, description: {type: "string"},
23
- template: {type: "string"}, llm_model: {type: "string"}
23
+ template: {type: "string"}, llm_model: {type: "string"},
24
+ tag_names: {type: "array", items: {type: "string"}}
24
25
  },
25
26
  required: ["name", "template", "llm_model"]
26
27
  },
@@ -32,7 +33,8 @@ module CompletionKit
32
33
  type: "object",
33
34
  properties: {
34
35
  id: {type: "integer"}, name: {type: "string"}, description: {type: "string"},
35
- template: {type: "string"}, llm_model: {type: "string"}
36
+ template: {type: "string"}, llm_model: {type: "string"},
37
+ tag_names: {type: "array", items: {type: "string"}}
36
38
  },
37
39
  required: ["id"]
38
40
  },
@@ -60,8 +62,9 @@ module CompletionKit
60
62
 
61
63
  def self.create(args)
62
64
  prompt = Prompt.new(args.slice("name", "description", "template", "llm_model"))
65
+ prompt.tag_names = args["tag_names"] if args.key?("tag_names")
63
66
  if prompt.save
64
- text_result(prompt.as_json)
67
+ text_result(prompt.reload.as_json)
65
68
  else
66
69
  error_result(prompt.errors.full_messages.join(", "))
67
70
  end
@@ -73,9 +76,11 @@ module CompletionKit
73
76
  if prompt.runs.exists?
74
77
  new_prompt = prompt.clone_as_new_version(attrs)
75
78
  new_prompt.publish!
76
- text_result(new_prompt.as_json)
79
+ new_prompt.update!(tag_names: args["tag_names"]) if args.key?("tag_names")
80
+ text_result(new_prompt.reload.as_json)
77
81
  elsif prompt.update(attrs)
78
- text_result(prompt.as_json)
82
+ prompt.update!(tag_names: args["tag_names"]) if args.key?("tag_names")
83
+ text_result(prompt.reload.as_json)
79
84
  else
80
85
  error_result(prompt.errors.full_messages.join(", "))
81
86
  end
@@ -21,7 +21,8 @@ module CompletionKit
21
21
  properties: {
22
22
  name: {type: "string"}, prompt_id: {type: "integer"},
23
23
  dataset_id: {type: "integer"}, judge_model: {type: "string"},
24
- metric_ids: {type: "array", items: {type: "integer"}}
24
+ metric_ids: {type: "array", items: {type: "integer"}},
25
+ tag_names: {type: "array", items: {type: "string"}}
25
26
  },
26
27
  required: ["name", "prompt_id"]
27
28
  },
@@ -34,7 +35,8 @@ module CompletionKit
34
35
  properties: {
35
36
  id: {type: "integer"}, name: {type: "string"},
36
37
  dataset_id: {type: "integer"}, judge_model: {type: "string"},
37
- metric_ids: {type: "array", items: {type: "integer"}}
38
+ metric_ids: {type: "array", items: {type: "integer"}},
39
+ tag_names: {type: "array", items: {type: "string"}}
38
40
  },
39
41
  required: ["id"]
40
42
  },
@@ -64,6 +66,7 @@ module CompletionKit
64
66
  run = Run.new(args.slice("name", "prompt_id", "dataset_id", "judge_model"))
65
67
  if run.save
66
68
  run.replace_metrics!(args["metric_ids"])
69
+ run.update!(tag_names: args["tag_names"]) if args.key?("tag_names")
67
70
  text_result(run.reload.as_json)
68
71
  else
69
72
  error_result(run.errors.full_messages.join(", "))
@@ -72,8 +75,9 @@ module CompletionKit
72
75
 
73
76
  def self.update(args)
74
77
  run = Run.find(args["id"])
75
- if run.update(args.except("id", "metric_ids").slice("name", "dataset_id", "judge_model"))
78
+ if run.update(args.except("id", "metric_ids", "tag_names").slice("name", "dataset_id", "judge_model"))
76
79
  run.replace_metrics!(args["metric_ids"]) if args.key?("metric_ids")
80
+ run.update!(tag_names: args["tag_names"]) if args.key?("tag_names")
77
81
  text_result(run.reload.as_json)
78
82
  else
79
83
  error_result(run.errors.full_messages.join(", "))
@@ -0,0 +1,74 @@
1
+ module CompletionKit
2
+ module McpTools
3
+ module Tags
4
+ extend Base
5
+
6
+ TOOLS = {
7
+ "tags_list" => {
8
+ description: "List all tags",
9
+ inputSchema: {type: "object", properties: {}, required: []},
10
+ handler: :list
11
+ },
12
+ "tags_get" => {
13
+ description: "Get a tag by ID",
14
+ inputSchema: {type: "object", properties: {id: {type: "integer"}}, required: ["id"]},
15
+ handler: :get
16
+ },
17
+ "tags_create" => {
18
+ description: "Create a tag. Color is auto-assigned.",
19
+ inputSchema: {
20
+ type: "object",
21
+ properties: {name: {type: "string"}},
22
+ required: ["name"]
23
+ },
24
+ handler: :create
25
+ },
26
+ "tags_update" => {
27
+ description: "Rename a tag.",
28
+ inputSchema: {
29
+ type: "object",
30
+ properties: {id: {type: "integer"}, name: {type: "string"}},
31
+ required: ["id"]
32
+ },
33
+ handler: :update
34
+ },
35
+ "tags_delete" => {
36
+ description: "Delete a tag. Removes the tag from every linked metric, prompt, run, and dataset.",
37
+ inputSchema: {type: "object", properties: {id: {type: "integer"}}, required: ["id"]},
38
+ handler: :delete
39
+ }
40
+ }.freeze
41
+
42
+ def self.list(_args)
43
+ text_result(CompletionKit::Tag.order(:name).map(&:as_json))
44
+ end
45
+
46
+ def self.get(args)
47
+ text_result(CompletionKit::Tag.find(args["id"]).as_json)
48
+ end
49
+
50
+ def self.create(args)
51
+ tag = CompletionKit::Tag.new(name: args["name"])
52
+ if tag.save
53
+ text_result(tag.as_json)
54
+ else
55
+ error_result(tag.errors.full_messages.join(", "))
56
+ end
57
+ end
58
+
59
+ def self.update(args)
60
+ tag = CompletionKit::Tag.find(args["id"])
61
+ if tag.update(name: args["name"])
62
+ text_result(tag.as_json)
63
+ else
64
+ error_result(tag.errors.full_messages.join(", "))
65
+ end
66
+ end
67
+
68
+ def self.delete(args)
69
+ CompletionKit::Tag.find(args["id"]).destroy!
70
+ text_result("Tag #{args["id"]} deleted")
71
+ end
72
+ end
73
+ end
74
+ end