llm_meta_client 0.3.0 → 0.5.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/CHANGELOG.md +47 -0
- data/app/assets/stylesheets/llm_meta_client/generation_settings.css +90 -0
- data/lib/generators/llm_meta_client/scaffold/scaffold_generator.rb +11 -0
- data/lib/generators/llm_meta_client/scaffold/templates/app/controllers/api/mcp_servers_controller.rb +18 -0
- data/lib/generators/llm_meta_client/scaffold/templates/app/controllers/chats_controller.rb +18 -2
- data/lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/generation_settings_controller.js +51 -0
- data/lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/tool_selector_controller.js +229 -0
- data/lib/generators/llm_meta_client/scaffold/templates/app/models/chat.rb +5 -5
- data/lib/generators/llm_meta_client/scaffold/templates/app/views/chats/edit.html.erb +4 -0
- data/lib/generators/llm_meta_client/scaffold/templates/app/views/chats/new.html.erb +4 -0
- data/lib/generators/llm_meta_client/scaffold/templates/app/views/shared/_generation_settings_field.html.erb +87 -0
- data/lib/generators/llm_meta_client/scaffold/templates/app/views/shared/_tool_selector_field.html.erb +23 -0
- data/lib/llm_meta_client/server_query.rb +8 -4
- data/lib/llm_meta_client/server_resource.rb +42 -4
- data/lib/llm_meta_client/version.rb +1 -1
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 60116965474267f22c077da1849611c0b6c4490a876a98deebfa243d2f9aef49
|
|
4
|
+
data.tar.gz: 748fdc1631edcb65c7792b1dc13a3ae8d0625422c05d4e4a47732f9c8167a7d9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c87684a604a914fe6097a1277948d009530f83a07ec7991aa5b6caa92ce8d2eb9568b4a55b648ba2b50f12e06bb9df95e31333bd211a56289cad4c3c3c8ddc33
|
|
7
|
+
data.tar.gz: 6ba2df271d37e40be53981e40c8c416239318872118019da88bdd35a845607d28e95d9f33e53328442b85f309448e30e6d7b8a874dcbd68996760be61ea97cb3
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,53 @@ 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
|
+
## [0.5.0] - 2026-03-17
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Generation settings support:
|
|
13
|
+
- `generation_settings` parameter in `ServerQuery` API layer for configuring LLM generation parameters
|
|
14
|
+
- `generation_settings` threading through `Chat` model
|
|
15
|
+
- `generation_settings` parameter extraction in `ChatsController`
|
|
16
|
+
- Generation settings UI components for configuring parameters in chat forms
|
|
17
|
+
|
|
18
|
+
## [0.4.0] - 2026-03-11
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- MCP (Model Context Protocol) tool selection support:
|
|
23
|
+
- `ServerResource.fetch_mcp_servers` and `ServerResource.fetch_mcp_tools` for retrieving MCP server/tool data from the LLM service
|
|
24
|
+
- `Api::McpServersController` with `index` and `tools` endpoints
|
|
25
|
+
- API routes for MCP servers (`/api/mcp_servers` and `/api/mcp_servers/:uuid/tools`)
|
|
26
|
+
- `tool_ids` parameter support through `ServerQuery`, `Chat` model, and `ChatsController`
|
|
27
|
+
- Tool selector UI component (Stimulus controller + view partial) for selecting MCP tools in chat forms
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- Extracted `authenticated_get` helper in `ServerResource` to reduce duplication in authenticated API calls
|
|
32
|
+
|
|
33
|
+
### Security
|
|
34
|
+
|
|
35
|
+
- Escape HTML attribute values (`server.uuid`, `tool.id`) in tool selector to prevent XSS
|
|
36
|
+
- Use `CSS.escape()` for `querySelector` and `encodeURIComponent()` for fetch URLs to prevent selector/URL injection
|
|
37
|
+
|
|
38
|
+
## [0.3.0] - 2026-03-05
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
|
|
42
|
+
- Update Ruby version requirement from 3.4.8 to 4.0.1
|
|
43
|
+
- Update gem dependencies to latest versions
|
|
44
|
+
|
|
45
|
+
## [0.2.0] - 2026-03-04
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
|
|
49
|
+
- Switch configuration to use Rails credentials instead of environment variables
|
|
50
|
+
|
|
51
|
+
### Documentation
|
|
52
|
+
|
|
53
|
+
- Update README with architectural details, setup instructions, and Rails credentials usage
|
|
54
|
+
|
|
8
55
|
## [0.1.0] - 2026-02-27
|
|
9
56
|
|
|
10
57
|
### Added
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/* Generation Settings */
|
|
2
|
+
.generation-settings-field {
|
|
3
|
+
margin-bottom: 10px;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.generation-settings-toggle-button {
|
|
7
|
+
display: flex;
|
|
8
|
+
align-items: center;
|
|
9
|
+
gap: 6px;
|
|
10
|
+
background: none;
|
|
11
|
+
border: 1px solid #d1d5db;
|
|
12
|
+
border-radius: 6px;
|
|
13
|
+
padding: 6px 12px;
|
|
14
|
+
font-size: 13px;
|
|
15
|
+
font-weight: 600;
|
|
16
|
+
color: #374151;
|
|
17
|
+
cursor: pointer;
|
|
18
|
+
transition: border-color 0.2s, background-color 0.2s;
|
|
19
|
+
|
|
20
|
+
&:hover {
|
|
21
|
+
background-color: #f9fafb;
|
|
22
|
+
border-color: #9ca3af;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.generation-settings-panel {
|
|
27
|
+
margin-top: 8px;
|
|
28
|
+
border: 1px solid #e5e7eb;
|
|
29
|
+
border-radius: 8px;
|
|
30
|
+
background-color: #f9fafb;
|
|
31
|
+
padding: 12px 16px;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.generation-setting-item {
|
|
35
|
+
margin-bottom: 14px;
|
|
36
|
+
|
|
37
|
+
&:last-child {
|
|
38
|
+
margin-bottom: 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
label {
|
|
42
|
+
display: flex;
|
|
43
|
+
align-items: center;
|
|
44
|
+
gap: 8px;
|
|
45
|
+
font-size: 13px;
|
|
46
|
+
font-weight: 600;
|
|
47
|
+
color: #374151;
|
|
48
|
+
margin-bottom: 4px;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
input[type="range"] {
|
|
52
|
+
width: 100%;
|
|
53
|
+
accent-color: #3b82f6;
|
|
54
|
+
cursor: pointer;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.max-tokens-input {
|
|
58
|
+
width: 100%;
|
|
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
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.setting-value {
|
|
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;
|
|
87
|
+
font-size: 11px;
|
|
88
|
+
color: #9ca3af;
|
|
89
|
+
margin-top: 2px;
|
|
90
|
+
}
|
|
@@ -20,6 +20,7 @@ module LlmMetaClient
|
|
|
20
20
|
def create_controllers
|
|
21
21
|
template "app/controllers/chats_controller.rb"
|
|
22
22
|
template "app/controllers/prompts_controller.rb"
|
|
23
|
+
template "app/controllers/api/mcp_servers_controller.rb"
|
|
23
24
|
end
|
|
24
25
|
|
|
25
26
|
def create_views
|
|
@@ -32,6 +33,8 @@ module LlmMetaClient
|
|
|
32
33
|
template "app/views/shared/_family_field.html.erb"
|
|
33
34
|
template "app/views/shared/_api_key_field.html.erb"
|
|
34
35
|
template "app/views/shared/_model_field.html.erb"
|
|
36
|
+
template "app/views/shared/_tool_selector_field.html.erb"
|
|
37
|
+
template "app/views/shared/_generation_settings_field.html.erb"
|
|
35
38
|
template "app/views/layouts/application.html.erb"
|
|
36
39
|
template "app/views/layouts/_header.html.erb"
|
|
37
40
|
template "app/views/layouts/_sidebar.html.erb"
|
|
@@ -41,6 +44,8 @@ module LlmMetaClient
|
|
|
41
44
|
template "app/javascript/controllers/llm_selector_controller.js"
|
|
42
45
|
template "app/javascript/controllers/chats_form_controller.js"
|
|
43
46
|
template "app/javascript/controllers/chat_title_edit_controller.js"
|
|
47
|
+
template "app/javascript/controllers/tool_selector_controller.js"
|
|
48
|
+
template "app/javascript/controllers/generation_settings_controller.js"
|
|
44
49
|
copy_file "app/javascript/popover.js"
|
|
45
50
|
end
|
|
46
51
|
|
|
@@ -69,6 +74,12 @@ module LlmMetaClient
|
|
|
69
74
|
end
|
|
70
75
|
end
|
|
71
76
|
resources :prompts, only: [ :show ]
|
|
77
|
+
|
|
78
|
+
namespace :api do
|
|
79
|
+
resources :mcp_servers, only: [ :index ], param: :uuid do
|
|
80
|
+
get :tools, on: :member
|
|
81
|
+
end
|
|
82
|
+
end
|
|
72
83
|
RUBY
|
|
73
84
|
end
|
|
74
85
|
|
data/lib/generators/llm_meta_client/scaffold/templates/app/controllers/api/mcp_servers_controller.rb
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Api::McpServersController < ApplicationController
|
|
4
|
+
skip_before_action :authenticate_user!, raise: false
|
|
5
|
+
before_action :authenticate_user!
|
|
6
|
+
|
|
7
|
+
def index
|
|
8
|
+
jwt_token = current_user.id_token
|
|
9
|
+
mcp_servers = LlmMetaClient::ServerResource.fetch_mcp_servers(jwt_token)
|
|
10
|
+
render json: { mcp_servers: mcp_servers }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def tools
|
|
14
|
+
jwt_token = current_user.id_token
|
|
15
|
+
tools = LlmMetaClient::ServerResource.fetch_mcp_tools(jwt_token, params[:uuid])
|
|
16
|
+
render json: { tools: tools }
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -82,7 +82,7 @@ class ChatsController < ApplicationController
|
|
|
82
82
|
|
|
83
83
|
# Send to LLM and get assistant response
|
|
84
84
|
begin
|
|
85
|
-
@assistant_message = @chat.add_assistant_response(@prompt_execution, jwt_token)
|
|
85
|
+
@assistant_message = @chat.add_assistant_response(@prompt_execution, jwt_token, tool_ids: tool_ids_param, generation_settings: generation_settings_param)
|
|
86
86
|
# Generate chat title from the user's prompt (only if title is not yet set)
|
|
87
87
|
@chat.generate_title(params[:message], jwt_token)
|
|
88
88
|
rescue StandardError => e
|
|
@@ -172,7 +172,7 @@ class ChatsController < ApplicationController
|
|
|
172
172
|
|
|
173
173
|
# Send to LLM and get assistant response
|
|
174
174
|
begin
|
|
175
|
-
@assistant_message = @chat.add_assistant_response(@prompt_execution, jwt_token)
|
|
175
|
+
@assistant_message = @chat.add_assistant_response(@prompt_execution, jwt_token, tool_ids: tool_ids_param, generation_settings: generation_settings_param)
|
|
176
176
|
rescue StandardError => e
|
|
177
177
|
Rails.logger.error "Error in chat response: #{e.class} - #{e.message}\n#{e.backtrace&.join("\n")}"
|
|
178
178
|
@error_message = "An error occurred while getting the response. Please try again."
|
|
@@ -185,4 +185,20 @@ class ChatsController < ApplicationController
|
|
|
185
185
|
format.html { redirect_to new_chat_path }
|
|
186
186
|
end
|
|
187
187
|
end
|
|
188
|
+
|
|
189
|
+
private
|
|
190
|
+
|
|
191
|
+
def tool_ids_param
|
|
192
|
+
params[:tool_ids].presence || []
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def generation_settings_param
|
|
196
|
+
settings = {}
|
|
197
|
+
settings[:temperature] = params[:temperature].to_f if params[:temperature].present?
|
|
198
|
+
settings[:top_k] = params[:top_k].to_i if params[:top_k].present?
|
|
199
|
+
settings[:top_p] = params[:top_p].to_f if params[:top_p].present?
|
|
200
|
+
settings[:max_tokens] = params[:max_tokens].to_i if params[:max_tokens].present?
|
|
201
|
+
settings[:repeat_penalty] = params[:repeat_penalty].to_f if params[:repeat_penalty].present?
|
|
202
|
+
settings
|
|
203
|
+
end
|
|
188
204
|
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// Connects to data-controller="generation-settings"
|
|
4
|
+
export default class extends Controller {
|
|
5
|
+
static targets = [
|
|
6
|
+
"toggleButton",
|
|
7
|
+
"toggleIcon",
|
|
8
|
+
"panel",
|
|
9
|
+
"temperatureRange",
|
|
10
|
+
"temperatureValue",
|
|
11
|
+
"topKRange",
|
|
12
|
+
"topKValue",
|
|
13
|
+
"topPRange",
|
|
14
|
+
"topPValue",
|
|
15
|
+
"maxTokensInput",
|
|
16
|
+
"repeatPenaltyRange",
|
|
17
|
+
"repeatPenaltyValue",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
connect() {
|
|
21
|
+
this.expanded = false
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
toggle() {
|
|
25
|
+
if (!this.hasPanelTarget) return
|
|
26
|
+
|
|
27
|
+
this.expanded = !this.expanded
|
|
28
|
+
this.panelTarget.style.display = this.expanded ? "block" : "none"
|
|
29
|
+
|
|
30
|
+
if (this.hasToggleIconTarget) {
|
|
31
|
+
this.toggleIconTarget.classList.toggle("bi-chevron-down", !this.expanded)
|
|
32
|
+
this.toggleIconTarget.classList.toggle("bi-chevron-up", this.expanded)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
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
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// Connects to data-controller="tool-selector"
|
|
4
|
+
export default class extends Controller {
|
|
5
|
+
static targets = [
|
|
6
|
+
"toggleButton",
|
|
7
|
+
"toggleIcon",
|
|
8
|
+
"countBadge",
|
|
9
|
+
"panel",
|
|
10
|
+
"loading",
|
|
11
|
+
"serverList",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
connect() {
|
|
15
|
+
this.mcpServers = []
|
|
16
|
+
this.expanded = false
|
|
17
|
+
this.selectedToolIds = new Set()
|
|
18
|
+
this.#ensureHiddenFields()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
toggle() {
|
|
22
|
+
if (!this.hasPanelTarget) return
|
|
23
|
+
|
|
24
|
+
this.expanded = !this.expanded
|
|
25
|
+
this.panelTarget.style.display = this.expanded ? "block" : "none"
|
|
26
|
+
|
|
27
|
+
if (this.hasToggleIconTarget) {
|
|
28
|
+
this.toggleIconTarget.classList.toggle("bi-chevron-down", !this.expanded)
|
|
29
|
+
this.toggleIconTarget.classList.toggle("bi-chevron-up", this.expanded)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (this.expanded && this.mcpServers.length === 0) {
|
|
33
|
+
this.#fetchMcpServers()
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
toggleServer(event) {
|
|
38
|
+
const serverUuid = event.currentTarget.dataset.serverUuid
|
|
39
|
+
const toolsContainer = this.serverListTarget.querySelector(
|
|
40
|
+
`[data-server-tools="${CSS.escape(serverUuid)}"]`
|
|
41
|
+
)
|
|
42
|
+
const icon = event.currentTarget.querySelector(".server-toggle-icon")
|
|
43
|
+
|
|
44
|
+
if (!toolsContainer) return
|
|
45
|
+
|
|
46
|
+
const isVisible = toolsContainer.style.display !== "none"
|
|
47
|
+
toolsContainer.style.display = isVisible ? "none" : "block"
|
|
48
|
+
icon.classList.toggle("bi-chevron-right", isVisible)
|
|
49
|
+
icon.classList.toggle("bi-chevron-down", !isVisible)
|
|
50
|
+
|
|
51
|
+
// Fetch tools if not yet loaded
|
|
52
|
+
if (
|
|
53
|
+
!isVisible &&
|
|
54
|
+
toolsContainer.dataset.loaded !== "true"
|
|
55
|
+
) {
|
|
56
|
+
this.#fetchToolsForServer(serverUuid, toolsContainer)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
toggleTool(event) {
|
|
61
|
+
const toolId = event.currentTarget.value
|
|
62
|
+
if (event.currentTarget.checked) {
|
|
63
|
+
this.selectedToolIds.add(toolId)
|
|
64
|
+
} else {
|
|
65
|
+
this.selectedToolIds.delete(toolId)
|
|
66
|
+
}
|
|
67
|
+
this.#updateCountBadge()
|
|
68
|
+
this.#updateHiddenFields()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async #fetchMcpServers() {
|
|
72
|
+
this.loadingTarget.style.display = "block"
|
|
73
|
+
this.serverListTarget.innerHTML = ""
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const response = await fetch("/api/mcp_servers", {
|
|
77
|
+
headers: { Accept: "application/json" },
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
if (!response.ok) {
|
|
81
|
+
throw new Error(`HTTP ${response.status}`)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const data = await response.json()
|
|
85
|
+
this.mcpServers = data.mcp_servers || []
|
|
86
|
+
|
|
87
|
+
if (this.mcpServers.length === 0) {
|
|
88
|
+
this.serverListTarget.innerHTML =
|
|
89
|
+
'<div class="no-servers">No MCP servers available</div>'
|
|
90
|
+
} else {
|
|
91
|
+
this.#renderServerList()
|
|
92
|
+
}
|
|
93
|
+
} catch (e) {
|
|
94
|
+
console.error("Failed to fetch MCP servers:", e)
|
|
95
|
+
this.serverListTarget.innerHTML =
|
|
96
|
+
'<div class="no-servers">Failed to load MCP servers</div>'
|
|
97
|
+
} finally {
|
|
98
|
+
this.loadingTarget.style.display = "none"
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
#renderServerList() {
|
|
103
|
+
this.serverListTarget.innerHTML = ""
|
|
104
|
+
|
|
105
|
+
for (const server of this.mcpServers) {
|
|
106
|
+
if (!server.active) continue
|
|
107
|
+
|
|
108
|
+
const serverDiv = document.createElement("div")
|
|
109
|
+
serverDiv.className = "mcp-server-item"
|
|
110
|
+
const escapedUuid = this.#escapeAttr(server.uuid)
|
|
111
|
+
serverDiv.innerHTML = `
|
|
112
|
+
<div class="mcp-server-header" data-action="click->tool-selector#toggleServer" data-server-uuid="${escapedUuid}">
|
|
113
|
+
<i class="bi bi-chevron-right server-toggle-icon"></i>
|
|
114
|
+
<i class="bi bi-server"></i>
|
|
115
|
+
<span class="mcp-server-name">${this.#escapeHtml(server.name)}</span>
|
|
116
|
+
${server.tools && server.tools.length > 0 ? `<span class="tool-available-count">${server.tools.filter((t) => t.active).length} tools</span>` : ""}
|
|
117
|
+
</div>
|
|
118
|
+
<div class="mcp-server-tools" data-server-tools="${escapedUuid}" style="display: none;" data-loaded="${server.tools && server.tools.length > 0 ? "true" : "false"}">
|
|
119
|
+
${server.tools && server.tools.length > 0 ? this.#renderTools(server.tools) : '<div class="tool-loading-inline">Click to load tools...</div>'}
|
|
120
|
+
</div>
|
|
121
|
+
`
|
|
122
|
+
this.serverListTarget.appendChild(serverDiv)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
#renderTools(tools) {
|
|
127
|
+
const activeTools = tools.filter((t) => t.active)
|
|
128
|
+
if (activeTools.length === 0) {
|
|
129
|
+
return '<div class="no-tools">No active tools</div>'
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return activeTools
|
|
133
|
+
.map(
|
|
134
|
+
(tool) => `
|
|
135
|
+
<label class="tool-item">
|
|
136
|
+
<input type="checkbox"
|
|
137
|
+
value="${this.#escapeAttr(String(tool.id))}"
|
|
138
|
+
data-action="change->tool-selector#toggleTool"
|
|
139
|
+
${this.selectedToolIds.has(String(tool.id)) ? "checked" : ""}>
|
|
140
|
+
<div class="tool-info">
|
|
141
|
+
<span class="tool-name">${this.#escapeHtml(tool.name)}</span>
|
|
142
|
+
${tool.description ? `<span class="tool-description">${this.#escapeHtml(tool.description)}</span>` : ""}
|
|
143
|
+
</div>
|
|
144
|
+
</label>
|
|
145
|
+
`
|
|
146
|
+
)
|
|
147
|
+
.join("")
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async #fetchToolsForServer(serverUuid, container) {
|
|
151
|
+
container.innerHTML =
|
|
152
|
+
'<div class="tool-loading-inline">Loading tools...</div>'
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const response = await fetch(
|
|
156
|
+
`/api/mcp_servers/${encodeURIComponent(serverUuid)}/tools`,
|
|
157
|
+
{
|
|
158
|
+
headers: { Accept: "application/json" },
|
|
159
|
+
}
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
if (!response.ok) {
|
|
163
|
+
throw new Error(`HTTP ${response.status}`)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const data = await response.json()
|
|
167
|
+
const tools = data.tools || []
|
|
168
|
+
|
|
169
|
+
container.dataset.loaded = "true"
|
|
170
|
+
container.innerHTML = this.#renderTools(tools)
|
|
171
|
+
|
|
172
|
+
// Update cached server data
|
|
173
|
+
const server = this.mcpServers.find((s) => s.uuid === serverUuid)
|
|
174
|
+
if (server) {
|
|
175
|
+
server.tools = tools
|
|
176
|
+
}
|
|
177
|
+
} catch (e) {
|
|
178
|
+
console.error("Failed to fetch tools:", e)
|
|
179
|
+
container.innerHTML =
|
|
180
|
+
'<div class="no-tools">Failed to load tools</div>'
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
#updateCountBadge() {
|
|
185
|
+
const count = this.selectedToolIds.size
|
|
186
|
+
this.countBadgeTarget.textContent = count
|
|
187
|
+
this.countBadgeTarget.style.display = count > 0 ? "inline-block" : "none"
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
#ensureHiddenFields() {
|
|
191
|
+
// Container for hidden tool_ids fields
|
|
192
|
+
let container = this.element.querySelector(".tool-ids-hidden-fields")
|
|
193
|
+
if (!container) {
|
|
194
|
+
container = document.createElement("div")
|
|
195
|
+
container.className = "tool-ids-hidden-fields"
|
|
196
|
+
container.style.display = "none"
|
|
197
|
+
this.element.appendChild(container)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
#updateHiddenFields() {
|
|
202
|
+
const container = this.element.querySelector(".tool-ids-hidden-fields")
|
|
203
|
+
if (!container) return
|
|
204
|
+
|
|
205
|
+
container.innerHTML = ""
|
|
206
|
+
for (const toolId of this.selectedToolIds) {
|
|
207
|
+
const input = document.createElement("input")
|
|
208
|
+
input.type = "hidden"
|
|
209
|
+
input.name = "tool_ids[]"
|
|
210
|
+
input.value = toolId
|
|
211
|
+
container.appendChild(input)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
#escapeHtml(text) {
|
|
216
|
+
const div = document.createElement("div")
|
|
217
|
+
div.textContent = text
|
|
218
|
+
return div.innerHTML
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
#escapeAttr(text) {
|
|
222
|
+
return String(text)
|
|
223
|
+
.replace(/&/g, "&")
|
|
224
|
+
.replace(/"/g, """)
|
|
225
|
+
.replace(/'/g, "'")
|
|
226
|
+
.replace(/</g, "<")
|
|
227
|
+
.replace(/>/g, ">")
|
|
228
|
+
}
|
|
229
|
+
}
|
|
@@ -67,8 +67,8 @@ class Chat < ApplicationRecord
|
|
|
67
67
|
end
|
|
68
68
|
|
|
69
69
|
# Add assistant response by sending to LLM
|
|
70
|
-
def add_assistant_response(prompt_execution, jwt_token)
|
|
71
|
-
response_content = send_to_llm(jwt_token)
|
|
70
|
+
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)
|
|
72
72
|
prompt_execution.update!(
|
|
73
73
|
llm_platform: llm_type(jwt_token),
|
|
74
74
|
response: response_content
|
|
@@ -122,7 +122,7 @@ class Chat < ApplicationRecord
|
|
|
122
122
|
end
|
|
123
123
|
|
|
124
124
|
# Send messages to LLM and get response
|
|
125
|
-
def send_to_llm(jwt_token)
|
|
125
|
+
def send_to_llm(jwt_token, tool_ids: [], generation_settings: {})
|
|
126
126
|
# Get LLM options
|
|
127
127
|
llm_options = LlmMetaClient::ServerResource.available_llm_options(jwt_token)
|
|
128
128
|
|
|
@@ -148,7 +148,7 @@ class Chat < ApplicationRecord
|
|
|
148
148
|
|
|
149
149
|
summarized_context += "Additional prompt: Responses from the assistant must consist solely of the response body."
|
|
150
150
|
|
|
151
|
-
# Send chat request using
|
|
152
|
-
LlmMetaClient::ServerQuery.new.call(jwt_token, llm_uuid, model, summarized_context, prompt)
|
|
151
|
+
# Send chat request using LlmMetaClient::ServerQuery
|
|
152
|
+
LlmMetaClient::ServerQuery.new.call(jwt_token, llm_uuid, model, summarized_context, prompt, tool_ids: tool_ids, generation_settings: generation_settings)
|
|
153
153
|
end
|
|
154
154
|
end
|
|
@@ -15,6 +15,10 @@
|
|
|
15
15
|
<%%= render "shared/api_key_field", stimulus_controller: "llm-selector" %>
|
|
16
16
|
<%%= render "shared/model_field", stimulus_controller: "llm-selector" %>
|
|
17
17
|
</div>
|
|
18
|
+
<%% if user_signed_in? %>
|
|
19
|
+
<%%= render "shared/tool_selector_field", stimulus_controller: "tool-selector" %>
|
|
20
|
+
<%% end %>
|
|
21
|
+
<%%= render "shared/generation_settings_field", stimulus_controller: "generation-settings" %>
|
|
18
22
|
<%% end %>
|
|
19
23
|
<div class="input-wrapper">
|
|
20
24
|
<%%= f.text_area :message,
|
|
@@ -15,6 +15,10 @@
|
|
|
15
15
|
<%%= render "shared/api_key_field", stimulus_controller: "llm-selector" %>
|
|
16
16
|
<%%= render "shared/model_field", stimulus_controller: "llm-selector" %>
|
|
17
17
|
</div>
|
|
18
|
+
<%% if user_signed_in? %>
|
|
19
|
+
<%%= render "shared/tool_selector_field", stimulus_controller: "tool-selector" %>
|
|
20
|
+
<%% end %>
|
|
21
|
+
<%%= render "shared/generation_settings_field", stimulus_controller: "generation-settings" %>
|
|
18
22
|
<%% end %>
|
|
19
23
|
<div class="input-wrapper">
|
|
20
24
|
<%%= f.text_area :message,
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
<%%
|
|
2
|
+
stimulus_controller = local_assigns[:stimulus_controller] || "generation-settings"
|
|
3
|
+
%>
|
|
4
|
+
<div class="generation-settings-field" data-controller="<%%= stimulus_controller %>">
|
|
5
|
+
<div class="generation-settings-toggle">
|
|
6
|
+
<button type="button"
|
|
7
|
+
class="generation-settings-toggle-button"
|
|
8
|
+
data-<%%= stimulus_controller %>-target="toggleButton"
|
|
9
|
+
data-action="click-><%%= stimulus_controller %>#toggle">
|
|
10
|
+
<i class="bi bi-sliders"></i>
|
|
11
|
+
Generation Settings
|
|
12
|
+
<i class="bi bi-chevron-down toggle-icon" data-<%%= stimulus_controller %>-target="toggleIcon"></i>
|
|
13
|
+
</button>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="generation-settings-panel" data-<%%= stimulus_controller %>-target="panel" style="display: none;">
|
|
16
|
+
<div class="generation-setting-item">
|
|
17
|
+
<label for="temperature">
|
|
18
|
+
Temperature
|
|
19
|
+
<span class="setting-value" data-<%%= stimulus_controller %>-target="temperatureValue">0.7</span>
|
|
20
|
+
</label>
|
|
21
|
+
<input type="range" name="temperature" id="temperature"
|
|
22
|
+
min="0" max="2" step="0.1" value="0.7"
|
|
23
|
+
data-<%%= stimulus_controller %>-target="temperatureRange"
|
|
24
|
+
data-action="input-><%%= stimulus_controller %>#updateTemperature">
|
|
25
|
+
<div class="setting-range-labels">
|
|
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>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<%%
|
|
2
|
+
stimulus_controller = local_assigns[:stimulus_controller] || "tool-selector"
|
|
3
|
+
%>
|
|
4
|
+
<div class="tool-selector-field" data-controller="<%%= stimulus_controller %>">
|
|
5
|
+
<div class="tool-selector-toggle">
|
|
6
|
+
<button type="button"
|
|
7
|
+
class="tool-toggle-button"
|
|
8
|
+
data-<%%= stimulus_controller %>-target="toggleButton"
|
|
9
|
+
data-action="click-><%%= stimulus_controller %>#toggle">
|
|
10
|
+
<i class="bi bi-tools"></i>
|
|
11
|
+
Tools
|
|
12
|
+
<span class="tool-count-badge" data-<%%= stimulus_controller %>-target="countBadge" style="display: none;">0</span>
|
|
13
|
+
<i class="bi bi-chevron-down toggle-icon" data-<%%= stimulus_controller %>-target="toggleIcon"></i>
|
|
14
|
+
</button>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="tool-selector-panel" data-<%%= stimulus_controller %>-target="panel" style="display: none;">
|
|
17
|
+
<div class="tool-loading" data-<%%= stimulus_controller %>-target="loading" style="display: none;">
|
|
18
|
+
Loading tools...
|
|
19
|
+
</div>
|
|
20
|
+
<div class="mcp-server-list" data-<%%= stimulus_controller %>-target="serverList">
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
module LlmMetaClient
|
|
2
2
|
class ServerQuery
|
|
3
|
-
def call(id_token, api_key_uuid, model_id, context, user_content)
|
|
3
|
+
def call(id_token, api_key_uuid, model_id, context, user_content, tool_ids: [], generation_settings: {})
|
|
4
4
|
debug_log "Context: #{context}"
|
|
5
5
|
context_and_user_content = "Context:#{context}, User Prompt: #{user_content}"
|
|
6
6
|
debug_log "Request to LLM: \n===>\n#{context_and_user_content}\n===>"
|
|
7
7
|
|
|
8
|
-
response = request(api_key_uuid, id_token, model_id, context_and_user_content)
|
|
8
|
+
response = request(api_key_uuid, id_token, model_id, context_and_user_content, tool_ids, generation_settings)
|
|
9
9
|
|
|
10
10
|
raise Exceptions::ServerError, "LLM server returned HTTP #{response.code}" unless response.success?
|
|
11
11
|
|
|
@@ -28,14 +28,18 @@ module LlmMetaClient
|
|
|
28
28
|
Rails.logger.info(message) if Rails.env.development?
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
def request(api_key_uuid, id_token, model_id, user_content)
|
|
31
|
+
def request(api_key_uuid, id_token, model_id, user_content, tool_ids, generation_settings)
|
|
32
32
|
headers = { "Content-Type" => "application/json" }
|
|
33
33
|
headers["Authorization"] = "Bearer #{id_token}" if id_token.present?
|
|
34
34
|
|
|
35
|
+
body = { prompt: user_content.to_s }
|
|
36
|
+
body[:tool_ids] = tool_ids if tool_ids.present?
|
|
37
|
+
body[:generation_settings] = generation_settings if generation_settings.present?
|
|
38
|
+
|
|
35
39
|
HTTParty.post(
|
|
36
40
|
url(api_key_uuid, model_id),
|
|
37
41
|
headers: headers,
|
|
38
|
-
body:
|
|
42
|
+
body: body.to_json,
|
|
39
43
|
timeout: 300 # 5 minute timeout setting (both read and connect)
|
|
40
44
|
)
|
|
41
45
|
end
|
|
@@ -53,6 +53,38 @@ module LlmMetaClient
|
|
|
53
53
|
build_families(ollama_opts, api_keys)
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
+
def fetch_mcp_servers(jwt_token)
|
|
57
|
+
return [] if jwt_token.blank?
|
|
58
|
+
|
|
59
|
+
response = authenticated_get(jwt_token, "api/mcp_servers")
|
|
60
|
+
|
|
61
|
+
if response.success?
|
|
62
|
+
response.parsed_response["mcp_servers"] || []
|
|
63
|
+
else
|
|
64
|
+
Rails.logger.error "Failed to fetch MCP servers: HTTP #{response.code}"
|
|
65
|
+
[]
|
|
66
|
+
end
|
|
67
|
+
rescue StandardError => e
|
|
68
|
+
Rails.logger.error "Error fetching MCP servers: #{e.class} - #{e.message}"
|
|
69
|
+
[]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def fetch_mcp_tools(jwt_token, mcp_server_uuid)
|
|
73
|
+
return [] if jwt_token.blank? || mcp_server_uuid.blank?
|
|
74
|
+
|
|
75
|
+
response = authenticated_get(jwt_token, "api/mcp_servers/#{mcp_server_uuid}/tools")
|
|
76
|
+
|
|
77
|
+
if response.success?
|
|
78
|
+
response.parsed_response["tools"] || []
|
|
79
|
+
else
|
|
80
|
+
Rails.logger.error "Failed to fetch MCP tools for #{mcp_server_uuid}: HTTP #{response.code}"
|
|
81
|
+
[]
|
|
82
|
+
end
|
|
83
|
+
rescue StandardError => e
|
|
84
|
+
Rails.logger.error "Error fetching MCP tools: #{e.class} - #{e.message}"
|
|
85
|
+
[]
|
|
86
|
+
end
|
|
87
|
+
|
|
56
88
|
private
|
|
57
89
|
|
|
58
90
|
def build_families(ollama_opts, api_keys)
|
|
@@ -110,10 +142,7 @@ module LlmMetaClient
|
|
|
110
142
|
end
|
|
111
143
|
|
|
112
144
|
def llm_api_keys(jwt_token)
|
|
113
|
-
|
|
114
|
-
headers = { "Content-Type" => "application/json", "Authorization" => "Bearer #{jwt_token}" }
|
|
115
|
-
|
|
116
|
-
response = HTTParty.get api_url, headers: headers
|
|
145
|
+
response = authenticated_get(jwt_token, "api/llm_api_keys")
|
|
117
146
|
|
|
118
147
|
if response.success?
|
|
119
148
|
response.parsed_response["llm_api_keys"] || []
|
|
@@ -122,6 +151,15 @@ module LlmMetaClient
|
|
|
122
151
|
[]
|
|
123
152
|
end
|
|
124
153
|
end
|
|
154
|
+
|
|
155
|
+
def authenticated_get(jwt_token, path)
|
|
156
|
+
api_url = "#{Rails.configuration.llm_service_base_url}/#{path}"
|
|
157
|
+
headers = {
|
|
158
|
+
"Content-Type" => "application/json",
|
|
159
|
+
"Authorization" => "Bearer #{jwt_token}"
|
|
160
|
+
}
|
|
161
|
+
HTTParty.get(api_url, headers: headers)
|
|
162
|
+
end
|
|
125
163
|
end
|
|
126
164
|
end
|
|
127
165
|
end
|
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: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- dhq_boiler
|
|
@@ -85,6 +85,7 @@ files:
|
|
|
85
85
|
- README.md
|
|
86
86
|
- Rakefile
|
|
87
87
|
- app/assets/stylesheets/llm_meta_client/application.css
|
|
88
|
+
- app/assets/stylesheets/llm_meta_client/generation_settings.css
|
|
88
89
|
- app/controllers/llm_meta_client/application_controller.rb
|
|
89
90
|
- app/helpers/llm_meta_client/application_helper.rb
|
|
90
91
|
- app/jobs/llm_meta_client/application_job.rb
|
|
@@ -101,11 +102,14 @@ files:
|
|
|
101
102
|
- lib/generators/llm_meta_client/authentication/templates/config/locales/devise.en.yml
|
|
102
103
|
- lib/generators/llm_meta_client/authentication/templates/db/migrate/create_users.rb
|
|
103
104
|
- lib/generators/llm_meta_client/scaffold/scaffold_generator.rb
|
|
105
|
+
- lib/generators/llm_meta_client/scaffold/templates/app/controllers/api/mcp_servers_controller.rb
|
|
104
106
|
- lib/generators/llm_meta_client/scaffold/templates/app/controllers/chats_controller.rb
|
|
105
107
|
- lib/generators/llm_meta_client/scaffold/templates/app/controllers/prompts_controller.rb
|
|
106
108
|
- lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/chat_title_edit_controller.js
|
|
107
109
|
- lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/chats_form_controller.js
|
|
110
|
+
- lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/generation_settings_controller.js
|
|
108
111
|
- lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/llm_selector_controller.js
|
|
112
|
+
- lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/tool_selector_controller.js
|
|
109
113
|
- lib/generators/llm_meta_client/scaffold/templates/app/javascript/popover.js
|
|
110
114
|
- lib/generators/llm_meta_client/scaffold/templates/app/models/chat.rb
|
|
111
115
|
- lib/generators/llm_meta_client/scaffold/templates/app/models/message.rb
|
|
@@ -120,7 +124,9 @@ files:
|
|
|
120
124
|
- lib/generators/llm_meta_client/scaffold/templates/app/views/layouts/application.html.erb
|
|
121
125
|
- lib/generators/llm_meta_client/scaffold/templates/app/views/shared/_api_key_field.html.erb
|
|
122
126
|
- lib/generators/llm_meta_client/scaffold/templates/app/views/shared/_family_field.html.erb
|
|
127
|
+
- lib/generators/llm_meta_client/scaffold/templates/app/views/shared/_generation_settings_field.html.erb
|
|
123
128
|
- lib/generators/llm_meta_client/scaffold/templates/app/views/shared/_model_field.html.erb
|
|
129
|
+
- lib/generators/llm_meta_client/scaffold/templates/app/views/shared/_tool_selector_field.html.erb
|
|
124
130
|
- lib/generators/llm_meta_client/scaffold/templates/config/initializers/llm_service.rb
|
|
125
131
|
- lib/generators/llm_meta_client/scaffold/templates/db/migrate/create_chats.rb
|
|
126
132
|
- lib/generators/llm_meta_client/scaffold/templates/db/migrate/create_messages.rb
|