layered-assistant-rails 0.2.2 → 0.3.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.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +4 -0
  3. data/README.md +1 -7
  4. data/app/controllers/layered/assistant/assistants_controller.rb +13 -3
  5. data/app/controllers/layered/assistant/personas_controller.rb +60 -0
  6. data/app/models/layered/assistant/assistant.rb +1 -0
  7. data/app/models/layered/assistant/conversation.rb +10 -0
  8. data/app/models/layered/assistant/persona.rb +20 -0
  9. data/app/services/layered/assistant/client_service.rb +1 -3
  10. data/app/services/layered/assistant/clients/anthropic.rb +2 -2
  11. data/app/services/layered/assistant/clients/base.rb +1 -1
  12. data/app/services/layered/assistant/clients/openai.rb +2 -2
  13. data/app/services/layered/assistant/messages_service.rb +5 -7
  14. data/app/services/layered/assistant/system_prompt_service.rb +17 -0
  15. data/app/views/layered/assistant/assistants/_form.html.erb +9 -3
  16. data/app/views/layered/assistant/conversations/show.html.erb +0 -1
  17. data/app/views/layered/assistant/panel/conversations/show.html.erb +0 -1
  18. data/app/views/layered/assistant/personas/_form.html.erb +31 -0
  19. data/app/views/layered/assistant/personas/edit.html.erb +2 -0
  20. data/app/views/layered/assistant/personas/index.html.erb +41 -0
  21. data/app/views/layered/assistant/personas/new.html.erb +2 -0
  22. data/app/views/layouts/layered/assistant/application.html.erb +1 -0
  23. data/config/routes.rb +1 -0
  24. data/db/migrate/20260403000000_create_layered_assistant_personas.rb +13 -0
  25. data/db/migrate/20260403000001_add_persona_to_layered_assistant_assistants.rb +5 -0
  26. data/db/migrate/20260406000000_rename_system_prompt_to_instructions.rb +5 -0
  27. data/lib/generators/layered/assistant/templates/initializer.rb +2 -4
  28. data/lib/layered/assistant/version.rb +1 -1
  29. metadata +11 -2
  30. data/app/views/layered/assistant/messages/_system_prompt.html.erb +0 -10
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b0c052e155f7489ab469b2fe1cc4a59b9285fceaaf2595c5947287a95f31a1ed
4
- data.tar.gz: 8449a3ab76b185cae858779e37fe6888679a1d9f96b76717871acf3fa5b49443
3
+ metadata.gz: 8c4d53502a7e91e56f50420d3d16830d3441a49a3a6a5a399be405d10e0c05b4
4
+ data.tar.gz: 6155fe4297f73702449ac22e099692cc17d6582c697f68c0e7b9df8f0f3bc96f
5
5
  SHA512:
6
- metadata.gz: 614f4ec4a6e1a592604a9f98572a61cd39808bd47dbcc9e2fcc8d80532dedb378f708877770aef11aaed25f9838be1a867b844b170efbdcb7be6a30dadb7d8b5
7
- data.tar.gz: 2655b683321d5e9a191106d8e20ab887b679d1b524e504a165a399dd30ef94874078908112ae6481c6bcbc7f2e00df07b9c3ea842e05313c7b9f3bd57b4eff74
6
+ metadata.gz: 8131ee6e51b4f9417501de38b88af330e1dbd6b0603e4284e56900e37b41c549265d56821b6518846e1d1c95382d4fe186fca8c2d5a450c7c4a17f2a5ac79266
7
+ data.tar.gz: 9ff9c8b830ee1cabaeed1da03332e57f5622ce79104f3d0f2e760be19d2dc503adcc0195bb3eb0d877b9b4a656a8556e6596301b087eb9fb805d619291978e18
data/AGENTS.md CHANGED
@@ -39,6 +39,10 @@ You can run the tests with: `bundle exec rake test`
39
39
  - Tables prefixed `layered_assistant_`
40
40
  - Models inherit from `Layered::Assistant::ApplicationRecord`
41
41
 
42
+ ### Ownership and scoping
43
+
44
+ Ownership is enforced at the controller layer via `scoped()`, not with model validations. When a controller action accepts a foreign key for a scoped model (e.g. `persona_id`, `assistant_id`), look up the record through `scoped(Model).find(params[:id])` so that out-of-scope IDs return 404. Do not add model-level validations that duplicate this scoping logic.
45
+
42
46
  ### Generators
43
47
 
44
48
  Verify dependencies before modifying host app files. Use `inject_into_file` for safe insertion. Check for existing imports to ensure idempotency.
data/README.md CHANGED
@@ -106,13 +106,7 @@ The `l_assistant_accessible?` helper evaluates the authorize block without side
106
106
 
107
107
  By default, all records are visible to any authorised user. If your application is multi-tenant or you need to restrict which records a user can see, configure a `scope` block in the initialiser.
108
108
 
109
- The block receives the model class, runs in controller context, and must return an `ActiveRecord::Relation`. The following models are passed through the scope block:
110
-
111
- | Model | Description |
112
- |---|---|
113
- | `Layered::Assistant::Conversation` | User conversations (has polymorphic `owner`) |
114
- | `Layered::Assistant::Assistant` | Assistant configurations (has polymorphic `owner`) |
115
- | `Layered::Assistant::Provider` | API provider credentials (has polymorphic `owner`) |
109
+ The block receives the model class, runs in controller context, and must return an `ActiveRecord::Relation`. All engine models with a polymorphic `owner` association are passed through the scope block.
116
110
 
117
111
  ### Scope all owned resources to the current user
118
112
 
@@ -3,6 +3,7 @@ module Layered
3
3
  class AssistantsController < ApplicationController
4
4
  before_action :set_assistant, only: [:edit, :update, :destroy]
5
5
  before_action :set_models, only: [:new, :create, :edit, :update]
6
+ before_action :set_personas, only: [:new, :create, :edit, :update]
6
7
 
7
8
  def index
8
9
  @page_title = "Assistants"
@@ -15,8 +16,9 @@ module Layered
15
16
  end
16
17
 
17
18
  def create
18
- @assistant = Assistant.new(assistant_params)
19
+ @assistant = Assistant.new(assistant_params.except(:persona_id))
19
20
  @assistant.owner = l_ui_current_user
21
+ @assistant.persona = scoped(Persona).find(assistant_params[:persona_id]) if assistant_params[:persona_id].present?
20
22
 
21
23
  if @assistant.save
22
24
  redirect_to layered_assistant.assistants_path, notice: "Assistant was successfully created."
@@ -30,7 +32,11 @@ module Layered
30
32
  end
31
33
 
32
34
  def update
33
- if @assistant.update(assistant_params)
35
+ if assistant_params.key?(:persona_id)
36
+ @assistant.persona = assistant_params[:persona_id].present? ? scoped(Persona).find(assistant_params[:persona_id]) : nil
37
+ end
38
+
39
+ if @assistant.update(assistant_params.except(:persona_id))
34
40
  redirect_to layered_assistant.assistants_path, notice: "Assistant was successfully updated."
35
41
  else
36
42
  render :edit, status: :unprocessable_entity
@@ -52,8 +58,12 @@ module Layered
52
58
  @models = Model.available
53
59
  end
54
60
 
61
+ def set_personas
62
+ @personas = scoped(Persona).by_name
63
+ end
64
+
55
65
  def assistant_params
56
- params.require(:assistant).permit(:name, :description, :system_prompt, :default_model_id, :public)
66
+ params.require(:assistant).permit(:name, :description, :instructions, :default_model_id, :persona_id, :public)
57
67
  end
58
68
  end
59
69
  end
@@ -0,0 +1,60 @@
1
+ module Layered
2
+ module Assistant
3
+ class PersonasController < ApplicationController
4
+ before_action :set_persona, only: [:edit, :update, :destroy]
5
+
6
+ def index
7
+ @page_title = "Personas"
8
+ @pagy, @personas = pagy(scoped(Persona).by_name)
9
+ end
10
+
11
+ def new
12
+ @page_title = "New persona"
13
+ @persona = Persona.new
14
+ end
15
+
16
+ def create
17
+ @persona = Persona.new(persona_params)
18
+ @persona.owner = l_ui_current_user
19
+
20
+ if @persona.save
21
+ redirect_to layered_assistant.personas_path, notice: "Persona was successfully created."
22
+ else
23
+ render :new, status: :unprocessable_entity
24
+ end
25
+ end
26
+
27
+ def edit
28
+ @page_title = "Edit persona"
29
+ end
30
+
31
+ def update
32
+ if @persona.update(persona_params)
33
+ redirect_to layered_assistant.personas_path, notice: "Persona was successfully updated."
34
+ else
35
+ render :edit, status: :unprocessable_entity
36
+ end
37
+ end
38
+
39
+ def destroy
40
+ if @persona.destroy
41
+ redirect_to layered_assistant.personas_path, notice: "Persona was successfully deleted."
42
+ else
43
+ redirect_to layered_assistant.personas_path, alert: "Persona could not be deleted: #{@persona.errors.full_messages.to_sentence}."
44
+ end
45
+ rescue ActiveRecord::InvalidForeignKey
46
+ redirect_to layered_assistant.personas_path, alert: "Persona could not be deleted because it is assigned to assistants."
47
+ end
48
+
49
+ private
50
+
51
+ def set_persona
52
+ @persona = scoped(Persona).find(params[:id])
53
+ end
54
+
55
+ def persona_params
56
+ params.require(:persona).permit(:name, :description, :instructions)
57
+ end
58
+ end
59
+ end
60
+ end
@@ -7,6 +7,7 @@ module Layered
7
7
  # Associations
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
+ belongs_to :persona, optional: true, counter_cache: :assistants_count
10
11
  has_many :conversations, dependent: :destroy
11
12
 
12
13
  # Validations
@@ -10,6 +10,9 @@ module Layered
10
10
  belongs_to :subject, polymorphic: true, optional: true
11
11
  has_many :messages, dependent: :destroy
12
12
 
13
+ # Callbacks
14
+ after_create :create_system_message
15
+
13
16
  # Validations
14
17
  validates :name, presence: true
15
18
 
@@ -73,6 +76,13 @@ module Layered
73
76
 
74
77
  private
75
78
 
79
+ def create_system_message
80
+ prompt = SystemPromptService.new.call(assistant: assistant)
81
+ return if prompt.blank?
82
+
83
+ messages.create!(role: :system, content: prompt)
84
+ end
85
+
76
86
  def broadcast_name_updated(old_name)
77
87
  css_class = "#{ActionView::RecordIdentifier.dom_id(self)}_name"
78
88
  Turbo::StreamsChannel.broadcast_action_to(
@@ -0,0 +1,20 @@
1
+ module Layered
2
+ module Assistant
3
+ class Persona < ApplicationRecord
4
+ # UID
5
+ has_secure_token :uid
6
+
7
+ # Associations
8
+ belongs_to :owner, polymorphic: true, optional: true
9
+ has_many :assistants, dependent: :restrict_with_error
10
+
11
+ # Validations
12
+ validates :name, presence: true
13
+ validates :instructions, 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
@@ -4,13 +4,11 @@ module Layered
4
4
  def call(message:, stream_proc:)
5
5
  provider = message.model.provider
6
6
  client = Clients::Base.for(provider)
7
- system_prompt = message.conversation.assistant.system_prompt
8
7
 
9
8
  client.chat(
10
9
  messages: message.conversation.messages,
11
10
  model: message.model.identifier,
12
- stream_proc: stream_proc,
13
- system_prompt: system_prompt
11
+ stream_proc: stream_proc
14
12
  )
15
13
  end
16
14
  end
@@ -2,8 +2,8 @@ module Layered
2
2
  module Assistant
3
3
  module Clients
4
4
  class Anthropic < Base
5
- def chat(messages:, model:, stream_proc:, system_prompt: nil)
6
- formatted = MessagesService.new.format(messages, provider: @provider, system_prompt: system_prompt)
5
+ def chat(messages:, model:, stream_proc:)
6
+ formatted = MessagesService.new.format(messages, provider: @provider)
7
7
 
8
8
  parameters = {
9
9
  model: model,
@@ -9,7 +9,7 @@ module Layered
9
9
  raise StandardError, "API key is not set for provider #{provider.name}" if @api_key.blank?
10
10
  end
11
11
 
12
- def chat(messages:, model:, stream_proc:, system_prompt: nil)
12
+ def chat(messages:, model:, stream_proc:)
13
13
  raise NotImplementedError
14
14
  end
15
15
 
@@ -2,8 +2,8 @@ module Layered
2
2
  module Assistant
3
3
  module Clients
4
4
  class OpenAI < Base
5
- def chat(messages:, model:, stream_proc:, system_prompt: nil)
6
- formatted = MessagesService.new.format(messages, provider: @provider, system_prompt: system_prompt)
5
+ def chat(messages:, model:, stream_proc:)
6
+ formatted = MessagesService.new.format(messages, provider: @provider)
7
7
 
8
8
  client_options = {
9
9
  access_token: @api_key,
@@ -1,21 +1,20 @@
1
1
  module Layered
2
2
  module Assistant
3
3
  class MessagesService
4
- def format(messages, provider: nil, system_prompt: nil)
4
+ def format(messages, provider: nil)
5
5
  protocol = provider&.protocol
6
6
 
7
7
  if protocol == "openai"
8
- format_openai(messages, system_prompt: system_prompt)
8
+ format_openai(messages)
9
9
  else
10
- format_anthropic(messages, system_prompt: system_prompt)
10
+ format_anthropic(messages)
11
11
  end
12
12
  end
13
13
 
14
14
  private
15
15
 
16
- def format_anthropic(messages, system_prompt: nil)
16
+ def format_anthropic(messages)
17
17
  system_messages = []
18
- system_messages << system_prompt if system_prompt.present?
19
18
  regular_messages = []
20
19
 
21
20
  messages.by_created_at.each do |message|
@@ -35,9 +34,8 @@ module Layered
35
34
  result
36
35
  end
37
36
 
38
- def format_openai(messages, system_prompt: nil)
37
+ def format_openai(messages)
39
38
  formatted = []
40
- formatted << { role: "system", content: system_prompt } if system_prompt.present?
41
39
 
42
40
  messages.by_created_at.each do |message|
43
41
  case message.role
@@ -0,0 +1,17 @@
1
+ module Layered
2
+ module Assistant
3
+ class SystemPromptService
4
+ def call(assistant:)
5
+ parts = []
6
+
7
+ if assistant.persona&.instructions.present?
8
+ parts << "**Persona**\n\n#{assistant.persona.instructions}"
9
+ end
10
+
11
+ parts << assistant.instructions if assistant.instructions.present?
12
+
13
+ parts.join("\n\n").presence
14
+ end
15
+ end
16
+ end
17
+ end
@@ -14,9 +14,15 @@
14
14
  </div>
15
15
 
16
16
  <div class="l-ui-form__group">
17
- <%= render "layered_ui/shared/label", form: f, field: :system_prompt, name: "System prompt", required: false %>
18
- <%= f.text_area :system_prompt, class: "l-ui-form__field", rows: 5 %>
19
- <%= render "layered_ui/shared/field_error", object: assistant, field: :system_prompt %>
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: assistant, field: :instructions %>
20
+ </div>
21
+
22
+ <div class="l-ui-form__group">
23
+ <%= render "layered_ui/shared/label", form: f, field: :persona_id, name: "Persona", required: false %>
24
+ <%= f.select :persona_id, @personas.map { |p| [p.name, p.id] }, { include_blank: "None" }, class: "l-ui-select" %>
25
+ <%= render "layered_ui/shared/field_error", object: assistant, field: :persona_id %>
20
26
  </div>
21
27
 
22
28
  <div class="l-ui-form__group">
@@ -8,7 +8,6 @@
8
8
 
9
9
  <div class="l-ui-conversation__messages" data-controller="messages">
10
10
  <div id="<%= dom_id(@conversation) %>_messages" class="<%= dom_id(@conversation) %>_messages l-ui-conversation" aria-live="polite" data-messages-target="list">
11
- <%= render "layered/assistant/messages/system_prompt", conversation: @conversation %>
12
11
  <%= render partial: "layered/assistant/messages/message", collection: @messages, as: :message %>
13
12
  </div>
14
13
 
@@ -6,7 +6,6 @@
6
6
 
7
7
  <div class="l-ui-conversation__messages" data-controller="messages">
8
8
  <div id="panel_<%= dom_id(@conversation) %>_messages" class="<%= dom_id(@conversation) %>_messages l-ui-conversation" aria-live="polite" data-messages-target="list">
9
- <%= render "layered/assistant/messages/system_prompt", conversation: @conversation %>
10
9
  <%= render partial: "layered/assistant/messages/message", collection: @messages, as: :message %>
11
10
  </div>
12
11
 
@@ -0,0 +1,31 @@
1
+ <%= form_with(model: persona, url: url, html: { class: "l-ui-form" }) do |f| %>
2
+ <%= render "layered_ui/shared/form_errors", item: persona %>
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: persona, 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: persona, field: :description %>
14
+ </div>
15
+
16
+ <div class="l-ui-form__group">
17
+ <%= render "layered_ui/shared/label", form: f, field: :instructions, required: true %>
18
+ <%= f.text_area :instructions, class: "l-ui-form__field", rows: 5 %>
19
+ <p class="l-ui-form__hint">Shape the personality and tone of conversations, used alongside the assistant's own instructions. e.g. "Reply in British English with a formal tone"</p>
20
+ <%= render "layered_ui/shared/field_error", object: persona, field: :instructions %>
21
+ </div>
22
+
23
+ <div class="l-ui-container--spread l-ui-utility--mt-2xl">
24
+ <% if persona.persisted? %>
25
+ <%= link_to "Delete", layered_assistant.persona_path(persona), class: "l-ui-button--outline-danger", data: { turbo_method: :delete, turbo_confirm: "Are you sure?" } %>
26
+ <% else %>
27
+ <span></span>
28
+ <% end %>
29
+ <%= f.submit persona.new_record? ? "Create" : "Update", class: "l-ui-button--primary" %>
30
+ </div>
31
+ <% end %>
@@ -0,0 +1,2 @@
1
+ <h1>Edit persona</h1>
2
+ <%= render "form", persona: @persona, url: layered_assistant.persona_path(@persona) %>
@@ -0,0 +1,41 @@
1
+ <div class="l-ui-container--spread">
2
+ <h1>Personas</h1>
3
+ <%= link_to "New", layered_assistant.new_persona_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">Personas</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
+ <% @personas.each do |persona| %>
20
+ <tr>
21
+ <th scope="row" class="l-ui-table__cell--primary">
22
+ <%= persona.name %>
23
+ </th>
24
+ <td class="l-ui-table__cell">
25
+ <%= truncate(persona.description, length: 60) %>
26
+ </td>
27
+ <td class="l-ui-table__cell">
28
+ <%= persona.assistants_count %>
29
+ </td>
30
+ <td class="l-ui-table__cell--action">
31
+ <%= link_to "Edit", layered_assistant.edit_persona_path(persona) %>
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 persona</h1>
2
+ <%= render "form", persona: @persona, url: layered_assistant.personas_path %>
@@ -9,6 +9,7 @@
9
9
  <% if l_assistant_accessible? %>
10
10
  <%= l_ui_navigation_item "Setup", layered_assistant.root_path %>
11
11
  <%= l_ui_navigation_item "Providers", layered_assistant.providers_path %>
12
+ <%= l_ui_navigation_item "Personas", layered_assistant.personas_path %>
12
13
  <%= l_ui_navigation_item "Assistants", layered_assistant.assistants_path %>
13
14
  <%= l_ui_navigation_item "Conversations", layered_assistant.conversations_path %>
14
15
  <% else %>
data/config/routes.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  Layered::Assistant::Engine.routes.draw do
2
2
  root "setup#index"
3
+ resources :personas, except: [:show]
3
4
  resources :assistants, except: [:show] do
4
5
  resources :conversations, only: [:index]
5
6
  end
@@ -0,0 +1,13 @@
1
+ class CreateLayeredAssistantPersonas < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :layered_assistant_personas, 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,5 @@
1
+ class AddPersonaToLayeredAssistantAssistants < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_reference :layered_assistant_assistants, :persona, foreign_key: { to_table: :layered_assistant_personas }
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class RenameSystemPromptToInstructions < ActiveRecord::Migration[8.0]
2
+ def change
3
+ rename_column :layered_assistant_assistants, :system_prompt, :instructions
4
+ end
5
+ end
@@ -33,10 +33,8 @@
33
33
  # have access to current_user and other helpers. Return an ActiveRecord
34
34
  # relation (e.g. model_class.where(...) or model_class.all).
35
35
  #
36
- # Models passed through the scope block:
37
- # - Layered::Assistant::Conversation (has polymorphic owner)
38
- # - Layered::Assistant::Assistant (has polymorphic owner)
39
- # - Layered::Assistant::Provider (has polymorphic owner)
36
+ # All engine models with a polymorphic owner association are passed through
37
+ # the scope block.
40
38
  #
41
39
  # Scope all owned resources to the current user:
42
40
  #
@@ -1,5 +1,5 @@
1
1
  module Layered
2
2
  module Assistant
3
- VERSION = "0.2.2"
3
+ VERSION = "0.3.1"
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.2.2
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - layered.ai
@@ -313,6 +313,7 @@ files:
313
313
  - app/controllers/layered/assistant/models_controller.rb
314
314
  - app/controllers/layered/assistant/panel/conversations_controller.rb
315
315
  - app/controllers/layered/assistant/panel/messages_controller.rb
316
+ - app/controllers/layered/assistant/personas_controller.rb
316
317
  - app/controllers/layered/assistant/providers_controller.rb
317
318
  - app/controllers/layered/assistant/public/application_controller.rb
318
319
  - app/controllers/layered/assistant/public/assistants_controller.rb
@@ -339,6 +340,7 @@ files:
339
340
  - app/models/layered/assistant/conversation.rb
340
341
  - app/models/layered/assistant/message.rb
341
342
  - app/models/layered/assistant/model.rb
343
+ - app/models/layered/assistant/persona.rb
342
344
  - app/models/layered/assistant/provider.rb
343
345
  - app/services/layered/assistant/chunk_parser.rb
344
346
  - app/services/layered/assistant/chunk_service.rb
@@ -349,6 +351,7 @@ files:
349
351
  - app/services/layered/assistant/messages_service.rb
350
352
  - app/services/layered/assistant/models/create_service.rb
351
353
  - app/services/layered/assistant/response_timer.rb
354
+ - app/services/layered/assistant/system_prompt_service.rb
352
355
  - app/services/layered/assistant/token_estimator.rb
353
356
  - app/views/layered/assistant/assistants/_form.html.erb
354
357
  - app/views/layered/assistant/assistants/edit.html.erb
@@ -362,7 +365,6 @@ files:
362
365
  - app/views/layered/assistant/messages/_composer.html.erb
363
366
  - app/views/layered/assistant/messages/_composer_fields.html.erb
364
367
  - app/views/layered/assistant/messages/_message.html.erb
365
- - app/views/layered/assistant/messages/_system_prompt.html.erb
366
368
  - app/views/layered/assistant/messages/create.turbo_stream.erb
367
369
  - app/views/layered/assistant/messages/index.html.erb
368
370
  - app/views/layered/assistant/models/_form.html.erb
@@ -375,6 +377,10 @@ files:
375
377
  - app/views/layered/assistant/panel/conversations/show.html.erb
376
378
  - app/views/layered/assistant/panel/messages/_composer.html.erb
377
379
  - app/views/layered/assistant/panel/messages/create.turbo_stream.erb
380
+ - app/views/layered/assistant/personas/_form.html.erb
381
+ - app/views/layered/assistant/personas/edit.html.erb
382
+ - app/views/layered/assistant/personas/index.html.erb
383
+ - app/views/layered/assistant/personas/new.html.erb
378
384
  - app/views/layered/assistant/providers/_form.html.erb
379
385
  - app/views/layered/assistant/providers/edit.html.erb
380
386
  - app/views/layered/assistant/providers/index.html.erb
@@ -401,6 +407,9 @@ files:
401
407
  - db/migrate/20260315000000_add_stopped_to_layered_assistant_messages.rb
402
408
  - db/migrate/20260315100000_add_response_timing_to_layered_assistant_messages.rb
403
409
  - db/migrate/20260317000000_normalise_provider_protocol_values.rb
410
+ - db/migrate/20260403000000_create_layered_assistant_personas.rb
411
+ - db/migrate/20260403000001_add_persona_to_layered_assistant_assistants.rb
412
+ - db/migrate/20260406000000_rename_system_prompt_to_instructions.rb
404
413
  - lib/generators/layered/assistant/install_generator.rb
405
414
  - lib/generators/layered/assistant/migrations_generator.rb
406
415
  - lib/generators/layered/assistant/templates/initializer.rb
@@ -1,10 +0,0 @@
1
- <% if conversation.assistant.system_prompt.present? %>
2
- <div class="l-ui-message">
3
- <div class="l-ui-message__bubble">
4
- <div class="l-ui-message__author">System</div>
5
- <div class="l-ui-message__body">
6
- <%= conversation.assistant.system_prompt %>
7
- </div>
8
- </div>
9
- </div>
10
- <% end %>