layered-assistant-rails 0.3.1 → 0.4.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 (27) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -4
  3. data/app/controllers/layered/assistant/assistants_controller.rb +18 -4
  4. data/app/controllers/layered/assistant/skills_controller.rb +60 -0
  5. data/app/models/layered/assistant/assistant.rb +2 -0
  6. data/app/models/layered/assistant/assistant_skill.rb +12 -0
  7. data/app/models/layered/assistant/skill.rb +20 -0
  8. data/app/services/layered/assistant/clients/openai.rb +1 -1
  9. data/app/services/layered/assistant/models/create_service.rb +12 -1
  10. data/app/services/layered/assistant/system_prompt_service.rb +8 -1
  11. data/app/views/layered/assistant/assistants/_form.html.erb +18 -5
  12. data/app/views/layered/assistant/assistants/index.html.erb +8 -0
  13. data/app/views/layered/assistant/conversations/show.html.erb +0 -1
  14. data/app/views/layered/assistant/panel/conversations/show.html.erb +0 -1
  15. data/app/views/layered/assistant/public/conversations/show.html.erb +0 -1
  16. data/app/views/layered/assistant/public/panel/conversations/show.html.erb +0 -1
  17. data/app/views/layered/assistant/skills/_form.html.erb +30 -0
  18. data/app/views/layered/assistant/skills/edit.html.erb +2 -0
  19. data/app/views/layered/assistant/skills/index.html.erb +41 -0
  20. data/app/views/layered/assistant/skills/new.html.erb +2 -0
  21. data/app/views/layouts/layered/assistant/application.html.erb +1 -0
  22. data/config/routes.rb +1 -0
  23. data/data/models.json +21 -18
  24. data/db/migrate/20260406000001_create_layered_assistant_skills.rb +13 -0
  25. data/db/migrate/20260406000002_create_layered_assistant_assistant_skills.rb +13 -0
  26. data/lib/layered/assistant/version.rb +1 -1
  27. metadata +12 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8c4d53502a7e91e56f50420d3d16830d3441a49a3a6a5a399be405d10e0c05b4
4
- data.tar.gz: 6155fe4297f73702449ac22e099692cc17d6582c697f68c0e7b9df8f0f3bc96f
3
+ metadata.gz: 609342ee9956936967616a13c2d86a350cbbf099875cc5bc5321730dd0280c71
4
+ data.tar.gz: 89adcbc8c2f24834724e710427f7c7acc93d2d444b78d11d2005e3eb80d171cb
5
5
  SHA512:
6
- metadata.gz: 8131ee6e51b4f9417501de38b88af330e1dbd6b0603e4284e56900e37b41c549265d56821b6518846e1d1c95382d4fe186fca8c2d5a450c7c4a17f2a5ac79266
7
- data.tar.gz: 9ff9c8b830ee1cabaeed1da03332e57f5622ce79104f3d0f2e760be19d2dc503adcc0195bb3eb0d877b9b4a656a8556e6596301b087eb9fb805d619291978e18
6
+ metadata.gz: a8e997c64ebdb5685d15571096f7539887995392850f68e1454cd579de82294d7c0eb6bbcae433984d8afabe62674b568fb13b241605b46827bbbd0e4476b871
7
+ data.tar.gz: 00dd1808b570ff2204ba9866f589b5bc0ab159dd223846d3a9c6f12cbad6e685fb4d6a46e2c074b8c1f55a503ac83ac8b0c68a16f93c63a26a3fc665f9620bc9
data/README.md CHANGED
@@ -208,6 +208,10 @@ Run the gem tests from the root directory:
208
208
  bundle exec rake test
209
209
  ```
210
210
 
211
+ ## Contributing
212
+
213
+ This project is still in its early days. We welcome issues, feedback, and ideas - they genuinely help shape the direction of the project. That said, we're holding off on accepting pull requests for now to stay focused on getting the foundations right. Thank you for your patience and interest. See [CLA.md](CLA.md) for the full policy.
214
+
211
215
  ## License
212
216
 
213
217
  Released under the [Apache 2.0 License](LICENSE).
@@ -217,7 +221,3 @@ Copyright 2026 LAYERED AI LIMITED (UK company number: 17056830). See [NOTICE](NO
217
221
  ## Trademarks
218
222
 
219
223
  The source code is fully open, but the layered.ai name, logo, and brand assets are trademarks of LAYERED AI LIMITED. The Apache 2.0 license does not grant rights to use the layered.ai branding. Forks and redistributions must use a distinct name. See [TRADEMARK.md](TRADEMARK.md) for the full policy.
220
-
221
- ## Contributing
222
-
223
- - [CLA.md](CLA.md) - contributor license agreement
@@ -4,10 +4,11 @@ module Layered
4
4
  before_action :set_assistant, only: [:edit, :update, :destroy]
5
5
  before_action :set_models, only: [:new, :create, :edit, :update]
6
6
  before_action :set_personas, only: [:new, :create, :edit, :update]
7
+ before_action :set_skills, only: [:new, :create, :edit, :update]
7
8
 
8
9
  def index
9
10
  @page_title = "Assistants"
10
- @pagy, @assistants = pagy(scoped(Assistant).by_name)
11
+ @pagy, @assistants = pagy(scoped(Assistant).includes(:persona).by_name)
11
12
  end
12
13
 
13
14
  def new
@@ -16,11 +17,12 @@ module Layered
16
17
  end
17
18
 
18
19
  def create
19
- @assistant = Assistant.new(assistant_params.except(:persona_id))
20
+ @assistant = Assistant.new(assistant_params.except(:persona_id, :skill_ids))
20
21
  @assistant.owner = l_ui_current_user
21
22
  @assistant.persona = scoped(Persona).find(assistant_params[:persona_id]) if assistant_params[:persona_id].present?
22
23
 
23
24
  if @assistant.save
25
+ assign_skills
24
26
  redirect_to layered_assistant.assistants_path, notice: "Assistant was successfully created."
25
27
  else
26
28
  render :new, status: :unprocessable_entity
@@ -36,7 +38,8 @@ module Layered
36
38
  @assistant.persona = assistant_params[:persona_id].present? ? scoped(Persona).find(assistant_params[:persona_id]) : nil
37
39
  end
38
40
 
39
- if @assistant.update(assistant_params.except(:persona_id))
41
+ if @assistant.update(assistant_params.except(:persona_id, :skill_ids))
42
+ assign_skills
40
43
  redirect_to layered_assistant.assistants_path, notice: "Assistant was successfully updated."
41
44
  else
42
45
  render :edit, status: :unprocessable_entity
@@ -62,8 +65,19 @@ module Layered
62
65
  @personas = scoped(Persona).by_name
63
66
  end
64
67
 
68
+ def set_skills
69
+ @skills = scoped(Skill).by_name
70
+ end
71
+
72
+ def assign_skills
73
+ if assistant_params.key?(:skill_ids)
74
+ skill_ids = Array(assistant_params[:skill_ids]).compact_blank
75
+ @assistant.skills = scoped(Skill).where(id: skill_ids)
76
+ end
77
+ end
78
+
65
79
  def assistant_params
66
- params.require(:assistant).permit(:name, :description, :instructions, :default_model_id, :persona_id, :public)
80
+ params.require(:assistant).permit(:name, :description, :instructions, :default_model_id, :persona_id, :public, skill_ids: [])
67
81
  end
68
82
  end
69
83
  end
@@ -0,0 +1,60 @@
1
+ module Layered
2
+ module Assistant
3
+ class SkillsController < ApplicationController
4
+ before_action :set_skill, only: [:edit, :update, :destroy]
5
+
6
+ def index
7
+ @page_title = "Skills"
8
+ @pagy, @skills = pagy(scoped(Skill).by_name)
9
+ end
10
+
11
+ def new
12
+ @page_title = "New skill"
13
+ @skill = Skill.new
14
+ end
15
+
16
+ def create
17
+ @skill = Skill.new(skill_params)
18
+ @skill.owner = l_ui_current_user
19
+
20
+ if @skill.save
21
+ redirect_to layered_assistant.skills_path, notice: "Skill was successfully created."
22
+ else
23
+ render :new, status: :unprocessable_entity
24
+ end
25
+ end
26
+
27
+ def edit
28
+ @page_title = "Edit skill"
29
+ end
30
+
31
+ def update
32
+ if @skill.update(skill_params)
33
+ redirect_to layered_assistant.skills_path, notice: "Skill was successfully updated."
34
+ else
35
+ render :edit, status: :unprocessable_entity
36
+ end
37
+ end
38
+
39
+ def destroy
40
+ if @skill.destroy
41
+ redirect_to layered_assistant.skills_path, notice: "Skill was successfully deleted."
42
+ else
43
+ redirect_to layered_assistant.skills_path, alert: "Skill could not be deleted: #{@skill.errors.full_messages.to_sentence}."
44
+ end
45
+ rescue ActiveRecord::InvalidForeignKey
46
+ redirect_to layered_assistant.skills_path, alert: "Skill could not be deleted because it is assigned to assistants."
47
+ end
48
+
49
+ private
50
+
51
+ def set_skill
52
+ @skill = scoped(Skill).find(params[:id])
53
+ end
54
+
55
+ def skill_params
56
+ params.require(:skill).permit(:name, :description, :instructions)
57
+ end
58
+ end
59
+ end
60
+ end
@@ -8,6 +8,8 @@ module Layered
8
8
  belongs_to :owner, polymorphic: true, optional: true
9
9
  belongs_to :default_model, class_name: "Layered::Assistant::Model", optional: true, counter_cache: :assistants_count
10
10
  belongs_to :persona, optional: true, counter_cache: :assistants_count
11
+ has_many :assistant_skills, dependent: :destroy
12
+ has_many :skills, through: :assistant_skills
11
13
  has_many :conversations, dependent: :destroy
12
14
 
13
15
  # Validations
@@ -0,0 +1,12 @@
1
+ module Layered
2
+ module Assistant
3
+ class AssistantSkill < ApplicationRecord
4
+ # Associations
5
+ belongs_to :assistant, counter_cache: :assistant_skills_count
6
+ belongs_to :skill, counter_cache: :assistants_count
7
+
8
+ # Validations
9
+ validates :skill_id, uniqueness: { scope: :assistant_id }
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,20 @@
1
+ module Layered
2
+ module Assistant
3
+ class Skill < ApplicationRecord
4
+ # UID
5
+ has_secure_token :uid
6
+
7
+ # Associations
8
+ belongs_to :owner, polymorphic: true, optional: true
9
+ has_many :assistant_skills, dependent: :restrict_with_error
10
+ has_many :assistants, through: :assistant_skills
11
+
12
+ # Validations
13
+ validates :name, presence: true
14
+
15
+ # Scopes
16
+ scope :by_name, -> { order(name: :asc, created_at: :desc) }
17
+ scope :by_created_at, -> { order(created_at: :desc) }
18
+ end
19
+ end
20
+ end
@@ -12,7 +12,7 @@ module Layered
12
12
  }
13
13
  if @provider.url.present?
14
14
  client_options[:uri_base] = @provider.url.sub(/\/\z/, "")
15
- client_options[:api_version] = "" # Gemini and other OpenAI-compatible APIs use their own path
15
+ client_options[:api_version] = "" # Gemini and other OpenAI-compatible APIs use their own path
16
16
  end
17
17
 
18
18
  ::OpenAI::Client.new(**client_options) do |f|
@@ -6,13 +6,14 @@ module Layered
6
6
  module Models
7
7
  class CreateService
8
8
  MODELS_URL = "https://raw.githubusercontent.com/layered-ai-public/layered-assistant-rails/main/data/models.json".freeze
9
+ LOCAL_MODELS_PATH = Layered::Assistant::Engine.root.join("data", "models.json").freeze
9
10
 
10
11
  def initialize(provider)
11
12
  @provider = provider
12
13
  end
13
14
 
14
15
  def call
15
- models_data = fetch_models
16
+ models_data = Rails.env.development? ? load_local_models : fetch_models
16
17
  return if models_data.nil?
17
18
 
18
19
  entries = models_data[@provider.name]
@@ -30,6 +31,16 @@ module Layered
30
31
 
31
32
  private
32
33
 
34
+ def load_local_models
35
+ unless File.exist?(LOCAL_MODELS_PATH)
36
+ Rails.logger.info "[layered-ui-assistant] Local model catalogue not found at #{LOCAL_MODELS_PATH} - skipping model sync"
37
+ return nil
38
+ end
39
+
40
+ Rails.logger.info "[layered-ui-assistant] Loading model catalogue from local file (development)"
41
+ JSON.parse(File.read(LOCAL_MODELS_PATH))
42
+ end
43
+
33
44
  def fetch_models
34
45
  uri = URI(MODELS_URL)
35
46
  response = Net::HTTP.get_response(uri)
@@ -5,7 +5,14 @@ module Layered
5
5
  parts = []
6
6
 
7
7
  if assistant.persona&.instructions.present?
8
- parts << "**Persona**\n\n#{assistant.persona.instructions}"
8
+ parts << "## Persona\n\n#{assistant.persona.instructions}"
9
+ end
10
+
11
+ skill_sections = assistant.skills.filter_map do |s|
12
+ "### #{s.name}\n\n#{s.instructions}" if s.instructions.present?
13
+ end
14
+ if skill_sections.any?
15
+ parts << "## Skills\n\n#{skill_sections.join("\n\n---\n\n")}"
9
16
  end
10
17
 
11
18
  parts << assistant.instructions if assistant.instructions.present?
@@ -13,6 +13,12 @@
13
13
  <%= render "layered_ui/shared/field_error", object: assistant, field: :description %>
14
14
  </div>
15
15
 
16
+ <div class="l-ui-form__group">
17
+ <%= render "layered_ui/shared/label", form: f, field: :default_model_id, name: "Default model", required: false %>
18
+ <%= f.select :default_model_id, @models.map { |m| ["#{m.provider.name} - #{m.name}", m.id] }, { include_blank: "None" }, class: "l-ui-select" %>
19
+ <%= render "layered_ui/shared/field_error", object: assistant, field: :default_model_id %>
20
+ </div>
21
+
16
22
  <div class="l-ui-form__group">
17
23
  <%= render "layered_ui/shared/label", form: f, field: :instructions, required: false %>
18
24
  <%= f.text_area :instructions, class: "l-ui-form__field", rows: 5 %>
@@ -25,11 +31,18 @@
25
31
  <%= render "layered_ui/shared/field_error", object: assistant, field: :persona_id %>
26
32
  </div>
27
33
 
28
- <div class="l-ui-form__group">
29
- <%= render "layered_ui/shared/label", form: f, field: :default_model_id, name: "Default model", required: false %>
30
- <%= f.select :default_model_id, @models.map { |m| ["#{m.provider.name} - #{m.name}", m.id] }, { include_blank: "None" }, class: "l-ui-select" %>
31
- <%= render "layered_ui/shared/field_error", object: assistant, field: :default_model_id %>
32
- </div>
34
+ <% if @skills.any? %>
35
+ <fieldset class="l-ui-form__group">
36
+ <legend class="l-ui-label">Skills</legend>
37
+ <%= hidden_field_tag "assistant[skill_ids][]", "" %>
38
+ <% @skills.each do |skill| %>
39
+ <div class="l-ui-container--checkbox">
40
+ <%= f.check_box :skill_ids, { multiple: true, checked: assistant.skill_ids.include?(skill.id), id: "assistant_skill_ids_#{skill.id}" }, skill.id, nil %>
41
+ <label for="assistant_skill_ids_<%= skill.id %>" class="l-ui-label--checkbox"><%= skill.name %></label>
42
+ </div>
43
+ <% end %>
44
+ </fieldset>
45
+ <% end %>
33
46
 
34
47
  <label class="l-ui-switch l-ui-utility--mt-xl">
35
48
  <%= f.check_box :public, class: "l-ui-switch__input", role: "switch" %>
@@ -11,6 +11,8 @@
11
11
  <th scope="col" class="l-ui-table__header-cell">Name</th>
12
12
  <th scope="col" class="l-ui-table__header-cell">Description</th>
13
13
  <th scope="col" class="l-ui-table__header-cell">Default model</th>
14
+ <th scope="col" class="l-ui-table__header-cell">Persona</th>
15
+ <th scope="col" class="l-ui-table__header-cell">Skills</th>
14
16
  <th scope="col" class="l-ui-table__header-cell">Conversations</th>
15
17
  <th scope="col" class="l-ui-table__header-cell--action">Actions</th>
16
18
  </tr>
@@ -28,6 +30,12 @@
28
30
  <td class="l-ui-table__cell">
29
31
  <%= assistant.default_model&.name %>
30
32
  </td>
33
+ <td class="l-ui-table__cell">
34
+ <%= assistant.persona&.name || "None" %>
35
+ </td>
36
+ <td class="l-ui-table__cell">
37
+ <%= assistant.assistant_skills_count %>
38
+ </td>
31
39
  <td class="l-ui-table__cell">
32
40
  <%= link_to assistant.conversations_count, layered_assistant.assistant_conversations_path(assistant) %>
33
41
  </td>
@@ -16,7 +16,6 @@
16
16
  data-action="click->messages#jumpToBottom"
17
17
  aria-label="Scroll to bottom"
18
18
  type="button">
19
- <%= image_tag "layered_ui/icon_chevron_down.svg", class: "l-ui-icon--sm l-ui-icon--dark-invert", alt: "" %>
20
19
  </button>
21
20
  </div>
22
21
 
@@ -14,7 +14,6 @@
14
14
  data-action="click->messages#jumpToBottom"
15
15
  aria-label="Scroll to bottom"
16
16
  type="button">
17
- <%= image_tag "layered_ui/icon_chevron_down.svg", class: "l-ui-icon--sm l-ui-icon--dark-invert", alt: "" %>
18
17
  </button>
19
18
  </div>
20
19
 
@@ -24,7 +24,6 @@
24
24
  data-action="click->messages#jumpToBottom"
25
25
  aria-label="Scroll to bottom"
26
26
  type="button">
27
- <%= image_tag "layered_ui/icon_chevron_down.svg", class: "l-ui-icon--sm l-ui-icon--dark-invert", alt: "" %>
28
27
  </button>
29
28
  </div>
30
29
 
@@ -14,7 +14,6 @@
14
14
  data-action="click->messages#jumpToBottom"
15
15
  aria-label="Scroll to bottom"
16
16
  type="button">
17
- <%= image_tag "layered_ui/icon_chevron_down.svg", class: "l-ui-icon--sm l-ui-icon--dark-invert", alt: "" %>
18
17
  </button>
19
18
  </div>
20
19
 
@@ -0,0 +1,30 @@
1
+ <%= form_with(model: skill, url: url, html: { class: "l-ui-form" }) do |f| %>
2
+ <%= render "layered_ui/shared/form_errors", item: skill %>
3
+
4
+ <div class="l-ui-form__group">
5
+ <%= render "layered_ui/shared/label", form: f, field: :name, required: true %>
6
+ <%= f.text_field :name, class: "l-ui-form__field" %>
7
+ <%= render "layered_ui/shared/field_error", object: skill, field: :name %>
8
+ </div>
9
+
10
+ <div class="l-ui-form__group">
11
+ <%= render "layered_ui/shared/label", form: f, field: :description, required: false %>
12
+ <%= f.text_area :description, class: "l-ui-form__field", rows: 3 %>
13
+ <%= render "layered_ui/shared/field_error", object: skill, field: :description %>
14
+ </div>
15
+
16
+ <div class="l-ui-form__group">
17
+ <%= render "layered_ui/shared/label", form: f, field: :instructions, required: false %>
18
+ <%= f.text_area :instructions, class: "l-ui-form__field", rows: 5 %>
19
+ <%= render "layered_ui/shared/field_error", object: skill, field: :instructions %>
20
+ </div>
21
+
22
+ <div class="l-ui-container--spread l-ui-utility--mt-2xl">
23
+ <% if skill.persisted? %>
24
+ <%= link_to "Delete", layered_assistant.skill_path(skill), class: "l-ui-button--outline-danger", data: { turbo_method: :delete, turbo_confirm: "Are you sure?" } %>
25
+ <% else %>
26
+ <span></span>
27
+ <% end %>
28
+ <%= f.submit skill.new_record? ? "Create" : "Update", class: "l-ui-button--primary" %>
29
+ </div>
30
+ <% end %>
@@ -0,0 +1,2 @@
1
+ <h1>Edit skill</h1>
2
+ <%= render "form", skill: @skill, url: layered_assistant.skill_path(@skill) %>
@@ -0,0 +1,41 @@
1
+ <div class="l-ui-container--spread">
2
+ <h1>Skills</h1>
3
+ <%= link_to "New", layered_assistant.new_skill_path, class: "l-ui-button--primary" %>
4
+ </div>
5
+
6
+ <div class="l-ui-container--table l-ui-utility--mt-lg">
7
+ <table class="l-ui-table">
8
+ <caption class="l-ui-sr-only">Skills</caption>
9
+ <thead class="l-ui-table__header">
10
+ <tr>
11
+ <th scope="col" class="l-ui-table__header-cell">Name</th>
12
+ <th scope="col" class="l-ui-table__header-cell">Description</th>
13
+ <th scope="col" class="l-ui-table__header-cell">Assistants</th>
14
+ <th scope="col" class="l-ui-table__header-cell--action">Actions</th>
15
+ </tr>
16
+ </thead>
17
+
18
+ <tbody class="l-ui-table__body">
19
+ <% @skills.each do |skill| %>
20
+ <tr>
21
+ <th scope="row" class="l-ui-table__cell--primary">
22
+ <%= skill.name %>
23
+ </th>
24
+ <td class="l-ui-table__cell">
25
+ <%= truncate(skill.description, length: 60) %>
26
+ </td>
27
+ <td class="l-ui-table__cell">
28
+ <%= skill.assistants_count %>
29
+ </td>
30
+ <td class="l-ui-table__cell--action">
31
+ <%= link_to "Edit", layered_assistant.edit_skill_path(skill) %>
32
+ </td>
33
+ </tr>
34
+ <% end %>
35
+ </tbody>
36
+ </table>
37
+
38
+ <div class="l-ui-utility--mt-lg">
39
+ <%= l_ui_pagy(@pagy) %>
40
+ </div>
41
+ </div>
@@ -0,0 +1,2 @@
1
+ <h1>New skill</h1>
2
+ <%= render "form", skill: @skill, url: layered_assistant.skills_path %>
@@ -10,6 +10,7 @@
10
10
  <%= l_ui_navigation_item "Setup", layered_assistant.root_path %>
11
11
  <%= l_ui_navigation_item "Providers", layered_assistant.providers_path %>
12
12
  <%= l_ui_navigation_item "Personas", layered_assistant.personas_path %>
13
+ <%= l_ui_navigation_item "Skills", layered_assistant.skills_path %>
13
14
  <%= l_ui_navigation_item "Assistants", layered_assistant.assistants_path %>
14
15
  <%= l_ui_navigation_item "Conversations", layered_assistant.conversations_path %>
15
16
  <% else %>
data/config/routes.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  Layered::Assistant::Engine.routes.draw do
2
2
  root "setup#index"
3
3
  resources :personas, except: [:show]
4
+ resources :skills, except: [:show]
4
5
  resources :assistants, except: [:show] do
5
6
  resources :conversations, only: [:index]
6
7
  end
data/data/models.json CHANGED
@@ -1,42 +1,45 @@
1
1
  {
2
2
  "Anthropic": [
3
- { "name": "Claude Opus 4.6", "identifier": "claude-opus-4-6" },
3
+ { "name": "Claude Opus 4.7", "identifier": "claude-opus-4-7" },
4
4
  { "name": "Claude Sonnet 4.6", "identifier": "claude-sonnet-4-6" },
5
5
  { "name": "Claude Haiku 4.5", "identifier": "claude-haiku-4-5" }
6
6
  ],
7
7
  "OpenAI": [
8
+ { "name": "GPT-5.5", "identifier": "gpt-5.5" },
9
+ { "name": "GPT-5.5 Pro", "identifier": "gpt-5.5-pro" },
8
10
  { "name": "GPT-5.4", "identifier": "gpt-5.4" },
9
- { "name": "GPT-5 Mini", "identifier": "gpt-5-mini" }
11
+ { "name": "GPT-5.4 Mini", "identifier": "gpt-5.4-mini" },
12
+ { "name": "GPT-5.4 Nano", "identifier": "gpt-5.4-nano" }
10
13
  ],
11
14
  "Gemini": [
12
15
  { "name": "Gemini 3.1 Pro Preview", "identifier": "gemini-3.1-pro-preview" },
13
- { "name": "Gemini 2.5 Flash Lite", "identifier": "gemini-2.5-flash-lite" },
16
+ { "name": "Gemini 3 Flash Preview", "identifier": "gemini-3-flash-preview" },
17
+ { "name": "Gemini 3.1 Flash-Lite Preview", "identifier": "gemini-3.1-flash-lite-preview" },
18
+ { "name": "Gemini 2.5 Pro", "identifier": "gemini-2.5-pro" },
14
19
  { "name": "Gemini 2.5 Flash", "identifier": "gemini-2.5-flash" },
15
- { "name": "Gemini 2.5 Pro", "identifier": "gemini-2.5-pro" }
20
+ { "name": "Gemini 2.5 Flash Lite", "identifier": "gemini-2.5-flash-lite" }
16
21
  ],
17
22
  "OpenRouter": [
18
- { "name": "MiniMax M2.5", "identifier": "minimax/minimax-m2.5" },
19
- { "name": "DeepSeek V3.2", "identifier": "deepseek/deepseek-v3.2" },
23
+ { "name": "Grok 4.1 Fast", "identifier": "x-ai/grok-4.1-fast" },
20
24
  { "name": "Grok 4", "identifier": "x-ai/grok-4" },
21
- { "name": "Grok 4 (Fast)", "identifier": "x-ai/grok-4-fast" },
22
- { "name": "Grok 3", "identifier": "x-ai/grok-3" },
23
- { "name": "Llama 4 Maverick", "identifier": "meta-llama/llama-4-maverick-17b-128e-instruct" },
24
- { "name": "Llama 4 Scout", "identifier": "meta-llama/llama-4-scout-17b-16e-instruct" }
25
+ { "name": "DeepSeek V3.2", "identifier": "deepseek/deepseek-v3.2" },
26
+ { "name": "Kimi K2.6", "identifier": "moonshotai/kimi-k2.6" },
27
+ { "name": "MiniMax M2.7", "identifier": "minimax/minimax-m2.7" },
28
+ { "name": "MiniMax M2.5", "identifier": "minimax/minimax-m2.5" },
29
+ { "name": "MiMo V2 Pro", "identifier": "xiaomi/mimo-v2-pro" }
25
30
  ],
26
31
  "Groq": [
27
32
  { "name": "Compound", "identifier": "groq/compound" },
28
33
  { "name": "Compound Mini", "identifier": "groq/compound-mini" },
29
34
  { "name": "GPT-OSS 120B", "identifier": "openai/gpt-oss-120b" },
30
- { "name": "Llama 4 Maverick", "identifier": "meta-llama/llama-4-maverick-17b-128e-instruct" },
31
- { "name": "Llama 4 Scout", "identifier": "meta-llama/llama-4-scout-17b-16e-instruct" },
35
+ { "name": "GPT-OSS 20B", "identifier": "openai/gpt-oss-20b" },
32
36
  { "name": "Llama 3.3 70B Versatile", "identifier": "llama-3.3-70b-versatile" },
33
- { "name": "Llama 3.1 8B Instant", "identifier": "llama-3.1-8b-instant" },
34
- { "name": "Qwen 3 32B", "identifier": "qwen/qwen3-32b" },
35
- { "name": "Kimi K2.5", "identifier": "moonshotai/kimi-k2-instruct-0905" }
37
+ { "name": "Llama 3.1 8B Instant", "identifier": "llama-3.1-8b-instant" }
36
38
  ],
37
39
  "Mistral": [
38
- { "name": "Mistral Large 2512", "identifier": "mistral-large-2512" },
39
- { "name": "Mistral Medium 2508", "identifier": "mistral-medium-2508" },
40
- { "name": "Mistral Small 2506", "identifier": "mistral-small-2506" }
40
+ { "name": "Mistral Large 3", "identifier": "mistral-large-2512" },
41
+ { "name": "Mistral Medium 3.1", "identifier": "mistral-medium-2508" },
42
+ { "name": "Mistral Small 4", "identifier": "mistral-small-2603" },
43
+ { "name": "Magistral Medium 1.2", "identifier": "magistral-medium-2509" }
41
44
  ]
42
45
  }
@@ -0,0 +1,13 @@
1
+ class CreateLayeredAssistantSkills < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :layered_assistant_skills, if_not_exists: true do |t|
4
+ t.string :uid, null: false, index: { unique: true }
5
+ t.references :owner, polymorphic: true
6
+ t.string :name, null: false
7
+ t.text :description
8
+ t.text :instructions
9
+ t.bigint :assistants_count, default: 0, null: false
10
+ t.timestamps
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ class CreateLayeredAssistantAssistantSkills < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :layered_assistant_assistant_skills, if_not_exists: true do |t|
4
+ t.references :assistant, null: false, foreign_key: { to_table: :layered_assistant_assistants }
5
+ t.references :skill, null: false, foreign_key: { to_table: :layered_assistant_skills }
6
+ t.timestamps
7
+ end
8
+
9
+ add_index :layered_assistant_assistant_skills, [:assistant_id, :skill_id], unique: true, name: "idx_assistant_skills_on_assistant_and_skill"
10
+
11
+ add_column :layered_assistant_assistants, :assistant_skills_count, :bigint, default: 0, null: false
12
+ end
13
+ end
@@ -1,5 +1,5 @@
1
1
  module Layered
2
2
  module Assistant
3
- VERSION = "0.3.1"
3
+ VERSION = "0.4.0"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: layered-assistant-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - layered.ai
@@ -113,14 +113,14 @@ dependencies:
113
113
  requirements:
114
114
  - - "~>"
115
115
  - !ruby/object:Gem::Version
116
- version: 0.2.1
116
+ version: '0.8'
117
117
  type: :runtime
118
118
  prerelease: false
119
119
  version_requirements: !ruby/object:Gem::Requirement
120
120
  requirements:
121
121
  - - "~>"
122
122
  - !ruby/object:Gem::Version
123
- version: 0.2.1
123
+ version: '0.8'
124
124
  - !ruby/object:Gem::Dependency
125
125
  name: propshaft
126
126
  requirement: !ruby/object:Gem::Requirement
@@ -322,6 +322,7 @@ files:
322
322
  - app/controllers/layered/assistant/public/panel/conversations_controller.rb
323
323
  - app/controllers/layered/assistant/public/panel/messages_controller.rb
324
324
  - app/controllers/layered/assistant/setup_controller.rb
325
+ - app/controllers/layered/assistant/skills_controller.rb
325
326
  - app/helpers/layered/assistant/access_helper.rb
326
327
  - app/helpers/layered/assistant/messages_helper.rb
327
328
  - app/helpers/layered/assistant/panel_helper.rb
@@ -337,11 +338,13 @@ files:
337
338
  - app/jobs/layered/assistant/messages/response_job.rb
338
339
  - app/models/layered/assistant/application_record.rb
339
340
  - app/models/layered/assistant/assistant.rb
341
+ - app/models/layered/assistant/assistant_skill.rb
340
342
  - app/models/layered/assistant/conversation.rb
341
343
  - app/models/layered/assistant/message.rb
342
344
  - app/models/layered/assistant/model.rb
343
345
  - app/models/layered/assistant/persona.rb
344
346
  - app/models/layered/assistant/provider.rb
347
+ - app/models/layered/assistant/skill.rb
345
348
  - app/services/layered/assistant/chunk_parser.rb
346
349
  - app/services/layered/assistant/chunk_service.rb
347
350
  - app/services/layered/assistant/client_service.rb
@@ -397,6 +400,10 @@ files:
397
400
  - app/views/layered/assistant/public/panel/messages/create.turbo_stream.erb
398
401
  - app/views/layered/assistant/setup/_setup.html.erb
399
402
  - app/views/layered/assistant/setup/index.html.erb
403
+ - app/views/layered/assistant/skills/_form.html.erb
404
+ - app/views/layered/assistant/skills/edit.html.erb
405
+ - app/views/layered/assistant/skills/index.html.erb
406
+ - app/views/layered/assistant/skills/new.html.erb
400
407
  - app/views/layouts/layered/assistant/_host_navigation.html.erb
401
408
  - app/views/layouts/layered/assistant/application.html.erb
402
409
  - config/importmap.rb
@@ -410,6 +417,8 @@ files:
410
417
  - db/migrate/20260403000000_create_layered_assistant_personas.rb
411
418
  - db/migrate/20260403000001_add_persona_to_layered_assistant_assistants.rb
412
419
  - db/migrate/20260406000000_rename_system_prompt_to_instructions.rb
420
+ - db/migrate/20260406000001_create_layered_assistant_skills.rb
421
+ - db/migrate/20260406000002_create_layered_assistant_assistant_skills.rb
413
422
  - lib/generators/layered/assistant/install_generator.rb
414
423
  - lib/generators/layered/assistant/migrations_generator.rb
415
424
  - lib/generators/layered/assistant/templates/initializer.rb