layered-assistant-rails 0.8.0 → 0.9.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.
- checksums.yaml +4 -4
- data/README.md +7 -6
- data/app/controllers/layered/assistant/conversations_controller.rb +34 -54
- data/app/layered_resources/layered/assistant/conversation_resource.rb +87 -0
- data/app/models/layered/assistant/provider.rb +2 -1
- data/app/views/layered/assistant/panel/conversations/index.html.erb +11 -42
- data/app/views/layered/assistant/providers/new.html.erb +2 -1
- data/app/views/layered/assistant/public/panel/conversations/index.html.erb +8 -28
- data/config/routes.rb +7 -3
- data/data/models.json +34 -23
- data/lib/layered/assistant/version.rb +1 -1
- metadata +7 -10
- data/app/views/layered/assistant/conversations/_form.html.erb +0 -29
- data/app/views/layered/assistant/conversations/edit.html.erb +0 -6
- data/app/views/layered/assistant/conversations/index.html.erb +0 -62
- data/app/views/layered/assistant/conversations/new.html.erb +0 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 68ea0fb66720732e6e05a139e873ec6eb90912967e1a6e9c956461e76d7e347c
|
|
4
|
+
data.tar.gz: f4e561987280115a3e160a42a747ee036aab7373c8f65fcc33d36eac01dafec7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 56758769136c2e033813dcf466444c778643483739bcafa5048962bca0786e167a04a6a9de9085eed038a8d492cd08c2328363cc1a98f2ea58024bd6096efd99
|
|
7
|
+
data.tar.gz: 054c1b233c696bbec3d6c7e4e643c28fc69ffc378ce5ffa11b3110de59ed9cf8bbb87c9ed582b442f437a724a7d502609171c3022b67eb43f8b2a759a8190059
|
data/README.md
CHANGED
|
@@ -434,16 +434,17 @@ Then visit `http://localhost:3000`.
|
|
|
434
434
|
|
|
435
435
|
### Deploying the dummy app
|
|
436
436
|
|
|
437
|
-
The dummy app can be deployed with [Kamal](https://kamal-deploy.org).
|
|
437
|
+
The dummy app can be deployed with [Kamal](https://kamal-deploy.org). Copy `.env.deploy.example` to `.env.deploy` and fill in the target, then deploy with `bin/deploy`:
|
|
438
438
|
|
|
439
439
|
```bash
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
kamal deploy
|
|
440
|
+
cp .env.deploy.example .env.deploy
|
|
441
|
+
bin/deploy # kamal deploy
|
|
442
|
+
bin/deploy setup # first run on a new server
|
|
443
|
+
bin/deploy logs # any other kamal subcommand passes straight through
|
|
445
444
|
```
|
|
446
445
|
|
|
446
|
+
`.env.deploy` is gitignored, so the server address stays out of the repo. The script reads it, checks the three `KAMAL_*` variables are set, and runs Kamal from `test/dummy`, where `config/deploy.yml` lives.
|
|
447
|
+
|
|
447
448
|
## Testing
|
|
448
449
|
|
|
449
450
|
Run the gem tests from the root directory:
|
|
@@ -1,80 +1,60 @@
|
|
|
1
1
|
module Layered
|
|
2
2
|
module Assistant
|
|
3
|
-
|
|
3
|
+
# Conversations are a layered resource for listing, starting, renaming
|
|
4
|
+
# and deleting them; only `show` - the conversation itself - and `stop`
|
|
5
|
+
# are this engine's own.
|
|
6
|
+
class ConversationsController < ResourcesController
|
|
4
7
|
include StoppableResponse
|
|
5
8
|
|
|
6
|
-
before_action :set_conversation, only: [ :
|
|
7
|
-
before_action :
|
|
8
|
-
|
|
9
|
-
def index
|
|
10
|
-
if params[:assistant_id]
|
|
11
|
-
@assistant = scoped(Assistant).find(params[:assistant_id])
|
|
12
|
-
@page_title = "Conversations - #{@assistant.name}"
|
|
13
|
-
@pagy, @conversations = pagy(@assistant.conversations.merge(scoped(Conversation)).includes(:owner, :user).by_created_at)
|
|
14
|
-
else
|
|
15
|
-
@page_title = "Conversations"
|
|
16
|
-
@pagy, @conversations = pagy(scoped(Conversation).includes(:assistant, :owner, :user).by_created_at)
|
|
17
|
-
end
|
|
18
|
-
end
|
|
9
|
+
before_action :set_conversation, only: [ :stop ]
|
|
10
|
+
before_action :drop_assistant_column, only: [ :index ], if: -> { params[:assistant_id] }
|
|
11
|
+
before_action :scope_choice_fields, only: [ :new, :create, :edit, :update ]
|
|
19
12
|
|
|
20
13
|
def show
|
|
21
|
-
|
|
14
|
+
super
|
|
15
|
+
@conversation = @record
|
|
22
16
|
@messages = @conversation.messages.includes(:model).by_created_at
|
|
23
17
|
@models = scoped_models
|
|
24
18
|
@selected_model_id = @messages.last&.model_id || @conversation.assistant.default_model_id || @models.first&.id
|
|
25
19
|
end
|
|
26
20
|
|
|
27
|
-
|
|
28
|
-
@page_title = "New conversation"
|
|
29
|
-
@conversation = Conversation.new(params.permit(conversation: [ :assistant_id ])[:conversation])
|
|
30
|
-
end
|
|
21
|
+
private
|
|
31
22
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
@conversation
|
|
35
|
-
@conversation.user = current_conversation_user
|
|
36
|
-
@conversation.assistant = scoped(Assistant).find(conversation_params[:assistant_id]) if conversation_params[:assistant_id].present?
|
|
37
|
-
@conversation.name = Conversation.default_name if @conversation.name.blank?
|
|
38
|
-
if @conversation.save
|
|
39
|
-
redirect_to layered_assistant.conversation_path(@conversation)
|
|
40
|
-
else
|
|
41
|
-
render :new, status: :unprocessable_entity
|
|
42
|
-
end
|
|
23
|
+
# `stop` is a custom member action, so the gem has loaded @record.
|
|
24
|
+
def set_conversation
|
|
25
|
+
@conversation = @record
|
|
43
26
|
end
|
|
44
27
|
|
|
45
|
-
|
|
46
|
-
|
|
28
|
+
# Listed under an assistant, every row would name the same one.
|
|
29
|
+
def drop_assistant_column
|
|
30
|
+
@columns = @columns.reject { |column| column[:attribute] == :assistant_id }
|
|
47
31
|
end
|
|
48
32
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
33
|
+
# The assistant is chosen once, when the conversation is started, and
|
|
34
|
+
# only from the owner's own.
|
|
35
|
+
def scope_choice_fields
|
|
36
|
+
if action_name.in?(%w[new create])
|
|
37
|
+
assistants = scoped(Assistant).by_name.map { |assistant| [ assistant.name, assistant.id ] }
|
|
38
|
+
@fields = @fields.map { |field| field[:attribute] == :assistant_id ? field.merge(collection: assistants) : field }
|
|
52
39
|
else
|
|
53
|
-
|
|
40
|
+
@fields = @fields.reject { |field| field[:attribute] == :assistant_id }
|
|
54
41
|
end
|
|
55
42
|
end
|
|
56
43
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def set_conversation
|
|
65
|
-
@conversation = scoped(Conversation).find_by!(uid: params[:id])
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def set_assistants
|
|
69
|
-
@assistants = scoped(Assistant).by_name
|
|
70
|
-
end
|
|
44
|
+
# An out-of-scope assistant 404s rather than being silently dropped,
|
|
45
|
+
# matching how a record itself is looked up. A blank one leaves any
|
|
46
|
+
# assistant the route already chose in place.
|
|
47
|
+
def layered_resource_params
|
|
48
|
+
attributes = super
|
|
49
|
+
return attributes.except(:assistant_id) unless action_name == "create"
|
|
71
50
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
params.require(:conversation).permit(:name, :assistant_id)
|
|
51
|
+
if attributes[:assistant_id].present?
|
|
52
|
+
attributes[:assistant_id] = scoped(Assistant).find(attributes[:assistant_id]).id
|
|
75
53
|
else
|
|
76
|
-
|
|
54
|
+
attributes.delete(:assistant_id)
|
|
77
55
|
end
|
|
56
|
+
attributes[:name] = Conversation.default_name if attributes[:name].blank?
|
|
57
|
+
attributes
|
|
78
58
|
end
|
|
79
59
|
end
|
|
80
60
|
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
module Layered
|
|
2
|
+
module Assistant
|
|
3
|
+
class ConversationResource < Layered::Resource::Base
|
|
4
|
+
model Conversation
|
|
5
|
+
|
|
6
|
+
# Conversations are addressed by uid, not id.
|
|
7
|
+
lookup_attribute :uid
|
|
8
|
+
|
|
9
|
+
columns [
|
|
10
|
+
# The name links to the conversation itself, not its edit form.
|
|
11
|
+
{ attribute: :name, primary: true, link: false,
|
|
12
|
+
render: ->(record, view) {
|
|
13
|
+
view.link_to(record.name, view.layered_assistant.conversation_path(record),
|
|
14
|
+
data: { turbo_frame: "_top" })
|
|
15
|
+
} },
|
|
16
|
+
{ attribute: :assistant_id, label: "Assistant", sortable: false,
|
|
17
|
+
render: ->(record, view) {
|
|
18
|
+
view.link_to(record.assistant.name,
|
|
19
|
+
view.layered_assistant.assistant_conversations_path(record.assistant),
|
|
20
|
+
data: { turbo_frame: "_top" })
|
|
21
|
+
} },
|
|
22
|
+
{ attribute: :messages_count, label: "Messages",
|
|
23
|
+
render: ->(record, view) {
|
|
24
|
+
view.link_to(record.messages_count.to_i,
|
|
25
|
+
view.layered_assistant.conversation_messages_path(record),
|
|
26
|
+
data: { turbo_frame: "_top" })
|
|
27
|
+
} },
|
|
28
|
+
{ attribute: :token_estimate, label: "Tokens",
|
|
29
|
+
render: ->(record, view) { view.number_with_delimiter(record.token_estimate.to_i) } },
|
|
30
|
+
# The person who did the talking, which is the owner itself until an
|
|
31
|
+
# owner block scopes ownership elsewhere. Neither is set for an
|
|
32
|
+
# anonymous visitor on a public assistant.
|
|
33
|
+
{ attribute: :user_id, label: "User", sortable: false,
|
|
34
|
+
render: ->(record) { record.user.try(:name) || record.owner.try(:name) || "Guest" } },
|
|
35
|
+
{ attribute: :created_at, label: "Created",
|
|
36
|
+
render: ->(record) { record.created_at.to_fs(:short) } }
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
search_fields [ :name ]
|
|
40
|
+
|
|
41
|
+
default_sort attribute: :created_at, direction: :desc
|
|
42
|
+
|
|
43
|
+
# `assistant_id` carries no collection here: the options have to be
|
|
44
|
+
# scoped to the owner, which only the controller can resolve.
|
|
45
|
+
# ConversationsController fills them in per request, and drops the
|
|
46
|
+
# field once the conversation exists - its assistant is fixed.
|
|
47
|
+
fields [
|
|
48
|
+
{ attribute: :assistant_id, label: "Assistant", as: :select, include_blank: "Select an assistant:", required: true },
|
|
49
|
+
# Left blank, the name is set from the first message, so the form
|
|
50
|
+
# must not insist on one even though the model does.
|
|
51
|
+
{ attribute: :name, required: false, hint: "Auto-set from first message if left blank" }
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
# Nested under an assistant, only that assistant's conversations are
|
|
55
|
+
# listed, and an out-of-scope assistant 404s before any are reached.
|
|
56
|
+
def self.scope(controller)
|
|
57
|
+
scope = controller.send(:scoped, Conversation)
|
|
58
|
+
if (assistant_id = controller.params[:assistant_id])
|
|
59
|
+
scope = scope.where(assistant: controller.send(:scoped, Assistant).find(assistant_id))
|
|
60
|
+
end
|
|
61
|
+
scope.includes(:assistant, :owner, :user)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Started from an assistant's conversations, the assistant is chosen
|
|
65
|
+
# already - resolved through `scoped` so an out-of-scope one 404s.
|
|
66
|
+
def self.build_record(controller)
|
|
67
|
+
assistant_id = controller.params[:assistant_id]
|
|
68
|
+
|
|
69
|
+
Conversation.new(
|
|
70
|
+
owner: controller.send(:current_owner!),
|
|
71
|
+
user: controller.send(:current_conversation_user),
|
|
72
|
+
assistant: (controller.send(:scoped, Assistant).find(assistant_id) if assistant_id)
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# A new conversation opens straight into the chat; edits and deletes
|
|
77
|
+
# return to the list they were made from.
|
|
78
|
+
def self.after_save_path(controller, record)
|
|
79
|
+
if controller.action_name == "create"
|
|
80
|
+
controller.layered_assistant.conversation_path(record)
|
|
81
|
+
else
|
|
82
|
+
controller.layered_assistant.conversations_path
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -38,8 +38,9 @@ module Layered
|
|
|
38
38
|
{ key: "openai", name: "OpenAI", description: "GPT family of models. Requires an API key.", protocol: "openai", url: "https://api.openai.com/v1", keys_url: "https://platform.openai.com/api-keys" },
|
|
39
39
|
{ key: "gemini", name: "Gemini", description: "Google Gemini family of models. Requires an API key.", protocol: "openai", url: "https://generativelanguage.googleapis.com/v1beta/openai/", keys_url: "https://aistudio.google.com/api-keys" },
|
|
40
40
|
{ key: "mistral", name: "Mistral", description: "Mistral's own frontier models. Requires an API key.", protocol: "openai", url: "https://api.mistral.ai/v1", keys_url: "https://admin.mistral.ai/organization/api-keys" },
|
|
41
|
-
{ key: "groq", name: "Groq", description: "Low-latency inference for popular open-weight models. Requires an API key.", protocol: "openai", url: "https://api.groq.com/openai/v1", keys_url: "https://console.groq.com/keys" },
|
|
42
41
|
{ key: "openrouter", name: "OpenRouter", description: "Access hundreds of models through a single API. Requires an API key.", protocol: "openai", url: "https://openrouter.ai/api/v1/", keys_url: "https://openrouter.ai/settings/keys" },
|
|
42
|
+
{ key: "together", name: "Together AI", description: "Serverless inference for open-weight models. Requires an API key.", protocol: "openai", url: "https://api.together.ai/v1", keys_url: "https://api.together.ai/settings/projects/~current/api-keys" },
|
|
43
|
+
{ key: "groq", name: "Groq", description: "Low-latency inference for popular open-weight models. Requires an API key.", protocol: "openai", url: "https://api.groq.com/openai/v1", keys_url: "https://console.groq.com/keys" },
|
|
43
44
|
{ key: "fireworks", name: "Fireworks", description: "Serverless inference for open-weight models. Requires an API key.", protocol: "openai", url: "https://api.fireworks.ai/inference/v1", keys_url: "https://app.fireworks.ai/settings/users/api-keys" }
|
|
44
45
|
],
|
|
45
46
|
"Local" => [
|
|
@@ -1,46 +1,15 @@
|
|
|
1
1
|
<%= render "layered/assistant/panel/conversations/header", assistants: @assistants, conversations: @conversations, selected: layered_assistant.panel_conversations_path %>
|
|
2
2
|
|
|
3
3
|
<%= turbo_frame_tag "assistant_panel" do %>
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
</thead>
|
|
16
|
-
|
|
17
|
-
<tbody class="l-ui-table__body">
|
|
18
|
-
<% if @conversations.any? %>
|
|
19
|
-
<% @conversations.each do |conversation| %>
|
|
20
|
-
<tr>
|
|
21
|
-
<td class="l-ui-table__cell">
|
|
22
|
-
<%= conversation.created_at.to_fs(:short) %>
|
|
23
|
-
</td>
|
|
24
|
-
<th scope="row" class="l-ui-table__cell l-ui-table__cell--primary">
|
|
25
|
-
<%= link_to conversation.name, layered_assistant.panel_conversation_path(conversation), data: { turbo_frame: "assistant_panel" } %>
|
|
26
|
-
</th>
|
|
27
|
-
<td class="l-ui-table__cell">
|
|
28
|
-
<%= link_to conversation.messages_count || 0, layered_assistant.conversation_messages_path(conversation), data: { turbo_frame: "_top" } %>
|
|
29
|
-
</td>
|
|
30
|
-
<td class="l-ui-table__cell">
|
|
31
|
-
<%= number_with_delimiter(conversation.token_estimate.to_i) %>
|
|
32
|
-
</td>
|
|
33
|
-
<td class="l-ui-table__cell l-ui-table__cell--action">
|
|
34
|
-
<%= button_to "Delete", layered_assistant.panel_conversation_path(conversation), method: :delete, class: "l-ui-table__action l-ui-table__action--danger", data: { turbo_confirm: "Delete this conversation?" } %>
|
|
35
|
-
</td>
|
|
36
|
-
</tr>
|
|
37
|
-
<% end %>
|
|
38
|
-
<% else %>
|
|
39
|
-
<tr>
|
|
40
|
-
<td class="l-ui-table__cell" colspan="5">No conversations yet.</td>
|
|
41
|
-
</tr>
|
|
42
|
-
<% end %>
|
|
43
|
-
</tbody>
|
|
44
|
-
</table>
|
|
45
|
-
</div>
|
|
4
|
+
<%= l_ui_table(@conversations,
|
|
5
|
+
caption: "Conversations in panel",
|
|
6
|
+
columns: [
|
|
7
|
+
{ attribute: :created_at, label: "Created", render: ->(c) { c.created_at.to_fs(:short) } },
|
|
8
|
+
{ attribute: :name, primary: true, render: ->(c) { link_to c.name, layered_assistant.panel_conversation_path(c), data: { turbo_frame: "assistant_panel", turbo_prefetch: false } } },
|
|
9
|
+
{ attribute: :messages_count, label: "Messages", render: ->(c) { link_to c.messages_count || 0, layered_assistant.conversation_messages_path(c), data: { turbo_frame: "_top", turbo_prefetch: false } } },
|
|
10
|
+
{ attribute: :token_estimate, label: "Tokens", render: ->(c) { number_with_delimiter(c.token_estimate.to_i) } }
|
|
11
|
+
],
|
|
12
|
+
actions: ->(c) { button_to "Delete", layered_assistant.panel_conversation_path(c), method: :delete, class: "l-ui-table__action l-ui-table__action--danger", data: { turbo_confirm: "Delete this conversation?" } },
|
|
13
|
+
floating_actions: true
|
|
14
|
+
) %>
|
|
46
15
|
<% end %>
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
<div class="l-ui-spread">
|
|
38
38
|
<div>
|
|
39
39
|
<%= l_ui_breadcrumbs do %>
|
|
40
|
-
<%=
|
|
40
|
+
<%= layered_breadcrumb_item @model.model_name.human.pluralize, layered_collection_path %>
|
|
41
|
+
<%= layered_breadcrumb_item "New", current: true %>
|
|
41
42
|
<% end %>
|
|
42
43
|
|
|
43
44
|
<h1 class="l-ui-heading">New <%= @model.model_name.human.downcase %></h1>
|
|
@@ -9,34 +9,14 @@
|
|
|
9
9
|
<% end %>
|
|
10
10
|
|
|
11
11
|
<% if @conversations.any? %>
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
</tr>
|
|
21
|
-
</thead>
|
|
22
|
-
|
|
23
|
-
<tbody class="l-ui-table__body">
|
|
24
|
-
<% @conversations.each do |conversation| %>
|
|
25
|
-
<tr>
|
|
26
|
-
<td class="l-ui-table__cell">
|
|
27
|
-
<%= conversation.created_at.to_fs(:short) %>
|
|
28
|
-
</td>
|
|
29
|
-
<th scope="row" class="l-ui-table__cell l-ui-table__cell--primary">
|
|
30
|
-
<%= link_to conversation.name, layered_assistant.public_panel_conversation_path(conversation), data: { turbo_frame: "assistant_panel" } %>
|
|
31
|
-
</th>
|
|
32
|
-
<td class="l-ui-table__cell">
|
|
33
|
-
<%= conversation.messages_count || 0 %>
|
|
34
|
-
</td>
|
|
35
|
-
</tr>
|
|
36
|
-
<% end %>
|
|
37
|
-
</tbody>
|
|
38
|
-
</table>
|
|
39
|
-
</div>
|
|
12
|
+
<%= l_ui_table(@conversations,
|
|
13
|
+
caption: "Your conversations",
|
|
14
|
+
columns: [
|
|
15
|
+
{ attribute: :created_at, label: "Created", render: ->(c) { c.created_at.to_fs(:short) } },
|
|
16
|
+
{ attribute: :name, primary: true, render: ->(c) { link_to c.name, layered_assistant.public_panel_conversation_path(c), data: { turbo_frame: "assistant_panel", turbo_prefetch: false } } },
|
|
17
|
+
{ attribute: :messages_count, label: "Messages", render: ->(c) { c.messages_count || 0 } }
|
|
18
|
+
]
|
|
19
|
+
) %>
|
|
40
20
|
<% end %>
|
|
41
21
|
|
|
42
22
|
<%= form_with url: layered_assistant.public_panel_conversations_path(assistant_id: @assistant.id), method: :post do |f| %>
|
data/config/routes.rb
CHANGED
|
@@ -4,14 +4,18 @@ Layered::Assistant::Engine.routes.draw do
|
|
|
4
4
|
layered_resources :skills, namespace: "Layered::Assistant", except: [ :show ]
|
|
5
5
|
layered_resources :assistants, namespace: "Layered::Assistant", controller: "assistants", except: [ :show ]
|
|
6
6
|
resources :assistants, only: [] do
|
|
7
|
-
|
|
7
|
+
layered_resources :conversations, namespace: "Layered::Assistant", controller: "conversations", only: [ :index, :new, :create ]
|
|
8
8
|
end
|
|
9
9
|
layered_resources :providers, namespace: "Layered::Assistant", controller: "providers", except: [ :show ]
|
|
10
10
|
resources :providers, only: [] do
|
|
11
11
|
layered_resources :models, namespace: "Layered::Assistant", except: [ :show ]
|
|
12
12
|
end
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
layered_resources :conversations, namespace: "Layered::Assistant", controller: "conversations" do
|
|
14
|
+
member do
|
|
15
|
+
patch :stop
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
resources :conversations, only: [] do
|
|
15
19
|
resources :messages, only: [ :index, :create, :destroy ]
|
|
16
20
|
# Approving a tool call is the same act wherever it is rendered, and the
|
|
17
21
|
# partial carrying the buttons is broadcast rather than requested, so it
|
data/data/models.json
CHANGED
|
@@ -1,55 +1,66 @@
|
|
|
1
1
|
{
|
|
2
2
|
"Anthropic": [
|
|
3
|
+
{ "name": "Claude Opus 5.5", "identifier": "claude-opus-5-5" },
|
|
4
|
+
{ "name": "Claude Sonnet 5.5", "identifier": "claude-sonnet-5-5" },
|
|
5
|
+
{ "name": "Claude Fable 5.1", "identifier": "claude-fable-5-1" },
|
|
3
6
|
{ "name": "Claude Opus 5", "identifier": "claude-opus-5" },
|
|
4
|
-
{ "name": "Claude Sonnet 5", "identifier": "claude-sonnet-5" },
|
|
5
|
-
{ "name": "Claude Fable 5", "identifier": "claude-fable-5" },
|
|
6
|
-
{ "name": "Claude Opus 4.8", "identifier": "claude-opus-4-8" },
|
|
7
7
|
{ "name": "Claude Haiku 4.5", "identifier": "claude-haiku-4-5" }
|
|
8
8
|
],
|
|
9
9
|
"OpenAI": [
|
|
10
|
-
{ "name": "GPT-
|
|
11
|
-
{ "name": "GPT-
|
|
12
|
-
{ "name": "GPT-
|
|
13
|
-
{ "name": "GPT-5.
|
|
10
|
+
{ "name": "GPT-6 Astra", "identifier": "gpt-6-astra" },
|
|
11
|
+
{ "name": "GPT-6.1 Sol", "identifier": "gpt-6.1-sol" },
|
|
12
|
+
{ "name": "GPT-6 Luna", "identifier": "gpt-6-luna" },
|
|
13
|
+
{ "name": "GPT-5.6 Sol", "identifier": "gpt-5.6-sol" }
|
|
14
14
|
],
|
|
15
15
|
"Gemini": [
|
|
16
|
+
{ "name": "Gemini 3.8 Flash", "identifier": "gemini-3.8-flash" },
|
|
17
|
+
{ "name": "Gemini 3.7 Flash", "identifier": "gemini-3.7-flash" },
|
|
16
18
|
{ "name": "Gemini 3.6 Flash", "identifier": "gemini-3.6-flash" },
|
|
17
19
|
{ "name": "Gemini 3.5 Flash", "identifier": "gemini-3.5-flash" },
|
|
18
20
|
{ "name": "Gemini 3.5 Flash-Lite", "identifier": "gemini-3.5-flash-lite" },
|
|
19
21
|
{ "name": "Gemini 3.1 Pro Preview", "identifier": "gemini-3.1-pro-preview" },
|
|
20
|
-
{ "name": "Gemini 3.1 Flash-Lite", "identifier": "gemini-3.1-flash-lite" }
|
|
21
|
-
{ "name": "Gemini 2.5 Pro", "identifier": "gemini-2.5-pro" },
|
|
22
|
-
{ "name": "Gemini 2.5 Flash", "identifier": "gemini-2.5-flash" },
|
|
23
|
-
{ "name": "Gemini 2.5 Flash-Lite", "identifier": "gemini-2.5-flash-lite" }
|
|
22
|
+
{ "name": "Gemini 3.1 Flash-Lite", "identifier": "gemini-3.1-flash-lite" }
|
|
24
23
|
],
|
|
25
24
|
"OpenRouter": [
|
|
26
25
|
{ "name": "Auto Router (Beta)", "identifier": "openrouter/auto-beta" },
|
|
27
|
-
{ "name": "Qwen3.8 Max", "identifier": "qwen/qwen3.8-max" },
|
|
28
|
-
{ "name": "DeepSeek V4 Flash
|
|
26
|
+
{ "name": "Qwen3.8 Max (0902)", "identifier": "qwen/qwen3.8-max-0902" },
|
|
27
|
+
{ "name": "DeepSeek V4.1 Flash", "identifier": "deepseek/deepseek-v4.1-flash" },
|
|
28
|
+
{ "name": "Grok 4.7", "identifier": "x-ai/grok-4.7" },
|
|
29
|
+
{ "name": "GLM 5.3", "identifier": "z-ai/glm-5.3" },
|
|
29
30
|
{ "name": "Kimi K3", "identifier": "moonshotai/kimi-k3" },
|
|
30
|
-
{ "name": "Grok 4.5", "identifier": "x-ai/grok-4.5" },
|
|
31
|
-
{ "name": "GLM-5.2", "identifier": "z-ai/glm-5.2" },
|
|
32
31
|
{ "name": "Kimi K2.7 Code", "identifier": "moonshotai/kimi-k2.7-code" },
|
|
33
32
|
{ "name": "MiniMax M3", "identifier": "minimax/minimax-m3" },
|
|
34
|
-
{ "name": "DeepSeek V4 Pro", "identifier": "deepseek/deepseek-v4-pro" }
|
|
33
|
+
{ "name": "DeepSeek V4 Pro 0813", "identifier": "deepseek/deepseek-v4-pro-0813" }
|
|
35
34
|
],
|
|
36
35
|
"Groq": [
|
|
37
|
-
{ "name": "Qwen3.
|
|
36
|
+
{ "name": "Qwen3.8 27B", "identifier": "qwen/qwen3.8-27b" },
|
|
38
37
|
{ "name": "GPT-OSS 120B", "identifier": "openai/gpt-oss-120b" },
|
|
39
38
|
{ "name": "GPT-OSS 20B", "identifier": "openai/gpt-oss-20b" },
|
|
40
|
-
{ "name": "
|
|
41
|
-
{ "name": "Compound Mini", "identifier": "groq/compound-mini" }
|
|
39
|
+
{ "name": "Llama 3.3 70B", "identifier": "llama-3.3-70b-versatile" }
|
|
42
40
|
],
|
|
43
41
|
"Fireworks": [
|
|
44
|
-
{ "name": "
|
|
42
|
+
{ "name": "Ember-1", "identifier": "accounts/fireworks/models/ember-1" },
|
|
43
|
+
{ "name": "DeepSeek V4.1 Flash", "identifier": "accounts/fireworks/models/deepseek-v4p1-flash" },
|
|
44
|
+
{ "name": "GLM 5.3", "identifier": "accounts/fireworks/models/glm-5p3" },
|
|
45
|
+
{ "name": "GLM 5.3 Flash", "identifier": "accounts/fireworks/models/glm-5p3-flash" },
|
|
45
46
|
{ "name": "Kimi K3", "identifier": "accounts/fireworks/models/kimi-k3" },
|
|
46
|
-
{ "name": "
|
|
47
|
-
{ "name": "Qwen3.7 Plus", "identifier": "accounts/fireworks/models/qwen3p7-plus" },
|
|
47
|
+
{ "name": "Qwen3.8 Max", "identifier": "accounts/fireworks/models/qwen3p8-max" },
|
|
48
48
|
{ "name": "Kimi K2.7 Code", "identifier": "accounts/fireworks/models/kimi-k2p7-code" },
|
|
49
49
|
{ "name": "MiniMax M3", "identifier": "accounts/fireworks/models/minimax-m3" },
|
|
50
|
-
{ "name": "DeepSeek V4 Pro", "identifier": "accounts/fireworks/models/deepseek-v4-pro" },
|
|
51
50
|
{ "name": "GPT-OSS 120B", "identifier": "accounts/fireworks/models/gpt-oss-120b" }
|
|
52
51
|
],
|
|
52
|
+
"Together AI": [
|
|
53
|
+
{ "name": "Qwen3.8 2.4T A95B", "identifier": "Qwen/Qwen3.8-2.4T-A95B" },
|
|
54
|
+
{ "name": "DeepSeek V4.1 Flash", "identifier": "deepseek-ai/DeepSeek-V4.1-Flash" },
|
|
55
|
+
{ "name": "Kimi K3", "identifier": "moonshotai/Kimi-K3" },
|
|
56
|
+
{ "name": "GLM 5.3", "identifier": "zai-org/GLM-5.3" },
|
|
57
|
+
{ "name": "GLM 5.3 Flash", "identifier": "zai-org/GLM-5.3-Flash" },
|
|
58
|
+
{ "name": "Qwen3.7 Max", "identifier": "Qwen/Qwen3.7-Max" },
|
|
59
|
+
{ "name": "MiniMax M3", "identifier": "MiniMaxAI/MiniMax-M3" },
|
|
60
|
+
{ "name": "DeepSeek V4 Pro 0813", "identifier": "deepseek-ai/DeepSeek-V4-Pro-0813" },
|
|
61
|
+
{ "name": "GPT-OSS 120B", "identifier": "openai/gpt-oss-120b" },
|
|
62
|
+
{ "name": "Llama 3.3 70B Instruct Turbo", "identifier": "meta-llama/Llama-3.3-70B-Instruct-Turbo" }
|
|
63
|
+
],
|
|
53
64
|
"Mistral": [
|
|
54
65
|
{ "name": "Mistral Medium 3.5", "identifier": "mistral-medium-2604" },
|
|
55
66
|
{ "name": "Mistral Small 4", "identifier": "mistral-small-2603" },
|
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.
|
|
4
|
+
version: 0.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- layered.ai
|
|
@@ -85,28 +85,28 @@ dependencies:
|
|
|
85
85
|
requirements:
|
|
86
86
|
- - "~>"
|
|
87
87
|
- !ruby/object:Gem::Version
|
|
88
|
-
version: '0.
|
|
88
|
+
version: '0.27'
|
|
89
89
|
type: :runtime
|
|
90
90
|
prerelease: false
|
|
91
91
|
version_requirements: !ruby/object:Gem::Requirement
|
|
92
92
|
requirements:
|
|
93
93
|
- - "~>"
|
|
94
94
|
- !ruby/object:Gem::Version
|
|
95
|
-
version: '0.
|
|
95
|
+
version: '0.27'
|
|
96
96
|
- !ruby/object:Gem::Dependency
|
|
97
97
|
name: layered-resource-rails
|
|
98
98
|
requirement: !ruby/object:Gem::Requirement
|
|
99
99
|
requirements:
|
|
100
100
|
- - "~>"
|
|
101
101
|
- !ruby/object:Gem::Version
|
|
102
|
-
version: '0.
|
|
102
|
+
version: '0.4'
|
|
103
103
|
type: :runtime
|
|
104
104
|
prerelease: false
|
|
105
105
|
version_requirements: !ruby/object:Gem::Requirement
|
|
106
106
|
requirements:
|
|
107
107
|
- - "~>"
|
|
108
108
|
- !ruby/object:Gem::Version
|
|
109
|
-
version: '0.
|
|
109
|
+
version: '0.4'
|
|
110
110
|
- !ruby/object:Gem::Dependency
|
|
111
111
|
name: propshaft
|
|
112
112
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -349,6 +349,7 @@ files:
|
|
|
349
349
|
- app/jobs/layered/assistant/messages/response_job.rb
|
|
350
350
|
- app/jobs/layered/assistant/messages/tool_call_job.rb
|
|
351
351
|
- app/layered_resources/layered/assistant/assistant_resource.rb
|
|
352
|
+
- app/layered_resources/layered/assistant/conversation_resource.rb
|
|
352
353
|
- app/layered_resources/layered/assistant/model_resource.rb
|
|
353
354
|
- app/layered_resources/layered/assistant/persona_resource.rb
|
|
354
355
|
- app/layered_resources/layered/assistant/provider_resource.rb
|
|
@@ -380,10 +381,6 @@ files:
|
|
|
380
381
|
- app/services/layered/assistant/tool_registry.rb
|
|
381
382
|
- app/services/layered/assistant/tool_runner_service.rb
|
|
382
383
|
- app/tools/layered/assistant/tool.rb
|
|
383
|
-
- app/views/layered/assistant/conversations/_form.html.erb
|
|
384
|
-
- app/views/layered/assistant/conversations/edit.html.erb
|
|
385
|
-
- app/views/layered/assistant/conversations/index.html.erb
|
|
386
|
-
- app/views/layered/assistant/conversations/new.html.erb
|
|
387
384
|
- app/views/layered/assistant/conversations/show.html.erb
|
|
388
385
|
- app/views/layered/assistant/messages/_composer.html.erb
|
|
389
386
|
- app/views/layered/assistant/messages/_composer_fields.html.erb
|
|
@@ -480,7 +477,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
480
477
|
- !ruby/object:Gem::Version
|
|
481
478
|
version: '0'
|
|
482
479
|
requirements: []
|
|
483
|
-
rubygems_version:
|
|
480
|
+
rubygems_version: 3.6.9
|
|
484
481
|
specification_version: 4
|
|
485
482
|
summary: Open source, multi-provider, streaming AI assistant engine for Rails 8+ built
|
|
486
483
|
on `layered-ui-rails`.
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
<%= form_with(model: conversation, url: url, html: { class: "l-ui-form" }) do |f| %>
|
|
2
|
-
<%= render "layered_ui/shared/form_errors", item: conversation %>
|
|
3
|
-
|
|
4
|
-
<% if conversation.new_record? %>
|
|
5
|
-
<div class="l-ui-form__group">
|
|
6
|
-
<%= render "layered_ui/shared/label", form: f, field: :assistant_id, name: "Assistant", required: true %>
|
|
7
|
-
<%= f.select :assistant_id, @assistants.map { |a| [a.name, a.id] }, { include_blank: "Select an assistant:" }, class: "l-ui-select" %>
|
|
8
|
-
<%= render "layered_ui/shared/field_error", object: conversation, field: :assistant %>
|
|
9
|
-
</div>
|
|
10
|
-
<% end %>
|
|
11
|
-
|
|
12
|
-
<div class="l-ui-form__group">
|
|
13
|
-
<%= render "layered_ui/shared/label", form: f, field: :name, required: false %>
|
|
14
|
-
<%= f.text_field :name, class: "l-ui-form__field" %>
|
|
15
|
-
<% if conversation.new_record? %>
|
|
16
|
-
<p class="l-ui-form__hint">Auto-set from first message if left blank</p>
|
|
17
|
-
<% end %>
|
|
18
|
-
<%= render "layered_ui/shared/field_error", object: conversation, field: :name %>
|
|
19
|
-
</div>
|
|
20
|
-
|
|
21
|
-
<div class="l-ui-spread l-ui-mt-8">
|
|
22
|
-
<% if conversation.persisted? %>
|
|
23
|
-
<%= link_to "Delete", layered_assistant.conversation_path(conversation), class: "l-ui-button l-ui-button--outline-danger", data: { turbo_method: :delete, turbo_confirm: "Are you sure?" } %>
|
|
24
|
-
<% else %>
|
|
25
|
-
<span></span>
|
|
26
|
-
<% end %>
|
|
27
|
-
<%= f.submit conversation.new_record? ? "Create" : "Update", class: "l-ui-button l-ui-button--primary" %>
|
|
28
|
-
</div>
|
|
29
|
-
<% end %>
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
<%= l_ui_title_bar(
|
|
2
|
-
title: @assistant ? "Conversations - #{@assistant.name}" : "Conversations",
|
|
3
|
-
breadcrumbs: @assistant ? [["Assistants", layered_assistant.assistants_path]] : []
|
|
4
|
-
) do %>
|
|
5
|
-
<%= link_to "New", layered_assistant.new_conversation_path(conversation: @assistant ? { assistant_id: @assistant.id } : {}), class: "l-ui-button l-ui-button--primary" %>
|
|
6
|
-
<% end %>
|
|
7
|
-
|
|
8
|
-
<div class="l-ui-table-container l-ui-mt-4">
|
|
9
|
-
<table class="l-ui-table">
|
|
10
|
-
<caption class="l-ui-sr-only"><%= @assistant ? "Conversations for #{@assistant.name}" : "Conversations" %></caption>
|
|
11
|
-
<thead class="l-ui-table__header">
|
|
12
|
-
<tr>
|
|
13
|
-
<th scope="col" class="l-ui-table__header-cell">Name</th>
|
|
14
|
-
<% unless @assistant %>
|
|
15
|
-
<th scope="col" class="l-ui-table__header-cell">Assistant</th>
|
|
16
|
-
<% end %>
|
|
17
|
-
<th scope="col" class="l-ui-table__header-cell">Messages</th>
|
|
18
|
-
<th scope="col" class="l-ui-table__header-cell">Tokens</th>
|
|
19
|
-
<th scope="col" class="l-ui-table__header-cell">User</th>
|
|
20
|
-
<th scope="col" class="l-ui-table__header-cell">Created</th>
|
|
21
|
-
<th scope="col" class="l-ui-table__header-cell l-ui-table__header-cell--action">Actions</th>
|
|
22
|
-
</tr>
|
|
23
|
-
</thead>
|
|
24
|
-
|
|
25
|
-
<tbody class="l-ui-table__body">
|
|
26
|
-
<% @conversations.each do |conversation| %>
|
|
27
|
-
<tr>
|
|
28
|
-
<th scope="row" class="l-ui-table__cell l-ui-table__cell--primary">
|
|
29
|
-
<%= link_to conversation.name, layered_assistant.conversation_path(conversation) %>
|
|
30
|
-
</th>
|
|
31
|
-
<% unless @assistant %>
|
|
32
|
-
<td class="l-ui-table__cell">
|
|
33
|
-
<%= link_to conversation.assistant.name, layered_assistant.assistant_conversations_path(conversation.assistant) %>
|
|
34
|
-
</td>
|
|
35
|
-
<% end %>
|
|
36
|
-
<td class="l-ui-table__cell">
|
|
37
|
-
<%= link_to conversation.messages_count.to_i, layered_assistant.conversation_messages_path(conversation) %>
|
|
38
|
-
</td>
|
|
39
|
-
<td class="l-ui-table__cell">
|
|
40
|
-
<%= number_with_delimiter(conversation.token_estimate.to_i) %>
|
|
41
|
-
</td>
|
|
42
|
-
<td class="l-ui-table__cell">
|
|
43
|
-
<%# The person who did the talking, which is the owner itself
|
|
44
|
-
until an owner block scopes ownership elsewhere. Neither is
|
|
45
|
-
set for an anonymous visitor on a public assistant. %>
|
|
46
|
-
<%= conversation.user.try(:name) || conversation.owner.try(:name) || "Guest" %>
|
|
47
|
-
</td>
|
|
48
|
-
<td class="l-ui-table__cell">
|
|
49
|
-
<%= conversation.created_at.to_fs(:short) %>
|
|
50
|
-
</td>
|
|
51
|
-
<td class="l-ui-table__cell l-ui-table__cell--action">
|
|
52
|
-
<%= link_to "Edit", layered_assistant.edit_conversation_path(conversation), class: "l-ui-table__action" %>
|
|
53
|
-
</td>
|
|
54
|
-
</tr>
|
|
55
|
-
<% end %>
|
|
56
|
-
</tbody>
|
|
57
|
-
</table>
|
|
58
|
-
|
|
59
|
-
<div class="l-ui-mt-4">
|
|
60
|
-
<%= l_ui_pagy(@pagy) %>
|
|
61
|
-
</div>
|
|
62
|
-
</div>
|