layered-assistant-rails 0.8.0 → 0.9.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.
- checksums.yaml +4 -4
- data/README.md +7 -6
- data/app/controllers/layered/assistant/conversations_controller.rb +34 -54
- data/app/controllers/layered/assistant/messages_controller.rb +8 -14
- data/app/layered_resources/layered/assistant/conversation_resource.rb +87 -0
- data/app/layered_resources/layered/assistant/message_resource.rb +44 -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 +8 -4
- data/data/models.json +34 -23
- data/lib/layered/assistant/version.rb +1 -1
- metadata +8 -11
- 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
- data/app/views/layered/assistant/messages/index.html.erb +0 -63
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c87ef1f390a6a4c102682cb4befebc1950f8566552f5213075a9d9f2ac4c7b49
|
|
4
|
+
data.tar.gz: 0d289b5e1f05d1f696ae8e37053ec75b51b3bf3c280091ceb33d836c0f06678b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 68cb5c184761c2ef7de4bf43cc6b84a32dfbe2b8cfd434cb3562c72265e7d9640bd3ad41f965a1c4cc04fec13c02b314cacf5780e1d779f2d08fec87aefa260a
|
|
7
|
+
data.tar.gz: 8f9cc996f9de499fa6d7c9afaf8f607eed0ea9c06c0f78f3e2ff8aeac67580a8a897002c0b5c350c668d76920311c9c2cbae10c2d11b452b99cd89a3c9c70533
|
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
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
module Layered
|
|
2
2
|
module Assistant
|
|
3
|
-
|
|
3
|
+
# Messages are a layered resource for listing and deleting them; only
|
|
4
|
+
# `create` - the composer posting a message - is this engine's own.
|
|
5
|
+
class MessagesController < ResourcesController
|
|
4
6
|
include MessageCreation
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
@page_title = "Messages"
|
|
11
|
-
@pagy, @messages = pagy(@conversation.messages.includes(:model).by_created_at)
|
|
12
|
-
end
|
|
8
|
+
# Messages have no fields, which the gem reads as having no form, but
|
|
9
|
+
# `create` is the composer's rather than the gem's.
|
|
10
|
+
skip_before_action :require_layered_fields, only: [ :create ]
|
|
11
|
+
before_action :set_conversation, only: [ :create, :destroy ]
|
|
13
12
|
|
|
14
13
|
def create
|
|
15
14
|
model_id = scoped_model_id(message_params[:model_id])
|
|
@@ -37,9 +36,8 @@ module Layered
|
|
|
37
36
|
end
|
|
38
37
|
|
|
39
38
|
def destroy
|
|
40
|
-
|
|
39
|
+
super
|
|
41
40
|
@conversation.update_token_totals!
|
|
42
|
-
redirect_to layered_assistant.conversation_messages_path(@conversation), notice: "Message was successfully deleted."
|
|
43
41
|
end
|
|
44
42
|
|
|
45
43
|
private
|
|
@@ -48,10 +46,6 @@ module Layered
|
|
|
48
46
|
@conversation = scoped(Conversation).find_by!(uid: params[:conversation_id])
|
|
49
47
|
end
|
|
50
48
|
|
|
51
|
-
def set_message
|
|
52
|
-
@message = @conversation.messages.find(params[:id])
|
|
53
|
-
end
|
|
54
|
-
|
|
55
49
|
def message_params
|
|
56
50
|
params.require(:message).permit(:content, :model_id)
|
|
57
51
|
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
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module Layered
|
|
2
|
+
module Assistant
|
|
3
|
+
# Messages are listed and deleted here; they are written through the
|
|
4
|
+
# composer, so there are no fields and no form.
|
|
5
|
+
class MessageResource < Layered::Resource::Base
|
|
6
|
+
model Message
|
|
7
|
+
|
|
8
|
+
columns [
|
|
9
|
+
{ attribute: :role, primary: true },
|
|
10
|
+
{ attribute: :content, sortable: false,
|
|
11
|
+
render: ->(record, view) { view.truncate(record.content, length: 100) } },
|
|
12
|
+
{ attribute: :model_id, label: "Model", sortable: false,
|
|
13
|
+
render: ->(record) { record.model&.name } },
|
|
14
|
+
{ attribute: :tokens, label: "Tokens", sortable: false,
|
|
15
|
+
render: ->(record, view) {
|
|
16
|
+
total = record.input_tokens.to_i + record.output_tokens.to_i
|
|
17
|
+
"#{'~' if record.tokens_estimated?}#{view.number_with_delimiter(total)}" if total > 0
|
|
18
|
+
} },
|
|
19
|
+
{ attribute: :tokens_per_second, label: "Tok/s", sortable: false,
|
|
20
|
+
render: ->(record) {
|
|
21
|
+
if record.output_tokens.to_i > 0 && record.response_ms.to_i >= MessagesHelper::MIN_RESPONSE_MS_FOR_TPS
|
|
22
|
+
(record.output_tokens * 1000.0 / record.response_ms).round(1)
|
|
23
|
+
end
|
|
24
|
+
} },
|
|
25
|
+
{ attribute: :ttft_ms, label: "TTFT",
|
|
26
|
+
render: ->(record) { "#{record.ttft_ms}ms" if record.ttft_ms } },
|
|
27
|
+
{ attribute: :created_at, label: "Created",
|
|
28
|
+
render: ->(record) { record.created_at.to_fs(:short) } }
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
search_fields [ :content ]
|
|
32
|
+
|
|
33
|
+
default_sort attribute: :created_at, direction: :asc
|
|
34
|
+
|
|
35
|
+
# The conversation is resolved through the engine's own scoping, so an
|
|
36
|
+
# out-of-scope one 404s before any message is reached.
|
|
37
|
+
def self.scope(controller)
|
|
38
|
+
controller.send(:scoped, Conversation)
|
|
39
|
+
.find_by!(uid: controller.params[:conversation_id])
|
|
40
|
+
.messages.includes(:model)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
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,15 +4,19 @@ 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
|
-
|
|
15
|
-
|
|
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
|
|
19
|
+
layered_resources :messages, namespace: "Layered::Assistant", controller: "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
|
|
18
22
|
# cannot know which namespace it landed in. One route serves them all.
|
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.1
|
|
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,8 @@ 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
|
|
353
|
+
- app/layered_resources/layered/assistant/message_resource.rb
|
|
352
354
|
- app/layered_resources/layered/assistant/model_resource.rb
|
|
353
355
|
- app/layered_resources/layered/assistant/persona_resource.rb
|
|
354
356
|
- app/layered_resources/layered/assistant/provider_resource.rb
|
|
@@ -380,17 +382,12 @@ files:
|
|
|
380
382
|
- app/services/layered/assistant/tool_registry.rb
|
|
381
383
|
- app/services/layered/assistant/tool_runner_service.rb
|
|
382
384
|
- 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
385
|
- app/views/layered/assistant/conversations/show.html.erb
|
|
388
386
|
- app/views/layered/assistant/messages/_composer.html.erb
|
|
389
387
|
- app/views/layered/assistant/messages/_composer_fields.html.erb
|
|
390
388
|
- app/views/layered/assistant/messages/_message.html.erb
|
|
391
389
|
- app/views/layered/assistant/messages/_tool_message.html.erb
|
|
392
390
|
- app/views/layered/assistant/messages/create.turbo_stream.erb
|
|
393
|
-
- app/views/layered/assistant/messages/index.html.erb
|
|
394
391
|
- app/views/layered/assistant/panel/conversations/_header.html.erb
|
|
395
392
|
- app/views/layered/assistant/panel/conversations/index.html.erb
|
|
396
393
|
- app/views/layered/assistant/panel/conversations/new.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>
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
<%= l_ui_title_bar(
|
|
2
|
-
title: "Messages",
|
|
3
|
-
breadcrumbs: [["Conversations", layered_assistant.conversations_path]]
|
|
4
|
-
) %>
|
|
5
|
-
|
|
6
|
-
<div class="l-ui-table-container l-ui-mt-4">
|
|
7
|
-
<table class="l-ui-table">
|
|
8
|
-
<caption class="l-ui-sr-only">Messages</caption>
|
|
9
|
-
<thead class="l-ui-table__header">
|
|
10
|
-
<tr>
|
|
11
|
-
<th scope="col" class="l-ui-table__header-cell">Role</th>
|
|
12
|
-
<th scope="col" class="l-ui-table__header-cell">Content</th>
|
|
13
|
-
<th scope="col" class="l-ui-table__header-cell">Model</th>
|
|
14
|
-
<th scope="col" class="l-ui-table__header-cell">Tokens</th>
|
|
15
|
-
<th scope="col" class="l-ui-table__header-cell">Tok/s</th>
|
|
16
|
-
<th scope="col" class="l-ui-table__header-cell">TTFT</th>
|
|
17
|
-
<th scope="col" class="l-ui-table__header-cell">Created</th>
|
|
18
|
-
<th scope="col" class="l-ui-table__header-cell l-ui-table__header-cell--action">Actions</th>
|
|
19
|
-
</tr>
|
|
20
|
-
</thead>
|
|
21
|
-
|
|
22
|
-
<tbody class="l-ui-table__body">
|
|
23
|
-
<% @messages.each do |message| %>
|
|
24
|
-
<tr>
|
|
25
|
-
<th scope="row" class="l-ui-table__cell l-ui-table__cell--primary">
|
|
26
|
-
<%= message.role %>
|
|
27
|
-
</th>
|
|
28
|
-
<td class="l-ui-table__cell">
|
|
29
|
-
<%= truncate(message.content, length: 100) %>
|
|
30
|
-
</td>
|
|
31
|
-
<td class="l-ui-table__cell">
|
|
32
|
-
<%= message.model&.name %>
|
|
33
|
-
</td>
|
|
34
|
-
<td class="l-ui-table__cell">
|
|
35
|
-
<% total = message.input_tokens.to_i + message.output_tokens.to_i %>
|
|
36
|
-
<% if total > 0 %>
|
|
37
|
-
<% prefix = message.tokens_estimated? ? "~" : "" %>
|
|
38
|
-
<%= "#{prefix}#{number_with_delimiter(total)}" %>
|
|
39
|
-
<% end %>
|
|
40
|
-
</td>
|
|
41
|
-
<td class="l-ui-table__cell">
|
|
42
|
-
<% if message.output_tokens.to_i > 0 && message.response_ms.to_i >= Layered::Assistant::MessagesHelper::MIN_RESPONSE_MS_FOR_TPS %>
|
|
43
|
-
<%= (message.output_tokens * 1000.0 / message.response_ms).round(1) %>
|
|
44
|
-
<% end %>
|
|
45
|
-
</td>
|
|
46
|
-
<td class="l-ui-table__cell">
|
|
47
|
-
<%= "#{message.ttft_ms}ms" if message.ttft_ms %>
|
|
48
|
-
</td>
|
|
49
|
-
<td class="l-ui-table__cell">
|
|
50
|
-
<%= message.created_at.to_fs(:short) %>
|
|
51
|
-
</td>
|
|
52
|
-
<td class="l-ui-table__cell l-ui-table__cell--action">
|
|
53
|
-
<%= link_to "Delete", layered_assistant.conversation_message_path(@conversation, message), class: "l-ui-table__action l-ui-table__action--danger", data: { turbo_method: :delete, turbo_confirm: "Are you sure?" } %>
|
|
54
|
-
</td>
|
|
55
|
-
</tr>
|
|
56
|
-
<% end %>
|
|
57
|
-
</tbody>
|
|
58
|
-
</table>
|
|
59
|
-
|
|
60
|
-
<div class="l-ui-mt-4">
|
|
61
|
-
<%= l_ui_pagy(@pagy) %>
|
|
62
|
-
</div>
|
|
63
|
-
</div>
|