layered-assistant-rails 0.3.1 → 0.3.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8c4d53502a7e91e56f50420d3d16830d3441a49a3a6a5a399be405d10e0c05b4
4
- data.tar.gz: 6155fe4297f73702449ac22e099692cc17d6582c697f68c0e7b9df8f0f3bc96f
3
+ metadata.gz: d8d828826c0b73a9efaa2324f15d1e7d684c2b3c00c760421d6d18bbf3f88d39
4
+ data.tar.gz: 11f5f52acb971db3b8bee92f6c5739e8cb89c7f5cbbd9411db8570fd53c2be87
5
5
  SHA512:
6
- metadata.gz: 8131ee6e51b4f9417501de38b88af330e1dbd6b0603e4284e56900e37b41c549265d56821b6518846e1d1c95382d4fe186fca8c2d5a450c7c4a17f2a5ac79266
7
- data.tar.gz: 9ff9c8b830ee1cabaeed1da03332e57f5622ce79104f3d0f2e760be19d2dc503adcc0195bb3eb0d877b9b4a656a8556e6596301b087eb9fb805d619291978e18
6
+ metadata.gz: d0e8c773d9b788562089ca773c615c1871e7440f453f18f8822074b83ea69db544cd747e8c540a21c10d53fb7babe3c37fe70245ce6bfc31a64fe496dff5a2ae
7
+ data.tar.gz: 3d3ee90a358372e72cdadccf200fff09e56adadfefc01c8a010991febc54281466b447d8730b6e00969a29225a56e38e5349187cd23b119274463e610f792fa1
@@ -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
@@ -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>
@@ -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
@@ -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.3.2"
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.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - layered.ai
@@ -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