llm_meta_client 0.6.1 → 1.0.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/CHANGELOG.md +20 -0
- data/app/assets/stylesheets/llm_meta_client/generation_settings.css +26 -47
- data/lib/generators/llm_meta_client/scaffold/scaffold_generator.rb +1 -0
- data/lib/generators/llm_meta_client/scaffold/templates/app/controllers/chats_controller.rb +56 -23
- data/lib/generators/llm_meta_client/scaffold/templates/app/controllers/prompts_controller.rb +5 -2
- data/lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/generation_settings_controller.js +1 -25
- data/lib/generators/llm_meta_client/scaffold/templates/app/models/chat.rb +30 -35
- data/lib/generators/llm_meta_client/scaffold/templates/app/views/chats/create.turbo_stream.erb +27 -19
- data/lib/generators/llm_meta_client/scaffold/templates/app/views/chats/edit.html.erb +10 -10
- data/lib/generators/llm_meta_client/scaffold/templates/app/views/chats/new.html.erb +1 -1
- data/lib/generators/llm_meta_client/scaffold/templates/app/views/chats/update.turbo_stream.erb +27 -20
- data/lib/generators/llm_meta_client/scaffold/templates/app/views/shared/_generation_settings_field.html.erb +10 -69
- data/lib/generators/llm_meta_client/scaffold/templates/db/migrate/create_chats.rb +0 -2
- data/lib/generators/llm_meta_client/scaffold/templates/db/migrate/migrate_llm_uuid_to_prompt_executions.rb +7 -0
- data/lib/llm_meta_client/server_resource.rb +1 -1
- data/lib/llm_meta_client/version.rb +1 -1
- metadata +6 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 95bb1f421088cc0c287b4f79e961455a1a5275b860b372fb23523ddc8781d37a
|
|
4
|
+
data.tar.gz: d4f92a150de0d9996d3ea6c54e69fe24989763f4bd359ce46a6b7ff69d36ecb7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 480919bad702190fec4166d8a2659121ff62b1880123e6db168d9a1c8a8b7f80f23ca175e2e6f4a31b96e6bde97fd0723d95caa71439845a5017c351c18e373e
|
|
7
|
+
data.tar.gz: c2600201eccae124c9929eabcd8b7d755b7dbac4092cd5441c55d1caf79c839af8f0b41b49d7bf1c2e6a324e33c6bd1957d29a1ac3e837a7ab6924d727e8ae91
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.0.0] - 2026-03-25
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- Replace generation settings UI from individual sliders to JSON textarea input
|
|
13
|
+
- Improve prompt execution branching logic to use `execution_id` instead of message UUID
|
|
14
|
+
- Use `find_by!` for proper 404 handling in controllers
|
|
15
|
+
- Use URL-based chat lookup in `chats#update` instead of session-based lookup
|
|
16
|
+
- Keep existing chat when switching model or LLM (update instead of creating new chat)
|
|
17
|
+
- Upgrade `prompt_navigator` dependency to `~> 1.0`
|
|
18
|
+
- Upgrade `chat_manager` dependency to `~> 1.0`
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- Fix Turbo Stream history sidebar element ID mismatch (`history-content` → `history-sidebar`)
|
|
23
|
+
- Fix `next_ann` for proper history card rendering
|
|
24
|
+
- Wrap inline JavaScript in IIFE to prevent variable conflicts across Turbo Stream updates
|
|
25
|
+
- Fix scroll event listener duplication across Turbo navigations
|
|
26
|
+
- Validate generation settings JSON input before sending to LLM
|
|
27
|
+
|
|
8
28
|
## [0.6.1] - 2026-03-19
|
|
9
29
|
|
|
10
30
|
### Fixed
|
|
@@ -31,60 +31,39 @@
|
|
|
31
31
|
padding: 12px 16px;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
.generation-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
.generation-settings-label {
|
|
35
|
+
display: block;
|
|
36
|
+
font-size: 13px;
|
|
37
|
+
font-weight: 600;
|
|
38
|
+
color: #374151;
|
|
39
|
+
margin-bottom: 6px;
|
|
40
|
+
}
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
42
|
+
.generation-settings-json-input {
|
|
43
|
+
width: 100%;
|
|
44
|
+
padding: 8px 10px;
|
|
45
|
+
border: 1px solid #d1d5db;
|
|
46
|
+
border-radius: 6px;
|
|
47
|
+
font-size: 13px;
|
|
48
|
+
font-family: "SFMono-Regular", "Consolas", "Liberation Mono", "Menlo", monospace;
|
|
49
|
+
background-color: white;
|
|
50
|
+
resize: vertical;
|
|
51
|
+
transition: border-color 0.2s;
|
|
52
|
+
box-sizing: border-box;
|
|
50
53
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
54
|
+
&:focus {
|
|
55
|
+
outline: none;
|
|
56
|
+
border-color: #3b82f6;
|
|
57
|
+
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
|
55
58
|
}
|
|
56
59
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
padding: 6px 10px;
|
|
60
|
-
border: 1px solid #d1d5db;
|
|
61
|
-
border-radius: 6px;
|
|
62
|
-
font-size: 13px;
|
|
63
|
-
background-color: white;
|
|
64
|
-
transition: border-color 0.2s;
|
|
65
|
-
|
|
66
|
-
&:focus {
|
|
67
|
-
outline: none;
|
|
68
|
-
border-color: #3b82f6;
|
|
69
|
-
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
&::placeholder {
|
|
73
|
-
color: #9ca3af;
|
|
74
|
-
}
|
|
60
|
+
&::placeholder {
|
|
61
|
+
color: #9ca3af;
|
|
75
62
|
}
|
|
76
63
|
}
|
|
77
64
|
|
|
78
|
-
.
|
|
79
|
-
font-weight: 700;
|
|
80
|
-
color: #3b82f6;
|
|
81
|
-
font-size: 13px;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
.setting-range-labels {
|
|
85
|
-
display: flex;
|
|
86
|
-
justify-content: space-between;
|
|
65
|
+
.generation-settings-hint {
|
|
87
66
|
font-size: 11px;
|
|
88
67
|
color: #9ca3af;
|
|
89
|
-
margin-top:
|
|
68
|
+
margin-top: 4px;
|
|
90
69
|
}
|
|
@@ -56,6 +56,7 @@ module LlmMetaClient
|
|
|
56
56
|
def add_migrations
|
|
57
57
|
migration_template "db/migrate/create_chats.rb", "db/migrate/create_chats.rb"
|
|
58
58
|
migration_template "db/migrate/create_messages.rb", "db/migrate/create_messages.rb"
|
|
59
|
+
migration_template "db/migrate/migrate_llm_uuid_to_prompt_executions.rb", "db/migrate/migrate_llm_uuid_to_prompt_executions.rb"
|
|
59
60
|
end
|
|
60
61
|
|
|
61
62
|
def configure_routes
|
|
@@ -10,7 +10,8 @@ class ChatsController < ApplicationController
|
|
|
10
10
|
# Initialize chat context
|
|
11
11
|
initialize_chat current_user&.chats
|
|
12
12
|
|
|
13
|
-
@chat = current_user&.chats.includes(:messages).find_by(uuid: params[:id])
|
|
13
|
+
@chat = current_user&.chats.includes(:messages).find_by!(uuid: params[:id])
|
|
14
|
+
session[:chat_id] = @chat.id
|
|
14
15
|
@messages = @chat.ordered_messages
|
|
15
16
|
|
|
16
17
|
# Initialize history
|
|
@@ -60,9 +61,7 @@ class ChatsController < ApplicationController
|
|
|
60
61
|
# Find or create chat
|
|
61
62
|
@chat = Chat.find_or_switch_for_session(
|
|
62
63
|
session,
|
|
63
|
-
current_user
|
|
64
|
-
llm_uuid: params[:api_key_uuid],
|
|
65
|
-
model: params[:model]
|
|
64
|
+
current_user
|
|
66
65
|
)
|
|
67
66
|
add_chat @chat
|
|
68
67
|
@messages = @chat&.ordered_messages || []
|
|
@@ -71,8 +70,21 @@ class ChatsController < ApplicationController
|
|
|
71
70
|
initialize_history @chat&.ordered_by_descending_prompt_executions
|
|
72
71
|
|
|
73
72
|
if params[:message].present?
|
|
73
|
+
# Validate generation settings before proceeding
|
|
74
|
+
begin
|
|
75
|
+
generation_settings = generation_settings_param
|
|
76
|
+
rescue InvalidGenerationSettingsError => e
|
|
77
|
+
@error_message = e.message
|
|
78
|
+
respond_to do |format|
|
|
79
|
+
format.turbo_stream
|
|
80
|
+
format.html { redirect_to new_chat_path, alert: e.message }
|
|
81
|
+
end
|
|
82
|
+
return
|
|
83
|
+
end
|
|
84
|
+
|
|
74
85
|
# Add user message (will be rendered via turbo stream)
|
|
75
86
|
@prompt_execution, @user_message = @chat.add_user_message(params[:message],
|
|
87
|
+
params[:api_key_uuid],
|
|
76
88
|
params[:model],
|
|
77
89
|
params[:branch_from_uuid])
|
|
78
90
|
# Push to history for rendering
|
|
@@ -82,7 +94,7 @@ class ChatsController < ApplicationController
|
|
|
82
94
|
|
|
83
95
|
# Send to LLM and get assistant response
|
|
84
96
|
begin
|
|
85
|
-
@assistant_message = @chat.add_assistant_response(@prompt_execution, jwt_token, tool_ids: tool_ids_param, generation_settings:
|
|
97
|
+
@assistant_message = @chat.add_assistant_response(@prompt_execution, jwt_token, tool_ids: tool_ids_param, generation_settings: generation_settings)
|
|
86
98
|
# Generate chat title from the user's prompt (only if title is not yet set)
|
|
87
99
|
@chat.generate_title(params[:message], jwt_token)
|
|
88
100
|
rescue StandardError => e
|
|
@@ -131,9 +143,7 @@ class ChatsController < ApplicationController
|
|
|
131
143
|
|
|
132
144
|
@chat = Chat.find_or_switch_for_session(
|
|
133
145
|
session,
|
|
134
|
-
current_user
|
|
135
|
-
llm_uuid: params[:api_key_uuid],
|
|
136
|
-
model: params[:model]
|
|
146
|
+
current_user
|
|
137
147
|
)
|
|
138
148
|
@messages = @chat&.ordered_messages || []
|
|
139
149
|
# initialize history for the chat
|
|
@@ -149,20 +159,29 @@ class ChatsController < ApplicationController
|
|
|
149
159
|
def update
|
|
150
160
|
jwt_token = current_user.id_token if user_signed_in?
|
|
151
161
|
|
|
152
|
-
#
|
|
153
|
-
@chat =
|
|
154
|
-
|
|
155
|
-
current_user,
|
|
156
|
-
llm_uuid: params[:api_key_uuid],
|
|
157
|
-
model: params[:model]
|
|
158
|
-
)
|
|
162
|
+
# Use the chat identified by the URL, not the session
|
|
163
|
+
@chat = current_user.chats.find(params[:id])
|
|
164
|
+
session[:chat_id] = @chat.id
|
|
159
165
|
@messages = @chat&.ordered_messages || []
|
|
160
166
|
# initialize history for the chat
|
|
161
167
|
initialize_history @chat&.ordered_by_descending_prompt_executions
|
|
162
168
|
|
|
163
169
|
if params[:message].present?
|
|
170
|
+
# Validate generation settings before proceeding
|
|
171
|
+
begin
|
|
172
|
+
generation_settings = generation_settings_param
|
|
173
|
+
rescue InvalidGenerationSettingsError => e
|
|
174
|
+
@error_message = e.message
|
|
175
|
+
respond_to do |format|
|
|
176
|
+
format.turbo_stream
|
|
177
|
+
format.html { redirect_to chat_path(@chat), alert: e.message }
|
|
178
|
+
end
|
|
179
|
+
return
|
|
180
|
+
end
|
|
181
|
+
|
|
164
182
|
# Add user message (will be rendered via turbo stream)
|
|
165
183
|
@prompt_execution, @user_message = @chat.add_user_message(params[:message],
|
|
184
|
+
params[:api_key_uuid],
|
|
166
185
|
params[:model],
|
|
167
186
|
params[:branch_from_uuid])
|
|
168
187
|
# Push to history for rendering
|
|
@@ -172,7 +191,7 @@ class ChatsController < ApplicationController
|
|
|
172
191
|
|
|
173
192
|
# Send to LLM and get assistant response
|
|
174
193
|
begin
|
|
175
|
-
@assistant_message = @chat.add_assistant_response(@prompt_execution, jwt_token, tool_ids: tool_ids_param, generation_settings:
|
|
194
|
+
@assistant_message = @chat.add_assistant_response(@prompt_execution, jwt_token, tool_ids: tool_ids_param, generation_settings: generation_settings)
|
|
176
195
|
rescue StandardError => e
|
|
177
196
|
Rails.logger.error "Error in chat response: #{e.class} - #{e.message}\n#{e.backtrace&.join("\n")}"
|
|
178
197
|
@error_message = "An error occurred while getting the response. Please try again."
|
|
@@ -192,13 +211,27 @@ class ChatsController < ApplicationController
|
|
|
192
211
|
params[:tool_ids].presence || []
|
|
193
212
|
end
|
|
194
213
|
|
|
214
|
+
ALLOWED_GENERATION_KEYS = %w[temperature top_k top_p max_tokens repeat_penalty].freeze
|
|
215
|
+
|
|
216
|
+
class InvalidGenerationSettingsError < StandardError; end
|
|
217
|
+
|
|
195
218
|
def generation_settings_param
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
settings
|
|
200
|
-
|
|
201
|
-
settings
|
|
202
|
-
|
|
219
|
+
return {} if params[:generation_settings_json].blank?
|
|
220
|
+
|
|
221
|
+
parsed = JSON.parse(params[:generation_settings_json])
|
|
222
|
+
raise InvalidGenerationSettingsError, "Generation settings must be a JSON object" unless parsed.is_a?(Hash)
|
|
223
|
+
|
|
224
|
+
settings = parsed.slice(*ALLOWED_GENERATION_KEYS)
|
|
225
|
+
invalid_keys = parsed.keys - ALLOWED_GENERATION_KEYS
|
|
226
|
+
raise InvalidGenerationSettingsError, "Unknown keys: #{invalid_keys.join(', ')}" if invalid_keys.any?
|
|
227
|
+
|
|
228
|
+
non_numeric = settings.reject { |_k, v| v.is_a?(Numeric) }
|
|
229
|
+
if non_numeric.any?
|
|
230
|
+
raise InvalidGenerationSettingsError, "Values must be numeric: #{non_numeric.keys.join(', ')}"
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
settings.symbolize_keys
|
|
234
|
+
rescue JSON::ParserError => e
|
|
235
|
+
raise InvalidGenerationSettingsError, "Invalid JSON: #{e.message}"
|
|
203
236
|
end
|
|
204
237
|
end
|
data/lib/generators/llm_meta_client/scaffold/templates/app/controllers/prompts_controller.rb
CHANGED
|
@@ -4,8 +4,8 @@ class PromptsController < ApplicationController
|
|
|
4
4
|
skip_before_action :authenticate_user!, raise: false
|
|
5
5
|
|
|
6
6
|
def show
|
|
7
|
-
@prompt_execution = PromptNavigator::PromptExecution.
|
|
8
|
-
@message = @prompt_execution.
|
|
7
|
+
@prompt_execution = PromptNavigator::PromptExecution.find_by!(execution_id: params[:id])
|
|
8
|
+
@message = Message.where(prompt_navigator_prompt_execution: @prompt_execution).order(:created_at).first
|
|
9
9
|
@chat = @message.chat
|
|
10
10
|
@messages = @chat.ordered_messages
|
|
11
11
|
|
|
@@ -25,6 +25,9 @@ class PromptsController < ApplicationController
|
|
|
25
25
|
# Set active UUID for history sidebar highlighting
|
|
26
26
|
set_active_message_uuid(@prompt_execution.execution_id)
|
|
27
27
|
|
|
28
|
+
# Set branch_from_uuid so the form knows which message to branch from
|
|
29
|
+
@branch_from_uuid = @prompt_execution.execution_id
|
|
30
|
+
|
|
28
31
|
render "chats/edit"
|
|
29
32
|
rescue StandardError => e
|
|
30
33
|
Rails.logger.error "Error in PromptsController#show_by_uuid: #{e.class} - #{e.message}\n#{e.backtrace&.join("\n")}"
|
|
@@ -6,15 +6,7 @@ export default class extends Controller {
|
|
|
6
6
|
"toggleButton",
|
|
7
7
|
"toggleIcon",
|
|
8
8
|
"panel",
|
|
9
|
-
"
|
|
10
|
-
"temperatureValue",
|
|
11
|
-
"topKRange",
|
|
12
|
-
"topKValue",
|
|
13
|
-
"topPRange",
|
|
14
|
-
"topPValue",
|
|
15
|
-
"maxTokensInput",
|
|
16
|
-
"repeatPenaltyRange",
|
|
17
|
-
"repeatPenaltyValue",
|
|
9
|
+
"jsonInput",
|
|
18
10
|
]
|
|
19
11
|
|
|
20
12
|
connect() {
|
|
@@ -32,20 +24,4 @@ export default class extends Controller {
|
|
|
32
24
|
this.toggleIconTarget.classList.toggle("bi-chevron-up", this.expanded)
|
|
33
25
|
}
|
|
34
26
|
}
|
|
35
|
-
|
|
36
|
-
updateTemperature() {
|
|
37
|
-
this.temperatureValueTarget.textContent = this.temperatureRangeTarget.value
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
updateTopK() {
|
|
41
|
-
this.topKValueTarget.textContent = this.topKRangeTarget.value
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
updateTopP() {
|
|
45
|
-
this.topPValueTarget.textContent = this.topPRangeTarget.value
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
updateRepeatPenalty() {
|
|
49
|
-
this.repeatPenaltyValueTarget.textContent = this.repeatPenaltyRangeTarget.value
|
|
50
|
-
}
|
|
51
27
|
}
|
|
@@ -6,25 +6,14 @@ class Chat < ApplicationRecord
|
|
|
6
6
|
|
|
7
7
|
before_create :set_uuid
|
|
8
8
|
|
|
9
|
-
validates :llm_uuid, presence: true
|
|
10
|
-
validates :model, presence: true
|
|
11
|
-
|
|
12
9
|
# Find existing chat from session or create new one
|
|
13
10
|
class << self
|
|
14
|
-
def find_or_switch_for_session(session, current_user
|
|
11
|
+
def find_or_switch_for_session(session, current_user)
|
|
15
12
|
chat = find_by_session_chat_id(session, current_user)
|
|
16
|
-
return chat if
|
|
17
|
-
|
|
18
|
-
# Create new chat if it doesn't exist or LLM/model has changed
|
|
19
|
-
if llm_uuid.present? && model.present? && (chat.nil? || (chat.present? && chat.needs_reset?(llm_uuid, model)))
|
|
20
|
-
chat = create!(
|
|
21
|
-
user: current_user,
|
|
22
|
-
llm_uuid: llm_uuid,
|
|
23
|
-
model: model
|
|
24
|
-
)
|
|
25
|
-
session[:chat_id] = chat.id
|
|
26
|
-
end
|
|
13
|
+
return chat if chat.present?
|
|
27
14
|
|
|
15
|
+
chat = create!(user: current_user)
|
|
16
|
+
session[:chat_id] = chat.id
|
|
28
17
|
chat
|
|
29
18
|
end
|
|
30
19
|
|
|
@@ -41,21 +30,19 @@ class Chat < ApplicationRecord
|
|
|
41
30
|
end
|
|
42
31
|
end
|
|
43
32
|
|
|
44
|
-
# Get the LLM type for this chat
|
|
45
|
-
def llm_type(jwt_token)
|
|
46
|
-
llm_options = LlmMetaClient::ServerResource.available_llm_options(jwt_token)
|
|
47
|
-
selected_llm = llm_options.find { |opt| opt[:uuid] == llm_uuid }
|
|
48
|
-
selected_llm&.dig(:llm_type) || "unknown"
|
|
49
|
-
end
|
|
50
|
-
|
|
51
33
|
# Add a user message to the chat
|
|
52
|
-
def add_user_message(message, model,
|
|
53
|
-
|
|
34
|
+
def add_user_message(message, llm_uuid, model, branch_from_execution_id = nil)
|
|
35
|
+
previous_id = if branch_from_execution_id.present?
|
|
36
|
+
PromptNavigator::PromptExecution.find_by(execution_id: branch_from_execution_id)&.id
|
|
37
|
+
else
|
|
38
|
+
messages.where(role: "user").order(:created_at).last&.prompt_navigator_prompt_execution_id
|
|
39
|
+
end
|
|
54
40
|
prompt_execution = PromptNavigator::PromptExecution.create!(
|
|
55
41
|
prompt: message,
|
|
42
|
+
llm_uuid: llm_uuid,
|
|
56
43
|
model: model,
|
|
57
44
|
configuration: "",
|
|
58
|
-
previous_id:
|
|
45
|
+
previous_id: previous_id
|
|
59
46
|
)
|
|
60
47
|
|
|
61
48
|
new_message = messages.create!(
|
|
@@ -68,9 +55,9 @@ class Chat < ApplicationRecord
|
|
|
68
55
|
|
|
69
56
|
# Add assistant response by sending to LLM
|
|
70
57
|
def add_assistant_response(prompt_execution, jwt_token, tool_ids: [], generation_settings: {})
|
|
71
|
-
response_content = send_to_llm(jwt_token, tool_ids: tool_ids, generation_settings: generation_settings)
|
|
58
|
+
response_content = send_to_llm(prompt_execution, jwt_token, tool_ids: tool_ids, generation_settings: generation_settings)
|
|
72
59
|
prompt_execution.update!(
|
|
73
|
-
llm_platform:
|
|
60
|
+
llm_platform: resolve_llm_type(prompt_execution.llm_uuid, jwt_token),
|
|
74
61
|
response: response_content
|
|
75
62
|
)
|
|
76
63
|
new_message = messages.create!(
|
|
@@ -98,19 +85,24 @@ class Chat < ApplicationRecord
|
|
|
98
85
|
.map(&:prompt_navigator_prompt_execution)
|
|
99
86
|
end
|
|
100
87
|
|
|
101
|
-
# Check if chat needs to be reset due to LLM or model change
|
|
102
|
-
def needs_reset?(new_llm_uuid, new_model)
|
|
103
|
-
llm_uuid != new_llm_uuid || model != new_model
|
|
104
|
-
end
|
|
105
|
-
|
|
106
88
|
private
|
|
107
89
|
|
|
90
|
+
# Resolve the LLM type (e.g. "openai", "google") from a given llm_uuid
|
|
91
|
+
def resolve_llm_type(llm_uuid, jwt_token)
|
|
92
|
+
llm_options = LlmMetaClient::ServerResource.available_llm_options(jwt_token)
|
|
93
|
+
selected_llm = llm_options.find { |opt| opt[:uuid] == llm_uuid }
|
|
94
|
+
selected_llm&.dig(:llm_type) || "unknown"
|
|
95
|
+
end
|
|
96
|
+
|
|
108
97
|
# Summarize the user's prompt into a short title via LLM (required by ChatManager::TitleGeneratable)
|
|
109
98
|
def summarize_for_title(prompt_text, jwt_token)
|
|
99
|
+
latest_pe = ordered_by_descending_prompt_executions.first
|
|
100
|
+
return nil unless latest_pe&.llm_uuid && latest_pe&.model
|
|
101
|
+
|
|
110
102
|
LlmMetaClient::ServerQuery.new.call(
|
|
111
103
|
jwt_token,
|
|
112
|
-
llm_uuid,
|
|
113
|
-
model,
|
|
104
|
+
latest_pe.llm_uuid,
|
|
105
|
+
latest_pe.model,
|
|
114
106
|
"No context available.",
|
|
115
107
|
{ role: "user", prompt: "Please summarize the following text into a short title (max 50 characters). Respond with only the title, nothing else: #{prompt_text}" }
|
|
116
108
|
)
|
|
@@ -122,7 +114,10 @@ class Chat < ApplicationRecord
|
|
|
122
114
|
end
|
|
123
115
|
|
|
124
116
|
# Send messages to LLM and get response
|
|
125
|
-
def send_to_llm(jwt_token, tool_ids: [], generation_settings: {})
|
|
117
|
+
def send_to_llm(prompt_execution, jwt_token, tool_ids: [], generation_settings: {})
|
|
118
|
+
llm_uuid = prompt_execution.llm_uuid
|
|
119
|
+
model = prompt_execution.model
|
|
120
|
+
|
|
126
121
|
# Get LLM options
|
|
127
122
|
llm_options = LlmMetaClient::ServerResource.available_llm_options(jwt_token)
|
|
128
123
|
|
data/lib/generators/llm_meta_client/scaffold/templates/app/views/chats/create.turbo_stream.erb
CHANGED
|
@@ -33,12 +33,12 @@
|
|
|
33
33
|
<%% # Update history sidebar - replace entire content to ensure update %>
|
|
34
34
|
<%% if @prompt_execution %>
|
|
35
35
|
<%%= turbo_stream.replace "history-sidebar" do %>
|
|
36
|
-
<div id="history-
|
|
36
|
+
<div id="history-sidebar">
|
|
37
37
|
<h2>History</h2>
|
|
38
38
|
<div class="history-stack" id="history-stack" data-controller="history">
|
|
39
39
|
<%%= render 'prompt_navigator/history_card', locals: {
|
|
40
40
|
ann: @prompt_execution,
|
|
41
|
-
next_ann:
|
|
41
|
+
next_ann: (@chat&.ordered_by_descending_prompt_executions || [])[1],
|
|
42
42
|
is_active: @prompt_execution.execution_id == @active_message_uuid,
|
|
43
43
|
card_path: ->(uuid) { prompt_path(uuid) }
|
|
44
44
|
} %>
|
|
@@ -60,25 +60,33 @@
|
|
|
60
60
|
<turbo-stream action="after" target="messages-list">
|
|
61
61
|
<template>
|
|
62
62
|
<script>
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
messageInput
|
|
67
|
-
|
|
68
|
-
|
|
63
|
+
(function() {
|
|
64
|
+
// Clear and refocus message input
|
|
65
|
+
const messageInput = document.getElementById('message-input');
|
|
66
|
+
if (messageInput) {
|
|
67
|
+
messageInput.value = '';
|
|
68
|
+
messageInput.focus();
|
|
69
|
+
}
|
|
69
70
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
71
|
+
// Update submit button state
|
|
72
|
+
const form = document.querySelector('[data-controller="chats-form"]');
|
|
73
|
+
if (form && messageInput) {
|
|
74
|
+
const event = new Event('input', { bubbles: true });
|
|
75
|
+
messageInput.dispatchEvent(event);
|
|
76
|
+
}
|
|
76
77
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
// Update branch_from_uuid to the latest prompt execution
|
|
79
|
+
const branchField = document.getElementById('branch_from_uuid');
|
|
80
|
+
if (branchField) {
|
|
81
|
+
branchField.value = '<%%= @prompt_execution&.execution_id %>';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Scroll to bottom
|
|
85
|
+
const chatMessages = document.getElementById('chat-messages');
|
|
86
|
+
if (chatMessages) {
|
|
87
|
+
chatMessages.scrollTop = chatMessages.scrollHeight;
|
|
88
|
+
}
|
|
89
|
+
})();
|
|
82
90
|
</script>
|
|
83
91
|
</template>
|
|
84
92
|
</turbo-stream>
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
id: "message-input",
|
|
31
31
|
data: { "chats-form-target": "prompt", action: "input->chats-form#updateSubmitButton" } %>
|
|
32
32
|
</div>
|
|
33
|
-
<%%= f.hidden_field :branch_from_uuid, value: params.dig(:chat, :branch_from_uuid) %>
|
|
33
|
+
<%%= f.hidden_field :branch_from_uuid, value: @branch_from_uuid || params.dig(:chat, :branch_from_uuid) %>
|
|
34
34
|
<div class="button-wrapper">
|
|
35
35
|
<%%= f.button type: "submit",
|
|
36
36
|
class: "send-button",
|
|
@@ -47,16 +47,18 @@
|
|
|
47
47
|
|
|
48
48
|
|
|
49
49
|
<script>
|
|
50
|
-
//
|
|
51
|
-
|
|
50
|
+
// Remove previous listener to prevent duplicates across Turbo navigations
|
|
51
|
+
if (window._scrollChatMessages) {
|
|
52
|
+
document.removeEventListener('turbo:load', window._scrollChatMessages);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
window._scrollChatMessages = function() {
|
|
52
56
|
const chatMessages = document.getElementById('chat-messages');
|
|
53
57
|
if (chatMessages) {
|
|
54
58
|
<%% if @target_message_id.present? %>
|
|
55
|
-
// Scroll to target message
|
|
56
59
|
const targetMessage = document.getElementById('message-<%%= @target_message_id %>');
|
|
57
60
|
if (targetMessage) {
|
|
58
61
|
targetMessage.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
59
|
-
// Highlight the target message
|
|
60
62
|
targetMessage.style.backgroundColor = '#fef3c7';
|
|
61
63
|
targetMessage.style.border = '2px solid #fbbf24';
|
|
62
64
|
setTimeout(() => {
|
|
@@ -69,10 +71,8 @@
|
|
|
69
71
|
chatMessages.scrollTop = chatMessages.scrollHeight;
|
|
70
72
|
<%% end %>
|
|
71
73
|
}
|
|
72
|
-
}
|
|
74
|
+
};
|
|
73
75
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
document.addEventListener('DOMContentLoaded', scrollChatMessages);
|
|
77
|
-
document.addEventListener('turbo:load', scrollChatMessages);
|
|
76
|
+
document.addEventListener('DOMContentLoaded', window._scrollChatMessages);
|
|
77
|
+
document.addEventListener('turbo:load', window._scrollChatMessages);
|
|
78
78
|
</script>
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
id: "message-input",
|
|
31
31
|
data: { "chats-form-target": "prompt", action: "input->chats-form#updateSubmitButton" } %>
|
|
32
32
|
</div>
|
|
33
|
-
<%%= f.hidden_field :branch_from_uuid, value: params.dig(:chat, :branch_from_uuid) %>
|
|
33
|
+
<%%= f.hidden_field :branch_from_uuid, value: @branch_from_uuid || params.dig(:chat, :branch_from_uuid) %>
|
|
34
34
|
<div class="button-wrapper">
|
|
35
35
|
<%%= f.button type: "submit",
|
|
36
36
|
class: "send-button",
|
data/lib/generators/llm_meta_client/scaffold/templates/app/views/chats/update.turbo_stream.erb
CHANGED
|
@@ -26,13 +26,12 @@
|
|
|
26
26
|
<%% # Update history sidebar - replace entire content to ensure update %>
|
|
27
27
|
<%% if @prompt_execution %>
|
|
28
28
|
<%%= turbo_stream.replace "history-sidebar" do %>
|
|
29
|
-
|
|
30
|
-
<div id="history-content">
|
|
29
|
+
<div id="history-sidebar">
|
|
31
30
|
<h2>History</h2>
|
|
32
31
|
<div class="history-stack" id="history-stack" data-controller="history">
|
|
33
32
|
<%%= render 'prompt_navigator/history_card', locals: {
|
|
34
33
|
ann: @prompt_execution,
|
|
35
|
-
next_ann:
|
|
34
|
+
next_ann: (@chat&.ordered_by_descending_prompt_executions || [])[1],
|
|
36
35
|
is_active: @prompt_execution.execution_id == @active_message_uuid,
|
|
37
36
|
card_path: ->(uuid) { prompt_path(uuid) }
|
|
38
37
|
} %>
|
|
@@ -54,25 +53,33 @@
|
|
|
54
53
|
<turbo-stream action="after" target="messages-list">
|
|
55
54
|
<template>
|
|
56
55
|
<script>
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
messageInput
|
|
61
|
-
|
|
62
|
-
|
|
56
|
+
(function() {
|
|
57
|
+
// Clear and refocus message input
|
|
58
|
+
const messageInput = document.getElementById('message-input');
|
|
59
|
+
if (messageInput) {
|
|
60
|
+
messageInput.value = '';
|
|
61
|
+
messageInput.focus();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Update submit button state
|
|
65
|
+
const form = document.querySelector('[data-controller="chats-form"]');
|
|
66
|
+
if (form && messageInput) {
|
|
67
|
+
const event = new Event('input', { bubbles: true });
|
|
68
|
+
messageInput.dispatchEvent(event);
|
|
69
|
+
}
|
|
63
70
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
71
|
+
// Update branch_from_uuid to the latest prompt execution
|
|
72
|
+
const branchField = document.getElementById('branch_from_uuid');
|
|
73
|
+
if (branchField) {
|
|
74
|
+
branchField.value = '<%%= @prompt_execution&.execution_id %>';
|
|
75
|
+
}
|
|
70
76
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
77
|
+
// Scroll to bottom
|
|
78
|
+
const chatMessages = document.getElementById('chat-messages');
|
|
79
|
+
if (chatMessages) {
|
|
80
|
+
chatMessages.scrollTop = chatMessages.scrollHeight;
|
|
81
|
+
}
|
|
82
|
+
})();
|
|
76
83
|
</script>
|
|
77
84
|
</template>
|
|
78
85
|
</turbo-stream>
|
|
@@ -13,75 +13,16 @@
|
|
|
13
13
|
</button>
|
|
14
14
|
</div>
|
|
15
15
|
<div class="generation-settings-panel" data-<%%= stimulus_controller %>-target="panel" style="display: none;">
|
|
16
|
-
<
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
<span>0 (deterministic)</span>
|
|
27
|
-
<span>2 (creative)</span>
|
|
28
|
-
</div>
|
|
29
|
-
</div>
|
|
30
|
-
|
|
31
|
-
<div class="generation-setting-item">
|
|
32
|
-
<label for="top_k">
|
|
33
|
-
Top-K
|
|
34
|
-
<span class="setting-value" data-<%%= stimulus_controller %>-target="topKValue">40</span>
|
|
35
|
-
</label>
|
|
36
|
-
<input type="range" name="top_k" id="top_k"
|
|
37
|
-
min="1" max="100" step="1" value="40"
|
|
38
|
-
data-<%%= stimulus_controller %>-target="topKRange"
|
|
39
|
-
data-action="input-><%%= stimulus_controller %>#updateTopK">
|
|
40
|
-
<div class="setting-range-labels">
|
|
41
|
-
<span>1 (focused)</span>
|
|
42
|
-
<span>100 (diverse)</span>
|
|
43
|
-
</div>
|
|
44
|
-
</div>
|
|
45
|
-
|
|
46
|
-
<div class="generation-setting-item">
|
|
47
|
-
<label for="top_p">
|
|
48
|
-
Top-P
|
|
49
|
-
<span class="setting-value" data-<%%= stimulus_controller %>-target="topPValue">0.9</span>
|
|
50
|
-
</label>
|
|
51
|
-
<input type="range" name="top_p" id="top_p"
|
|
52
|
-
min="0" max="1" step="0.05" value="0.9"
|
|
53
|
-
data-<%%= stimulus_controller %>-target="topPRange"
|
|
54
|
-
data-action="input-><%%= stimulus_controller %>#updateTopP">
|
|
55
|
-
<div class="setting-range-labels">
|
|
56
|
-
<span>0 (narrow)</span>
|
|
57
|
-
<span>1 (broad)</span>
|
|
58
|
-
</div>
|
|
59
|
-
</div>
|
|
60
|
-
|
|
61
|
-
<div class="generation-setting-item">
|
|
62
|
-
<label for="max_tokens">
|
|
63
|
-
Max Tokens
|
|
64
|
-
</label>
|
|
65
|
-
<input type="number" name="max_tokens" id="max_tokens"
|
|
66
|
-
min="1" max="128000" step="1" value=""
|
|
67
|
-
placeholder="Default (model-dependent)"
|
|
68
|
-
class="max-tokens-input"
|
|
69
|
-
data-<%%= stimulus_controller %>-target="maxTokensInput">
|
|
70
|
-
</div>
|
|
71
|
-
|
|
72
|
-
<div class="generation-setting-item">
|
|
73
|
-
<label for="repeat_penalty">
|
|
74
|
-
Repeat Penalty
|
|
75
|
-
<span class="setting-value" data-<%%= stimulus_controller %>-target="repeatPenaltyValue">1.1</span>
|
|
76
|
-
</label>
|
|
77
|
-
<input type="range" name="repeat_penalty" id="repeat_penalty"
|
|
78
|
-
min="1" max="2" step="0.05" value="1.1"
|
|
79
|
-
data-<%%= stimulus_controller %>-target="repeatPenaltyRange"
|
|
80
|
-
data-action="input-><%%= stimulus_controller %>#updateRepeatPenalty">
|
|
81
|
-
<div class="setting-range-labels">
|
|
82
|
-
<span>1.0 (no penalty)</span>
|
|
83
|
-
<span>2.0 (strong)</span>
|
|
84
|
-
</div>
|
|
16
|
+
<label for="generation_settings_json" class="generation-settings-label">
|
|
17
|
+
JSON format
|
|
18
|
+
</label>
|
|
19
|
+
<textarea name="generation_settings_json" id="generation_settings_json"
|
|
20
|
+
class="generation-settings-json-input"
|
|
21
|
+
rows="8"
|
|
22
|
+
placeholder='{"temperature": 0.7, "top_k": 40, "top_p": 0.9, "max_tokens": 4096, "repeat_penalty": 1.1}'
|
|
23
|
+
data-<%%= stimulus_controller %>-target="jsonInput"></textarea>
|
|
24
|
+
<div class="generation-settings-hint">
|
|
25
|
+
Available keys: temperature, top_k, top_p, max_tokens, repeat_penalty
|
|
85
26
|
</div>
|
|
86
27
|
</div>
|
|
87
28
|
</div>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
class MigrateLlmUuidToPromptExecutions < ActiveRecord::Migration[8.1]
|
|
2
|
+
def change
|
|
3
|
+
add_column :prompt_navigator_prompt_executions, :llm_uuid, :string unless column_exists?(:prompt_navigator_prompt_executions, :llm_uuid)
|
|
4
|
+
remove_column :chats, :llm_uuid, :string if column_exists?(:chats, :llm_uuid)
|
|
5
|
+
remove_column :chats, :model, :string if column_exists?(:chats, :model)
|
|
6
|
+
end
|
|
7
|
+
end
|
|
@@ -117,7 +117,7 @@ module LlmMetaClient
|
|
|
117
117
|
def ollama_options
|
|
118
118
|
ollama_list = llms.filter { it["family"] == "ollama" }
|
|
119
119
|
raise LlmMetaClient::Exceptions::OllamaUnavailableError if ollama_list.empty?
|
|
120
|
-
ollama_list
|
|
120
|
+
ollama_list.each { it["llm_type"] ||= "ollama" }
|
|
121
121
|
end
|
|
122
122
|
|
|
123
123
|
# Builds normalized option hashes from an array of prompts by slicing common keys
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: llm_meta_client
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 1.0.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- dhq_boiler
|
|
@@ -49,28 +49,28 @@ dependencies:
|
|
|
49
49
|
requirements:
|
|
50
50
|
- - "~>"
|
|
51
51
|
- !ruby/object:Gem::Version
|
|
52
|
-
version: '0
|
|
52
|
+
version: '1.0'
|
|
53
53
|
type: :runtime
|
|
54
54
|
prerelease: false
|
|
55
55
|
version_requirements: !ruby/object:Gem::Requirement
|
|
56
56
|
requirements:
|
|
57
57
|
- - "~>"
|
|
58
58
|
- !ruby/object:Gem::Version
|
|
59
|
-
version: '0
|
|
59
|
+
version: '1.0'
|
|
60
60
|
- !ruby/object:Gem::Dependency
|
|
61
61
|
name: chat_manager
|
|
62
62
|
requirement: !ruby/object:Gem::Requirement
|
|
63
63
|
requirements:
|
|
64
64
|
- - "~>"
|
|
65
65
|
- !ruby/object:Gem::Version
|
|
66
|
-
version: '0
|
|
66
|
+
version: '1.0'
|
|
67
67
|
type: :runtime
|
|
68
68
|
prerelease: false
|
|
69
69
|
version_requirements: !ruby/object:Gem::Requirement
|
|
70
70
|
requirements:
|
|
71
71
|
- - "~>"
|
|
72
72
|
- !ruby/object:Gem::Version
|
|
73
|
-
version: '0
|
|
73
|
+
version: '1.0'
|
|
74
74
|
description: llm_meta_client provides a Rails Engine with scaffold and authentication
|
|
75
75
|
generators for building LLM-powered chat applications. Supports OpenAI, Anthropic,
|
|
76
76
|
Google, and Ollama providers.
|
|
@@ -130,6 +130,7 @@ files:
|
|
|
130
130
|
- lib/generators/llm_meta_client/scaffold/templates/config/initializers/llm_service.rb
|
|
131
131
|
- lib/generators/llm_meta_client/scaffold/templates/db/migrate/create_chats.rb
|
|
132
132
|
- lib/generators/llm_meta_client/scaffold/templates/db/migrate/create_messages.rb
|
|
133
|
+
- lib/generators/llm_meta_client/scaffold/templates/db/migrate/migrate_llm_uuid_to_prompt_executions.rb
|
|
133
134
|
- lib/llm_meta_client.rb
|
|
134
135
|
- lib/llm_meta_client/chat_manageable.rb
|
|
135
136
|
- lib/llm_meta_client/engine.rb
|