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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ee640298ecdea5af5852a316bf54188f52b3a924380b7dcb28d0fde6a786f679
4
- data.tar.gz: 1a35dd306cbb14c0144ea5b5b581ee136caf1b33513ce0ad53559d1015c453c1
3
+ metadata.gz: 60116965474267f22c077da1849611c0b6c4490a876a98deebfa243d2f9aef49
4
+ data.tar.gz: 748fdc1631edcb65c7792b1dc13a3ae8d0625422c05d4e4a47732f9c8167a7d9
5
5
  SHA512:
6
- metadata.gz: 6c61cbaed89ffc56e1e5112cd81895da2434715fb88ff00ab1c6abc8859cd9265d29b3526e84402636adec11b7a99e78318608c1c1ef9c395f3c2a7ecb8df6ec
7
- data.tar.gz: f71f3cedabba96f0ba6f336b409cac79b1f30f84c27bd3538857975f3db40d9f7eb2c73300aabbce35b922893c84a6cd351f469893fad01805631ddccf95bcac
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
 
@@ -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, "&amp;")
224
+ .replace(/"/g, "&quot;")
225
+ .replace(/'/g, "&#39;")
226
+ .replace(/</g, "&lt;")
227
+ .replace(/>/g, "&gt;")
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 LlmMetaServerQuery
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: { prompt: "#{user_content}" }.to_json,
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
- api_url = "#{Rails.configuration.llm_service_base_url}/api/llm_api_keys"
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
@@ -1,3 +1,3 @@
1
1
  module LlmMetaClient
2
- VERSION = "0.3.0"
2
+ VERSION = "0.5.0"
3
3
  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.3.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