llm_meta_client 0.3.0 → 0.4.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 +37 -0
- data/lib/generators/llm_meta_client/scaffold/scaffold_generator.rb +9 -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 +8 -2
- 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 +3 -0
- data/lib/generators/llm_meta_client/scaffold/templates/app/views/chats/new.html.erb +3 -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 +7 -4
- data/lib/llm_meta_client/server_resource.rb +42 -4
- data/lib/llm_meta_client/version.rb +1 -1
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 620e720960f5e05d563885fa2e00900311d6a7b4543fa13f9d3c476515edd40c
|
|
4
|
+
data.tar.gz: 33981d0657e520260b569873c5547e3279eba4325f88a812d8bf28d18941ac88
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a557328075624b4ff7d17eda2ed181e178f5dfda39a23a9f87029141e04b22b78e6f648f81d751f6c92add2f0b54462b28c443bce2d4cf6fcc90f0dee5e1f175
|
|
7
|
+
data.tar.gz: f5b8ecaeec9af8153d8bbbed4d6056ba5213d267fba01381088d1f53bee80d6f1525d04992b79325476da5ce664976da9ab831844b02828e6b2db9279eb02ef0
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,43 @@ 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.4.0] - 2026-03-11
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- MCP (Model Context Protocol) tool selection support:
|
|
13
|
+
- `ServerResource.fetch_mcp_servers` and `ServerResource.fetch_mcp_tools` for retrieving MCP server/tool data from the LLM service
|
|
14
|
+
- `Api::McpServersController` with `index` and `tools` endpoints
|
|
15
|
+
- API routes for MCP servers (`/api/mcp_servers` and `/api/mcp_servers/:uuid/tools`)
|
|
16
|
+
- `tool_ids` parameter support through `ServerQuery`, `Chat` model, and `ChatsController`
|
|
17
|
+
- Tool selector UI component (Stimulus controller + view partial) for selecting MCP tools in chat forms
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Extracted `authenticated_get` helper in `ServerResource` to reduce duplication in authenticated API calls
|
|
22
|
+
|
|
23
|
+
### Security
|
|
24
|
+
|
|
25
|
+
- Escape HTML attribute values (`server.uuid`, `tool.id`) in tool selector to prevent XSS
|
|
26
|
+
- Use `CSS.escape()` for `querySelector` and `encodeURIComponent()` for fetch URLs to prevent selector/URL injection
|
|
27
|
+
|
|
28
|
+
## [0.3.0] - 2026-03-05
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- Update Ruby version requirement from 3.4.8 to 4.0.1
|
|
33
|
+
- Update gem dependencies to latest versions
|
|
34
|
+
|
|
35
|
+
## [0.2.0] - 2026-03-04
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
|
|
39
|
+
- Switch configuration to use Rails credentials instead of environment variables
|
|
40
|
+
|
|
41
|
+
### Documentation
|
|
42
|
+
|
|
43
|
+
- Update README with architectural details, setup instructions, and Rails credentials usage
|
|
44
|
+
|
|
8
45
|
## [0.1.0] - 2026-02-27
|
|
9
46
|
|
|
10
47
|
### Added
|
|
@@ -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,7 @@ 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"
|
|
35
37
|
template "app/views/layouts/application.html.erb"
|
|
36
38
|
template "app/views/layouts/_header.html.erb"
|
|
37
39
|
template "app/views/layouts/_sidebar.html.erb"
|
|
@@ -41,6 +43,7 @@ module LlmMetaClient
|
|
|
41
43
|
template "app/javascript/controllers/llm_selector_controller.js"
|
|
42
44
|
template "app/javascript/controllers/chats_form_controller.js"
|
|
43
45
|
template "app/javascript/controllers/chat_title_edit_controller.js"
|
|
46
|
+
template "app/javascript/controllers/tool_selector_controller.js"
|
|
44
47
|
copy_file "app/javascript/popover.js"
|
|
45
48
|
end
|
|
46
49
|
|
|
@@ -69,6 +72,12 @@ module LlmMetaClient
|
|
|
69
72
|
end
|
|
70
73
|
end
|
|
71
74
|
resources :prompts, only: [ :show ]
|
|
75
|
+
|
|
76
|
+
namespace :api do
|
|
77
|
+
resources :mcp_servers, only: [ :index ], param: :uuid do
|
|
78
|
+
get :tools, on: :member
|
|
79
|
+
end
|
|
80
|
+
end
|
|
72
81
|
RUBY
|
|
73
82
|
end
|
|
74
83
|
|
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)
|
|
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)
|
|
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,10 @@ 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
|
|
188
194
|
end
|
|
@@ -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: [])
|
|
71
|
+
response_content = send_to_llm(jwt_token, tool_ids: tool_ids)
|
|
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: [])
|
|
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)
|
|
153
153
|
end
|
|
154
154
|
end
|
|
@@ -15,6 +15,9 @@
|
|
|
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 %>
|
|
18
21
|
<%% end %>
|
|
19
22
|
<div class="input-wrapper">
|
|
20
23
|
<%%= f.text_area :message,
|
|
@@ -15,6 +15,9 @@
|
|
|
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 %>
|
|
18
21
|
<%% end %>
|
|
19
22
|
<div class="input-wrapper">
|
|
20
23
|
<%%= f.text_area :message,
|
|
@@ -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: [])
|
|
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)
|
|
9
9
|
|
|
10
10
|
raise Exceptions::ServerError, "LLM server returned HTTP #{response.code}" unless response.success?
|
|
11
11
|
|
|
@@ -28,14 +28,17 @@ 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)
|
|
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
|
+
|
|
35
38
|
HTTParty.post(
|
|
36
39
|
url(api_key_uuid, model_id),
|
|
37
40
|
headers: headers,
|
|
38
|
-
body:
|
|
41
|
+
body: body.to_json,
|
|
39
42
|
timeout: 300 # 5 minute timeout setting (both read and connect)
|
|
40
43
|
)
|
|
41
44
|
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.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- dhq_boiler
|
|
@@ -101,11 +101,13 @@ files:
|
|
|
101
101
|
- lib/generators/llm_meta_client/authentication/templates/config/locales/devise.en.yml
|
|
102
102
|
- lib/generators/llm_meta_client/authentication/templates/db/migrate/create_users.rb
|
|
103
103
|
- lib/generators/llm_meta_client/scaffold/scaffold_generator.rb
|
|
104
|
+
- lib/generators/llm_meta_client/scaffold/templates/app/controllers/api/mcp_servers_controller.rb
|
|
104
105
|
- lib/generators/llm_meta_client/scaffold/templates/app/controllers/chats_controller.rb
|
|
105
106
|
- lib/generators/llm_meta_client/scaffold/templates/app/controllers/prompts_controller.rb
|
|
106
107
|
- lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/chat_title_edit_controller.js
|
|
107
108
|
- lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/chats_form_controller.js
|
|
108
109
|
- lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/llm_selector_controller.js
|
|
110
|
+
- lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/tool_selector_controller.js
|
|
109
111
|
- lib/generators/llm_meta_client/scaffold/templates/app/javascript/popover.js
|
|
110
112
|
- lib/generators/llm_meta_client/scaffold/templates/app/models/chat.rb
|
|
111
113
|
- lib/generators/llm_meta_client/scaffold/templates/app/models/message.rb
|
|
@@ -121,6 +123,7 @@ files:
|
|
|
121
123
|
- lib/generators/llm_meta_client/scaffold/templates/app/views/shared/_api_key_field.html.erb
|
|
122
124
|
- lib/generators/llm_meta_client/scaffold/templates/app/views/shared/_family_field.html.erb
|
|
123
125
|
- lib/generators/llm_meta_client/scaffold/templates/app/views/shared/_model_field.html.erb
|
|
126
|
+
- lib/generators/llm_meta_client/scaffold/templates/app/views/shared/_tool_selector_field.html.erb
|
|
124
127
|
- lib/generators/llm_meta_client/scaffold/templates/config/initializers/llm_service.rb
|
|
125
128
|
- lib/generators/llm_meta_client/scaffold/templates/db/migrate/create_chats.rb
|
|
126
129
|
- lib/generators/llm_meta_client/scaffold/templates/db/migrate/create_messages.rb
|