llm_meta_client 1.0.2 β 1.3.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 +49 -0
- data/lib/generators/llm_meta_client/scaffold/scaffold_generator.rb +6 -0
- data/lib/generators/llm_meta_client/scaffold/templates/app/controllers/chat_streams_controller.rb +101 -0
- data/lib/generators/llm_meta_client/scaffold/templates/app/controllers/chats_controller.rb +15 -24
- data/lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/message_stream_controller.js +124 -0
- data/lib/generators/llm_meta_client/scaffold/templates/app/models/chat.rb +58 -15
- data/lib/generators/llm_meta_client/scaffold/templates/app/views/chats/_chat_sidebar.html.erb +8 -0
- data/lib/generators/llm_meta_client/scaffold/templates/app/views/chats/_streaming_message.html.erb +12 -0
- data/lib/generators/llm_meta_client/scaffold/templates/app/views/chats/_tool_call_message.html.erb +22 -0
- data/lib/generators/llm_meta_client/scaffold/templates/app/views/chats/create.turbo_stream.erb +4 -6
- data/lib/generators/llm_meta_client/scaffold/templates/app/views/chats/update.turbo_stream.erb +3 -3
- data/lib/llm_meta_client/server_query.rb +157 -1
- data/lib/llm_meta_client/version.rb +1 -1
- metadata +15 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2fcc6377f3293f8ecd13b81cd79ea63891c18f55dfa05ee225a151f7e1fa5b84
|
|
4
|
+
data.tar.gz: 35c4cba209aed5989b43606715205a7e2b85c669fa7dd71d65a33f4dd47a293a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c959d77e7d3b8c9f5070bf2f63a74e83ba1082391694788e094c09629e76883dcf0142336af7742dd08fa46c0d36ec20ad31ad85b558ea4199cb8b7e3c4345fc
|
|
7
|
+
data.tar.gz: 651d88ddb211fd11234daf2ecee22e368d8886d27e43b94c1786d4d3980cedfb078a562ee0cd0ad06a48140c7fb07cae779326237e604b21a819d4ceb663d815
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,55 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.3.0] - 2026-05-10
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Tool-call streaming end-to-end. `ServerQuery#stream` now accepts `tool_ids:` and yields a `tool_calls` event when the LLM decides to invoke MCP tools. Turn 1 (tool selection) runs synchronously; turn 2 (the follow-up after tool execution) is streamed.
|
|
13
|
+
- Scaffold renders a separate "π Tool calls" bubble during streaming via the new `_tool_call_message.html.erb` partial. The Stimulus controller inserts it before the streaming bubble when `event: tool_calls` arrives, and removes it once the assistant message is saved (the saved bubble's combined markdown contains the tool-call section).
|
|
14
|
+
- `Chat#stream_assistant_response` accepts `tool_ids:` and threads them through. Persistence is unchanged β the saved `Message.response` includes a markdown "Tool calls" section appended to the response text, matching the existing synchronous shape.
|
|
15
|
+
- When `tool_ids` is non-empty, the system prompt is augmented with an instruction to explain tool errors rather than fail silently. Models that ignore the instruction are caught by a server-side fallback (see below).
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Streaming error messages now parse `error` + `message` from the response body so users see context (e.g. "Rate limit exceeded β check your provider planβ¦") instead of a bare HTTP code. Mid-stream `event: error` payloads with codes like `rate_limit` get the same friendlier treatment.
|
|
20
|
+
|
|
21
|
+
### Notes
|
|
22
|
+
|
|
23
|
+
- Requires `llm_meta_server` with the matching tool-streaming additions: `LlmRbFacade.stream!` accepting `tools:` + `on_tool_calls:` and an `Api::ChatStreamsController` that emits the `tool_calls` SSE event. Server-side fixes that ship alongside this release: an Anthropic-tool-only-response rehydrator (Claude tool-only completions weren't surfacing through `Session#functions`), and a sink injection that emits MCP `isError: true` payloads as text deltas before turn 2 (Gemini sometimes returns nothing after a tool error and would otherwise leave the bubble blank).
|
|
24
|
+
|
|
25
|
+
## [1.2.0] - 2026-05-10
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- End-to-end SSE streaming for chat completions:
|
|
30
|
+
- `ServerQuery#stream` consumes SSE from the new `chat_streams` endpoint on `llm_meta_server` and yields parsed events. Returns the assembled content; absorbs upstream `done` markers and raises `ServerError` on upstream `error` events.
|
|
31
|
+
- `Chat#stream_assistant_response` and `Chat#finalize_streamed_response` for streaming generation with persistence at stream close (assistant message saved only on success β disconnects mid-stream don't persist).
|
|
32
|
+
- Scaffold now generates `ChatStreamsController`, `_streaming_message.html.erb` partial, `message_stream_controller.js` Stimulus controller, and a shared `_chat_sidebar.html.erb` partial. Routes add `resource :stream` nested under `chats`.
|
|
33
|
+
- Streaming bubble swaps to the host-rendered `_message` partial on save, so any markdown / syntax-highlighting customization in the host's `_message.html.erb` applies post-stream.
|
|
34
|
+
- `event: title` SSE event includes a turbo_stream snippet that updates the chat-sidebar in place when a brand-new chat gets its auto-generated title.
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- `ServerQuery` error messages parse the JSON response body from `llm_meta_server` and surface friendlier text (rate limits, auth errors, upstream unavailable) instead of bare `HTTP <code>`.
|
|
39
|
+
- Streaming endpoint v1 does not pass `tool_ids`. The synchronous `#call` path is unchanged and still supports tool calls.
|
|
40
|
+
|
|
41
|
+
### Notes
|
|
42
|
+
|
|
43
|
+
- The streaming endpoint requires `llm_meta_server` with the matching `chat_streams` route. SSE delivery through reverse proxies needs `proxy_buffering off` (nginx) or `flushpackets=on` + `SetEnv no-gzip 1` (Apache).
|
|
44
|
+
|
|
45
|
+
## [1.1.1] - 2026-04-22
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
|
|
49
|
+
- `ServerQuery#call` now surfaces tool calls from the LLM server response. When the response includes a `tool_calls` array, a markdown-formatted "Tool calls" section (name + JSON args) is appended to the returned content (separated by a horizontal rule). This lets host apps display which tools the LLM invoked without any schema or view changes; existing markdown renderers pick it up automatically. Previously, tool calls were silently dropped.
|
|
50
|
+
|
|
51
|
+
## [1.1.0] - 2026-04-22
|
|
52
|
+
|
|
53
|
+
### Changed
|
|
54
|
+
|
|
55
|
+
- Widen `prompt_navigator` dependency constraint from `~> 1.0` to `>= 1.0, < 3.0` so host apps can opt into `prompt_navigator` 2.0 (which requires Ruby 3.4.9+ and adds `PromptExecution.delete_set!`). Existing hosts on `prompt_navigator` 1.x keep resolving unchanged.
|
|
56
|
+
|
|
8
57
|
## [1.0.2] - 2026-03-27
|
|
9
58
|
|
|
10
59
|
### Added
|
|
@@ -19,6 +19,7 @@ module LlmMetaClient
|
|
|
19
19
|
|
|
20
20
|
def create_controllers
|
|
21
21
|
template "app/controllers/chats_controller.rb"
|
|
22
|
+
template "app/controllers/chat_streams_controller.rb"
|
|
22
23
|
template "app/controllers/prompts_controller.rb"
|
|
23
24
|
template "app/controllers/api/mcp_servers_controller.rb"
|
|
24
25
|
end
|
|
@@ -29,6 +30,9 @@ module LlmMetaClient
|
|
|
29
30
|
template "app/views/chats/create.turbo_stream.erb"
|
|
30
31
|
template "app/views/chats/update.turbo_stream.erb"
|
|
31
32
|
template "app/views/chats/_message.html.erb"
|
|
33
|
+
template "app/views/chats/_streaming_message.html.erb"
|
|
34
|
+
template "app/views/chats/_tool_call_message.html.erb"
|
|
35
|
+
template "app/views/chats/_chat_sidebar.html.erb"
|
|
32
36
|
template "app/views/chats/_messages_list.html.erb"
|
|
33
37
|
template "app/views/shared/_family_field.html.erb"
|
|
34
38
|
template "app/views/shared/_api_key_field.html.erb"
|
|
@@ -46,6 +50,7 @@ module LlmMetaClient
|
|
|
46
50
|
template "app/javascript/controllers/chat_title_edit_controller.js"
|
|
47
51
|
template "app/javascript/controllers/tool_selector_controller.js"
|
|
48
52
|
template "app/javascript/controllers/generation_settings_controller.js"
|
|
53
|
+
template "app/javascript/controllers/message_stream_controller.js"
|
|
49
54
|
copy_file "app/javascript/popover.js"
|
|
50
55
|
end
|
|
51
56
|
|
|
@@ -73,6 +78,7 @@ module LlmMetaClient
|
|
|
73
78
|
patch :update_title
|
|
74
79
|
get :download_csv
|
|
75
80
|
end
|
|
81
|
+
resource :stream, only: [ :show ], controller: "chat_streams"
|
|
76
82
|
end
|
|
77
83
|
resources :prompts, only: [ :show ]
|
|
78
84
|
|
data/lib/generators/llm_meta_client/scaffold/templates/app/controllers/chat_streams_controller.rb
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
class ChatStreamsController < ApplicationController
|
|
2
|
+
include ActionController::Live
|
|
3
|
+
|
|
4
|
+
skip_before_action :authenticate_user!, raise: false
|
|
5
|
+
skip_before_action :verify_authenticity_token
|
|
6
|
+
|
|
7
|
+
def show
|
|
8
|
+
response.headers["Content-Type"] = "text/event-stream"
|
|
9
|
+
response.headers["Cache-Control"] = "no-cache"
|
|
10
|
+
response.headers["X-Accel-Buffering"] = "no"
|
|
11
|
+
|
|
12
|
+
chat = find_chat
|
|
13
|
+
prompt_execution = PromptNavigator::PromptExecution.find_by!(execution_id: params[:execution_id])
|
|
14
|
+
unless chat.messages.exists?(prompt_navigator_prompt_execution_id: prompt_execution.id)
|
|
15
|
+
raise ActiveRecord::RecordNotFound
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
jwt_token = current_user.id_token if user_signed_in?
|
|
19
|
+
generation_settings = parse_generation_settings(params[:generation_settings_json])
|
|
20
|
+
tool_ids = Array(params[:tool_ids]).reject(&:blank?)
|
|
21
|
+
|
|
22
|
+
assembled = chat.stream_assistant_response(prompt_execution, jwt_token, tool_ids: tool_ids, generation_settings: generation_settings) do |event|
|
|
23
|
+
if event[:event] == "tool_calls"
|
|
24
|
+
tool_calls = event[:data]["tool_calls"] || []
|
|
25
|
+
forward(event: "tool_calls", data: {
|
|
26
|
+
tool_calls: tool_calls,
|
|
27
|
+
html: view_context.render(partial: "chats/tool_call_message", locals: { tool_calls: tool_calls })
|
|
28
|
+
})
|
|
29
|
+
else
|
|
30
|
+
forward(event)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
if assembled.present?
|
|
35
|
+
assistant_message = chat.finalize_streamed_response(prompt_execution, assembled, jwt_token)
|
|
36
|
+
if assistant_message
|
|
37
|
+
forward(event: "saved", data: {
|
|
38
|
+
message_id: assistant_message.id,
|
|
39
|
+
execution_id: prompt_execution.execution_id,
|
|
40
|
+
html: view_context.render(partial: "chats/message", locals: { message: assistant_message })
|
|
41
|
+
})
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
title_before = chat.title
|
|
45
|
+
chat.generate_title(prompt_execution.prompt, jwt_token)
|
|
46
|
+
if chat.reload.title.present? && chat.title != title_before
|
|
47
|
+
forward(event: "title", data: {
|
|
48
|
+
title: chat.title,
|
|
49
|
+
chat_uuid: chat.uuid,
|
|
50
|
+
turbo_stream: render_sidebar_update(chat)
|
|
51
|
+
})
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
forward(event: "done", data: {})
|
|
56
|
+
rescue ActionController::Live::ClientDisconnected
|
|
57
|
+
Rails.logger.info "[ChatStream] client disconnected"
|
|
58
|
+
rescue ActiveRecord::RecordNotFound
|
|
59
|
+
forward(event: "error", data: { code: "not_found", message: "Chat or prompt execution not found" }) rescue nil
|
|
60
|
+
rescue StandardError => e
|
|
61
|
+
Rails.logger.error "[ChatStream] #{e.class}: #{e.message}"
|
|
62
|
+
forward(event: "error", data: { code: e.class.name, message: e.message }) rescue nil
|
|
63
|
+
ensure
|
|
64
|
+
response.stream.close
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def find_chat
|
|
70
|
+
scope = user_signed_in? ? current_user.chats : Chat.where(user_id: nil)
|
|
71
|
+
scope.find_by!(uuid: params[:chat_id])
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def forward(event)
|
|
75
|
+
name = event[:event]
|
|
76
|
+
payload = event[:data].to_json
|
|
77
|
+
if name.nil? || name == "message"
|
|
78
|
+
response.stream.write "data: #{payload}\n\n"
|
|
79
|
+
else
|
|
80
|
+
response.stream.write "event: #{name}\ndata: #{payload}\n\n"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def parse_generation_settings(raw)
|
|
85
|
+
return {} if raw.blank?
|
|
86
|
+
parsed = JSON.parse(raw)
|
|
87
|
+
parsed.is_a?(Hash) ? parsed.symbolize_keys : {}
|
|
88
|
+
rescue JSON::ParserError
|
|
89
|
+
{}
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def render_sidebar_update(chat)
|
|
93
|
+
initialize_chat(user_signed_in? ? current_user.chats : nil)
|
|
94
|
+
add_chat(chat)
|
|
95
|
+
view_context.turbo_stream.replace(
|
|
96
|
+
"chat-sidebar",
|
|
97
|
+
partial: "chats/chat_sidebar",
|
|
98
|
+
locals: { chat: chat }
|
|
99
|
+
).to_s
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -70,9 +70,10 @@ class ChatsController < ApplicationController
|
|
|
70
70
|
initialize_history @chat&.ordered_by_descending_prompt_executions
|
|
71
71
|
|
|
72
72
|
if params[:message].present?
|
|
73
|
-
# Validate generation settings before proceeding
|
|
73
|
+
# Validate generation settings before proceeding (raises if invalid).
|
|
74
|
+
# The streaming controller re-parses them from the URL.
|
|
74
75
|
begin
|
|
75
|
-
|
|
76
|
+
generation_settings_param
|
|
76
77
|
rescue InvalidGenerationSettingsError => e
|
|
77
78
|
@error_message = e.message
|
|
78
79
|
respond_to do |format|
|
|
@@ -92,15 +93,11 @@ class ChatsController < ApplicationController
|
|
|
92
93
|
# Set active message UUID for highlighting in UI
|
|
93
94
|
set_active_message_uuid(@prompt_execution&.execution_id || params.dig(:chat, :branch_from_uuid))
|
|
94
95
|
|
|
95
|
-
#
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
rescue StandardError => e
|
|
101
|
-
Rails.logger.error "Error in chat response: #{e.class} - #{e.message}\n#{e.backtrace&.join("\n")}"
|
|
102
|
-
@error_message = "An error occurred while getting the response. Please try again."
|
|
103
|
-
end
|
|
96
|
+
# The assistant response is streamed by ChatStreamsController (SSE).
|
|
97
|
+
# The streaming bubble is rendered by create.turbo_stream.erb and opens
|
|
98
|
+
# the EventSource on connect; persistence + title gen happen at stream close.
|
|
99
|
+
@generation_settings_json = params[:generation_settings_json]
|
|
100
|
+
@tool_ids = Array(params[:tool_ids]).reject(&:blank?)
|
|
104
101
|
end
|
|
105
102
|
|
|
106
103
|
# Return turbo stream to render both messages
|
|
@@ -167,9 +164,10 @@ class ChatsController < ApplicationController
|
|
|
167
164
|
initialize_history @chat&.ordered_by_descending_prompt_executions
|
|
168
165
|
|
|
169
166
|
if params[:message].present?
|
|
170
|
-
# Validate generation settings before proceeding
|
|
167
|
+
# Validate generation settings before proceeding (raises if invalid).
|
|
168
|
+
# The streaming controller re-parses them from the URL.
|
|
171
169
|
begin
|
|
172
|
-
|
|
170
|
+
generation_settings_param
|
|
173
171
|
rescue InvalidGenerationSettingsError => e
|
|
174
172
|
@error_message = e.message
|
|
175
173
|
respond_to do |format|
|
|
@@ -189,13 +187,10 @@ class ChatsController < ApplicationController
|
|
|
189
187
|
# Set active message UUID for highlighting in UI
|
|
190
188
|
set_active_message_uuid(@prompt_execution&.execution_id || params.dig(:chat, :branch_from_uuid))
|
|
191
189
|
|
|
192
|
-
#
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
Rails.logger.error "Error in chat response: #{e.class} - #{e.message}\n#{e.backtrace&.join("\n")}"
|
|
197
|
-
@error_message = "An error occurred while getting the response. Please try again."
|
|
198
|
-
end
|
|
190
|
+
# The assistant response is streamed by ChatStreamsController (SSE).
|
|
191
|
+
# See create action for details.
|
|
192
|
+
@generation_settings_json = params[:generation_settings_json]
|
|
193
|
+
@tool_ids = Array(params[:tool_ids]).reject(&:blank?)
|
|
199
194
|
end
|
|
200
195
|
|
|
201
196
|
# Return turbo stream to render both messages
|
|
@@ -207,10 +202,6 @@ class ChatsController < ApplicationController
|
|
|
207
202
|
|
|
208
203
|
private
|
|
209
204
|
|
|
210
|
-
def tool_ids_param
|
|
211
|
-
params[:tool_ids].presence || []
|
|
212
|
-
end
|
|
213
|
-
|
|
214
205
|
ALLOWED_GENERATION_KEYS = %w[temperature top_k top_p max_tokens repeat_penalty].freeze
|
|
215
206
|
|
|
216
207
|
class InvalidGenerationSettingsError < StandardError; end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// Connects to data-controller="message-stream"
|
|
4
|
+
// Opens an EventSource on connect, appends each delta to the content target,
|
|
5
|
+
// closes on `done` / `error`.
|
|
6
|
+
export default class extends Controller {
|
|
7
|
+
static targets = ["content"]
|
|
8
|
+
static values = { url: String }
|
|
9
|
+
|
|
10
|
+
connect() {
|
|
11
|
+
this.completed = false
|
|
12
|
+
this.source = new EventSource(this.urlValue)
|
|
13
|
+
this.source.addEventListener("message", (e) => this.#onDelta(e))
|
|
14
|
+
this.source.addEventListener("done", () => this.#onDone())
|
|
15
|
+
this.source.addEventListener("title", (e) => this.#onTitle(e))
|
|
16
|
+
this.source.addEventListener("saved", (e) => this.#onSaved(e))
|
|
17
|
+
this.source.addEventListener("tool_calls", (e) => this.#onToolCalls(e))
|
|
18
|
+
this.source.addEventListener("error", (e) => this.#onError(e))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
disconnect() {
|
|
22
|
+
this.#close()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
#onDelta(event) {
|
|
26
|
+
let delta
|
|
27
|
+
try { delta = JSON.parse(event.data).delta } catch { return }
|
|
28
|
+
if (!delta) return
|
|
29
|
+
this.contentTarget.append(delta)
|
|
30
|
+
this.#scrollToBottom()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
#onTitle(event) {
|
|
34
|
+
try {
|
|
35
|
+
const data = JSON.parse(event.data)
|
|
36
|
+
if (data.turbo_stream && window.Turbo) {
|
|
37
|
+
window.Turbo.renderStreamMessage(data.turbo_stream)
|
|
38
|
+
}
|
|
39
|
+
} catch {}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
#onSaved(event) {
|
|
43
|
+
try {
|
|
44
|
+
const data = JSON.parse(event.data)
|
|
45
|
+
this.element.dataset.savedExecutionId = data.execution_id
|
|
46
|
+
if (data.html) this.#swapInRenderedMessage(data.html)
|
|
47
|
+
// The saved bubble's content already includes any tool calls section in
|
|
48
|
+
// markdown; remove the transient tool-call bubbles so reload and live look
|
|
49
|
+
// the same.
|
|
50
|
+
this.#removeTransientToolCallBubbles()
|
|
51
|
+
} catch {}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
#onToolCalls(event) {
|
|
55
|
+
try {
|
|
56
|
+
const data = JSON.parse(event.data)
|
|
57
|
+
if (!data.html) return
|
|
58
|
+
const wrapper = document.createElement("template")
|
|
59
|
+
wrapper.innerHTML = data.html.trim()
|
|
60
|
+
const bubble = wrapper.content.firstElementChild
|
|
61
|
+
if (!bubble) return
|
|
62
|
+
bubble.classList.add("tool-call-streaming")
|
|
63
|
+
this.element.parentNode.insertBefore(bubble, this.element)
|
|
64
|
+
this.#scrollToBottom()
|
|
65
|
+
} catch {}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
#removeTransientToolCallBubbles() {
|
|
69
|
+
document.querySelectorAll(".tool-call-streaming").forEach((el) => el.remove())
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Swap the streaming bubble's role + content with the host-rendered _message
|
|
73
|
+
// partial output so any markdown / syntax highlighting / partial customizations
|
|
74
|
+
// applied on reload also apply right after the stream finishes. We don't
|
|
75
|
+
// replace the whole element β that would disconnect this controller and
|
|
76
|
+
// close the EventSource before `title` / `done` arrive.
|
|
77
|
+
#swapInRenderedMessage(html) {
|
|
78
|
+
const doc = new DOMParser().parseFromString(html, "text/html")
|
|
79
|
+
const newBubble = doc.querySelector(".message")
|
|
80
|
+
if (!newBubble) return
|
|
81
|
+
|
|
82
|
+
const newRole = newBubble.querySelector(".message-role")
|
|
83
|
+
const oldRole = this.element.querySelector(".message-role")
|
|
84
|
+
if (newRole && oldRole) oldRole.innerHTML = newRole.innerHTML
|
|
85
|
+
|
|
86
|
+
const newContent = newBubble.querySelector(".message-content")
|
|
87
|
+
if (newContent) this.contentTarget.innerHTML = newContent.innerHTML
|
|
88
|
+
|
|
89
|
+
this.element.classList.remove("streaming")
|
|
90
|
+
if (newBubble.id) this.element.id = newBubble.id
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
#onDone() {
|
|
94
|
+
this.completed = true
|
|
95
|
+
this.#close()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
#onError(event) {
|
|
99
|
+
// EventSource fires onerror whenever the connection closes β including
|
|
100
|
+
// immediately after a clean `event: done`. Suppress those.
|
|
101
|
+
if (this.completed) {
|
|
102
|
+
this.#close()
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
let message = "Stream interrupted."
|
|
106
|
+
try { if (event.data) message = JSON.parse(event.data).message || message } catch {}
|
|
107
|
+
const errEl = document.createElement("p")
|
|
108
|
+
errEl.className = "stream-error"
|
|
109
|
+
errEl.textContent = `[error] ${message}`
|
|
110
|
+
this.contentTarget.appendChild(errEl)
|
|
111
|
+
this.#close()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
#close() {
|
|
115
|
+
if (this.source && this.source.readyState !== EventSource.CLOSED) {
|
|
116
|
+
this.source.close()
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
#scrollToBottom() {
|
|
121
|
+
const chatMessages = document.getElementById("chat-messages")
|
|
122
|
+
if (chatMessages) chatMessages.scrollTop = chatMessages.scrollHeight
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -68,6 +68,37 @@ class Chat < ApplicationRecord
|
|
|
68
68
|
new_message
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
+
# Stream the assistant response from the LLM. Yields each parsed SSE event.
|
|
72
|
+
# Returns the assembled content (with markdown "Tool calls" section appended
|
|
73
|
+
# if tools fired). Caller is responsible for persistence.
|
|
74
|
+
def stream_assistant_response(prompt_execution, jwt_token, tool_ids: [], generation_settings: {}, &block)
|
|
75
|
+
summarized_context, prompt = build_streaming_context(prompt_execution, jwt_token, with_tools: tool_ids.any?)
|
|
76
|
+
LlmMetaClient::ServerQuery.new.stream(
|
|
77
|
+
jwt_token,
|
|
78
|
+
prompt_execution.llm_uuid,
|
|
79
|
+
prompt_execution.model,
|
|
80
|
+
summarized_context,
|
|
81
|
+
prompt,
|
|
82
|
+
tool_ids: tool_ids,
|
|
83
|
+
generation_settings: generation_settings,
|
|
84
|
+
&block
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Persist the streamed assistant response. Skips persistence if content is blank.
|
|
89
|
+
def finalize_streamed_response(prompt_execution, content, jwt_token)
|
|
90
|
+
return nil if content.blank?
|
|
91
|
+
|
|
92
|
+
prompt_execution.update!(
|
|
93
|
+
llm_platform: resolve_llm_type(prompt_execution.llm_uuid, jwt_token),
|
|
94
|
+
response: content
|
|
95
|
+
)
|
|
96
|
+
messages.create!(
|
|
97
|
+
role: "assistant",
|
|
98
|
+
prompt_navigator_prompt_execution: prompt_execution
|
|
99
|
+
)
|
|
100
|
+
end
|
|
101
|
+
|
|
71
102
|
# Get all messages in order
|
|
72
103
|
def ordered_messages
|
|
73
104
|
messages
|
|
@@ -115,31 +146,43 @@ class Chat < ApplicationRecord
|
|
|
115
146
|
|
|
116
147
|
# Send messages to LLM and get response
|
|
117
148
|
def send_to_llm(prompt_execution, jwt_token, tool_ids: [], generation_settings: {})
|
|
118
|
-
|
|
119
|
-
|
|
149
|
+
summarized_context, prompt = build_streaming_context(prompt_execution, jwt_token, with_tools: tool_ids.any?)
|
|
150
|
+
LlmMetaClient::ServerQuery.new.call(
|
|
151
|
+
jwt_token,
|
|
152
|
+
prompt_execution.llm_uuid,
|
|
153
|
+
prompt_execution.model,
|
|
154
|
+
summarized_context,
|
|
155
|
+
prompt,
|
|
156
|
+
tool_ids: tool_ids,
|
|
157
|
+
generation_settings: generation_settings
|
|
158
|
+
)
|
|
159
|
+
end
|
|
120
160
|
|
|
121
|
-
|
|
161
|
+
# Build the (summarized_context, prompt) tuple for an LLM call.
|
|
162
|
+
# Shared by both the synchronous and streaming paths.
|
|
163
|
+
def build_streaming_context(prompt_execution, jwt_token, with_tools: false)
|
|
122
164
|
llm_options = LlmMetaClient::ServerResource.available_llm_options(jwt_token)
|
|
123
|
-
|
|
124
|
-
# Error if no LLM is available
|
|
125
165
|
raise LlmMetaClient::Exceptions::OllamaUnavailableError, "No LLM available" if llm_options.empty?
|
|
126
166
|
|
|
127
|
-
# Build prompt and context from direct lineage via PromptExecution
|
|
128
167
|
last_msg = ordered_messages.last
|
|
129
168
|
pe = last_msg.prompt_navigator_prompt_execution
|
|
130
|
-
|
|
131
169
|
prompt = { role: last_msg.role, prompt: pe.prompt }
|
|
132
170
|
context = pe.build_context(limit: Rails.configuration.summarize_conversation_count)
|
|
133
171
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
172
|
+
summarized_context =
|
|
173
|
+
if context.empty?
|
|
174
|
+
"No context available."
|
|
175
|
+
else
|
|
176
|
+
LlmMetaClient::ServerQuery.new.call(
|
|
177
|
+
jwt_token, prompt_execution.llm_uuid, prompt_execution.model,
|
|
178
|
+
context, "Please summarize the context"
|
|
179
|
+
)
|
|
180
|
+
end
|
|
140
181
|
summarized_context += "Additional prompt: Responses from the assistant must consist solely of the response body."
|
|
182
|
+
if with_tools
|
|
183
|
+
summarized_context += " If a tool call returns an error, do not give up silently β explain the error and what likely caused it (e.g. an invalid argument value)."
|
|
184
|
+
end
|
|
141
185
|
|
|
142
|
-
|
|
143
|
-
LlmMetaClient::ServerQuery.new.call(jwt_token, llm_uuid, model, summarized_context, prompt, tool_ids: tool_ids, generation_settings: generation_settings)
|
|
186
|
+
[ summarized_context, prompt ]
|
|
144
187
|
end
|
|
145
188
|
end
|
data/lib/generators/llm_meta_client/scaffold/templates/app/views/chats/_streaming_message.html.erb
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<%% stream_url = chat_stream_path(
|
|
2
|
+
chat_id: chat.uuid,
|
|
3
|
+
execution_id: prompt_execution.execution_id,
|
|
4
|
+
generation_settings_json: @generation_settings_json.presence,
|
|
5
|
+
tool_ids: (@tool_ids.presence || nil)
|
|
6
|
+
) %>
|
|
7
|
+
<div class="message assistant streaming"
|
|
8
|
+
data-controller="message-stream"
|
|
9
|
+
data-message-stream-url-value="<%%= stream_url %>">
|
|
10
|
+
<div class="message-role">π€ streamingβ¦</div>
|
|
11
|
+
<div class="message-content" data-message-stream-target="content"></div>
|
|
12
|
+
</div>
|
data/lib/generators/llm_meta_client/scaffold/templates/app/views/chats/_tool_call_message.html.erb
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<div class="message assistant tool-call">
|
|
2
|
+
<div class="message-role">π Tool calls</div>
|
|
3
|
+
<div class="message-content">
|
|
4
|
+
<ul>
|
|
5
|
+
<%% tool_calls.each do |tc| %>
|
|
6
|
+
<%% name = tc["name"] || tc[:name] || "(unknown)" %>
|
|
7
|
+
<%% args = tc["arguments"] || tc[:arguments] %>
|
|
8
|
+
<%% args_str = case args
|
|
9
|
+
when Hash, Array then args.to_json
|
|
10
|
+
when nil, "" then nil
|
|
11
|
+
else args.to_s
|
|
12
|
+
end %>
|
|
13
|
+
<li>
|
|
14
|
+
<code><%%= name %></code>
|
|
15
|
+
<%% if args_str %>
|
|
16
|
+
β <code><%%= args_str %></code>
|
|
17
|
+
<%% end %>
|
|
18
|
+
</li>
|
|
19
|
+
<%% end %>
|
|
20
|
+
</ul>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
data/lib/generators/llm_meta_client/scaffold/templates/app/views/chats/create.turbo_stream.erb
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
<%% # User message is already shown by JavaScript on form submit %>
|
|
6
6
|
<%% # Only render assistant message here %>
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
<%% if @
|
|
8
|
+
<%%# Render streaming assistant placeholder; the message-stream Stimulus controller opens an EventSource and appends deltas as they arrive. %>
|
|
9
|
+
<%% if @prompt_execution && @error_message.blank? %>
|
|
10
10
|
<%%= turbo_stream.append "messages-list" do %>
|
|
11
|
-
<%%= render partial: "chats/
|
|
11
|
+
<%%= render partial: "chats/streaming_message", locals: { chat: @chat, prompt_execution: @prompt_execution } %>
|
|
12
12
|
<%% end %>
|
|
13
13
|
<%% end %>
|
|
14
14
|
|
|
@@ -25,9 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
<%% # Update chat sidebar %>
|
|
27
27
|
<%%= turbo_stream.replace "chat-sidebar" do %>
|
|
28
|
-
|
|
29
|
-
<%%= chat_list(->(id) { chat_path(id) }, active_uuid: @chat&.uuid, download_csv_path: ->(id) { download_csv_chat_path(id) }, download_all_csv_path: download_all_csv_chats_path) %>
|
|
30
|
-
</div>
|
|
28
|
+
<%%= render partial: "chats/chat_sidebar", locals: { chat: @chat } %>
|
|
31
29
|
<%% end %>
|
|
32
30
|
|
|
33
31
|
<%% # Update history sidebar - replace entire content to ensure update %>
|
data/lib/generators/llm_meta_client/scaffold/templates/app/views/chats/update.turbo_stream.erb
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
<%% # User message is already shown by JavaScript on form submit %>
|
|
6
6
|
<%% # Only render assistant message here %>
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
<%% if @
|
|
8
|
+
<%%# Render streaming assistant placeholder; the message-stream Stimulus controller opens an EventSource and appends deltas as they arrive. %>
|
|
9
|
+
<%% if @prompt_execution && @error_message.blank? %>
|
|
10
10
|
<%%= turbo_stream.append "messages-list" do %>
|
|
11
|
-
<%%= render partial: "chats/
|
|
11
|
+
<%%= render partial: "chats/streaming_message", locals: { chat: @chat, prompt_execution: @prompt_execution } %>
|
|
12
12
|
<%% end %>
|
|
13
13
|
<%% end %>
|
|
14
14
|
|
|
@@ -1,5 +1,47 @@
|
|
|
1
|
+
require "net/http"
|
|
2
|
+
require "uri"
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
1
5
|
module LlmMetaClient
|
|
2
6
|
class ServerQuery
|
|
7
|
+
# Stream LLM responses incrementally. Yields each content delta event
|
|
8
|
+
# ({ event: "message", data: { "delta" => "..." } }) and any tool_calls
|
|
9
|
+
# event ({ event: "tool_calls", data: { "tool_calls" => [...] } }) to the
|
|
10
|
+
# caller's block. Upstream "done" markers are absorbed (end-of-stream is
|
|
11
|
+
# signaled by the block returning); upstream "error" events raise ServerError.
|
|
12
|
+
# Returns the final assistant content. If tool calls fired, the returned
|
|
13
|
+
# string mirrors the synchronous #call format (response + markdown
|
|
14
|
+
# "Tool calls" section appended) so persistence stays consistent.
|
|
15
|
+
def stream(id_token, api_key_uuid, model_id, context, user_content, tool_ids: [], generation_settings: {})
|
|
16
|
+
context_and_user_content = "Context:#{context}, User Prompt: #{user_content}"
|
|
17
|
+
debug_log "Streaming request to LLM: \n===>\n#{context_and_user_content}\n===>"
|
|
18
|
+
|
|
19
|
+
body = { prompt: context_and_user_content }
|
|
20
|
+
body[:tool_ids] = tool_ids if tool_ids.present?
|
|
21
|
+
body[:generation_settings] = generation_settings if generation_settings.present?
|
|
22
|
+
|
|
23
|
+
assembled = +""
|
|
24
|
+
collected_tool_calls = []
|
|
25
|
+
request_stream(api_key_uuid, id_token, model_id, body) do |event|
|
|
26
|
+
case event[:event]
|
|
27
|
+
when "message"
|
|
28
|
+
assembled << event[:data]["delta"].to_s
|
|
29
|
+
yield event if block_given?
|
|
30
|
+
when "tool_calls"
|
|
31
|
+
collected_tool_calls = event[:data]["tool_calls"] || []
|
|
32
|
+
yield event if block_given?
|
|
33
|
+
when "done"
|
|
34
|
+
# End-of-stream marker from upstream; no-op here.
|
|
35
|
+
when "error"
|
|
36
|
+
raise Exceptions::ServerError, format_stream_error(event[:data])
|
|
37
|
+
else
|
|
38
|
+
yield event if block_given?
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
collected_tool_calls.any? ? combine_with_tool_calls(assembled, collected_tool_calls) : assembled
|
|
43
|
+
end
|
|
44
|
+
|
|
3
45
|
def call(id_token, api_key_uuid, model_id, context, user_content, tool_ids: [], generation_settings: {})
|
|
4
46
|
debug_log "Context: #{context}"
|
|
5
47
|
context_and_user_content = "Context:#{context}, User Prompt: #{user_content}"
|
|
@@ -7,13 +49,17 @@ module LlmMetaClient
|
|
|
7
49
|
|
|
8
50
|
response = request(api_key_uuid, id_token, model_id, context_and_user_content, tool_ids, generation_settings)
|
|
9
51
|
|
|
10
|
-
|
|
52
|
+
unless response.success?
|
|
53
|
+
raise Exceptions::ServerError, build_error_message(response.code.to_i, response.parsed_response)
|
|
54
|
+
end
|
|
11
55
|
|
|
12
56
|
response_body = response.parsed_response
|
|
13
57
|
|
|
14
58
|
raise Exceptions::InvalidResponseError, "LLM server returned non-JSON response" unless response_body.is_a?(Hash)
|
|
15
59
|
|
|
16
60
|
content = response_body.dig("response", "message") || ""
|
|
61
|
+
tool_calls = response_body.dig("response", "tool_calls")
|
|
62
|
+
content = combine_with_tool_calls(content, tool_calls) if tool_calls.is_a?(Array) && tool_calls.any?
|
|
17
63
|
|
|
18
64
|
raise Exceptions::EmptyResponseError, "LLM server returned empty response" if content.blank?
|
|
19
65
|
|
|
@@ -28,6 +74,28 @@ module LlmMetaClient
|
|
|
28
74
|
Rails.logger.info(message) if Rails.env.development?
|
|
29
75
|
end
|
|
30
76
|
|
|
77
|
+
def combine_with_tool_calls(message, tool_calls)
|
|
78
|
+
tool_section = format_tool_calls(tool_calls)
|
|
79
|
+
return tool_section if message.blank?
|
|
80
|
+
"#{message}\n\n---\n\n#{tool_section}"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def format_tool_calls(tool_calls)
|
|
84
|
+
lines = [ "**Tool calls**", "" ]
|
|
85
|
+
tool_calls.each do |tc|
|
|
86
|
+
name = tc["name"] || tc[:name] || "(unknown)"
|
|
87
|
+
args = tc["arguments"] || tc[:arguments]
|
|
88
|
+
args_str =
|
|
89
|
+
case args
|
|
90
|
+
when Hash, Array then args.to_json
|
|
91
|
+
when nil then ""
|
|
92
|
+
else args.to_s
|
|
93
|
+
end
|
|
94
|
+
lines << (args_str.empty? ? "- `#{name}`" : "- `#{name}` β `#{args_str}`")
|
|
95
|
+
end
|
|
96
|
+
lines.join("\n")
|
|
97
|
+
end
|
|
98
|
+
|
|
31
99
|
def request(api_key_uuid, id_token, model_id, user_content, tool_ids, generation_settings)
|
|
32
100
|
headers = { "Content-Type" => "application/json" }
|
|
33
101
|
headers["Authorization"] = "Bearer #{id_token}" if id_token.present?
|
|
@@ -47,5 +115,93 @@ module LlmMetaClient
|
|
|
47
115
|
def url(api_key_uuid, model_id)
|
|
48
116
|
"#{Rails.application.config.llm_service_base_url}/api/llm_api_keys/#{api_key_uuid}/models/#{model_id}/chats"
|
|
49
117
|
end
|
|
118
|
+
|
|
119
|
+
def stream_url(api_key_uuid, model_id)
|
|
120
|
+
"#{Rails.application.config.llm_service_base_url}/api/llm_api_keys/#{api_key_uuid}/models/#{model_id}/chat_streams"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def request_stream(api_key_uuid, id_token, model_id, body)
|
|
124
|
+
uri = URI(stream_url(api_key_uuid, model_id))
|
|
125
|
+
|
|
126
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", read_timeout: 600) do |http|
|
|
127
|
+
req = Net::HTTP::Post.new(uri)
|
|
128
|
+
req["Content-Type"] = "application/json"
|
|
129
|
+
req["Accept"] = "text/event-stream"
|
|
130
|
+
req["Authorization"] = "Bearer #{id_token}" if id_token.present?
|
|
131
|
+
req.body = body.to_json
|
|
132
|
+
|
|
133
|
+
http.request(req) do |response|
|
|
134
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
135
|
+
body = JSON.parse(response.read_body.to_s) rescue nil
|
|
136
|
+
raise Exceptions::ServerError, build_error_message(response.code.to_i, body)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
buffer = +""
|
|
140
|
+
response.read_body do |chunk|
|
|
141
|
+
buffer << chunk
|
|
142
|
+
while (boundary = buffer.index("\n\n"))
|
|
143
|
+
raw_event = buffer.slice!(0, boundary + 2)
|
|
144
|
+
parsed = parse_sse_event(raw_event)
|
|
145
|
+
yield parsed if parsed
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Format an `event: error` SSE payload from llm_meta_server into a
|
|
153
|
+
# user-facing string. Payload shape: { "code" => "rate_limit", "message" => "..." }
|
|
154
|
+
def format_stream_error(data)
|
|
155
|
+
code = data["code"]
|
|
156
|
+
message = data["message"]
|
|
157
|
+
case code
|
|
158
|
+
when "rate_limit"
|
|
159
|
+
suffix = message.present? ? ": #{message}" : ""
|
|
160
|
+
"Rate limit exceeded β check your provider plan or retry shortly#{suffix}"
|
|
161
|
+
when "api_key_required"
|
|
162
|
+
message.presence || "API key required for this model"
|
|
163
|
+
else
|
|
164
|
+
message.presence || "Upstream stream error"
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Turn a non-success HTTP response from llm_meta_server into a user-facing
|
|
169
|
+
# error string. The server returns JSON like
|
|
170
|
+
# { "error" => "LLM API Rate limit exceeded", "message" => "Too many requests" }
|
|
171
|
+
# for known error classes; fall back to a generic message otherwise.
|
|
172
|
+
def build_error_message(status_code, body)
|
|
173
|
+
if body.is_a?(Hash)
|
|
174
|
+
err = body["error"]
|
|
175
|
+
msg = body["message"]
|
|
176
|
+
return "#{err}: #{msg}" if err.present? && msg.present?
|
|
177
|
+
return err if err.present?
|
|
178
|
+
return msg if msg.present?
|
|
179
|
+
end
|
|
180
|
+
case status_code
|
|
181
|
+
when 429 then "Rate limit exceeded β check your provider plan or retry shortly (HTTP 429)"
|
|
182
|
+
when 401, 403 then "LLM service rejected the request (HTTP #{status_code}) β check your API key"
|
|
183
|
+
when 502, 503, 504 then "LLM service is unavailable (HTTP #{status_code})"
|
|
184
|
+
else "LLM server returned HTTP #{status_code}"
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def parse_sse_event(raw)
|
|
189
|
+
event_name = "message"
|
|
190
|
+
data_lines = []
|
|
191
|
+
raw.each_line(chomp: true) do |line|
|
|
192
|
+
next if line.empty?
|
|
193
|
+
if line.start_with?("event:")
|
|
194
|
+
event_name = line.sub(/^event:\s*/, "")
|
|
195
|
+
elsif line.start_with?("data:")
|
|
196
|
+
data_lines << line.sub(/^data:\s*/, "")
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
return nil if data_lines.empty?
|
|
200
|
+
|
|
201
|
+
data = JSON.parse(data_lines.join("\n"))
|
|
202
|
+
{ event: event_name, data: data }
|
|
203
|
+
rescue JSON::ParserError
|
|
204
|
+
nil
|
|
205
|
+
end
|
|
50
206
|
end
|
|
51
207
|
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: 1.0
|
|
4
|
+
version: 1.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- dhq_boiler
|
|
@@ -47,16 +47,22 @@ dependencies:
|
|
|
47
47
|
name: prompt_navigator
|
|
48
48
|
requirement: !ruby/object:Gem::Requirement
|
|
49
49
|
requirements:
|
|
50
|
-
- - "
|
|
50
|
+
- - ">="
|
|
51
51
|
- !ruby/object:Gem::Version
|
|
52
52
|
version: '1.0'
|
|
53
|
+
- - "<"
|
|
54
|
+
- !ruby/object:Gem::Version
|
|
55
|
+
version: '3.0'
|
|
53
56
|
type: :runtime
|
|
54
57
|
prerelease: false
|
|
55
58
|
version_requirements: !ruby/object:Gem::Requirement
|
|
56
59
|
requirements:
|
|
57
|
-
- - "
|
|
60
|
+
- - ">="
|
|
58
61
|
- !ruby/object:Gem::Version
|
|
59
62
|
version: '1.0'
|
|
63
|
+
- - "<"
|
|
64
|
+
- !ruby/object:Gem::Version
|
|
65
|
+
version: '3.0'
|
|
60
66
|
- !ruby/object:Gem::Dependency
|
|
61
67
|
name: chat_manager
|
|
62
68
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -103,18 +109,23 @@ files:
|
|
|
103
109
|
- lib/generators/llm_meta_client/authentication/templates/db/migrate/create_users.rb
|
|
104
110
|
- lib/generators/llm_meta_client/scaffold/scaffold_generator.rb
|
|
105
111
|
- lib/generators/llm_meta_client/scaffold/templates/app/controllers/api/mcp_servers_controller.rb
|
|
112
|
+
- lib/generators/llm_meta_client/scaffold/templates/app/controllers/chat_streams_controller.rb
|
|
106
113
|
- lib/generators/llm_meta_client/scaffold/templates/app/controllers/chats_controller.rb
|
|
107
114
|
- lib/generators/llm_meta_client/scaffold/templates/app/controllers/prompts_controller.rb
|
|
108
115
|
- lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/chat_title_edit_controller.js
|
|
109
116
|
- lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/chats_form_controller.js
|
|
110
117
|
- lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/generation_settings_controller.js
|
|
111
118
|
- lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/llm_selector_controller.js
|
|
119
|
+
- lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/message_stream_controller.js
|
|
112
120
|
- lib/generators/llm_meta_client/scaffold/templates/app/javascript/controllers/tool_selector_controller.js
|
|
113
121
|
- lib/generators/llm_meta_client/scaffold/templates/app/javascript/popover.js
|
|
114
122
|
- lib/generators/llm_meta_client/scaffold/templates/app/models/chat.rb
|
|
115
123
|
- lib/generators/llm_meta_client/scaffold/templates/app/models/message.rb
|
|
124
|
+
- lib/generators/llm_meta_client/scaffold/templates/app/views/chats/_chat_sidebar.html.erb
|
|
116
125
|
- lib/generators/llm_meta_client/scaffold/templates/app/views/chats/_message.html.erb
|
|
117
126
|
- lib/generators/llm_meta_client/scaffold/templates/app/views/chats/_messages_list.html.erb
|
|
127
|
+
- lib/generators/llm_meta_client/scaffold/templates/app/views/chats/_streaming_message.html.erb
|
|
128
|
+
- lib/generators/llm_meta_client/scaffold/templates/app/views/chats/_tool_call_message.html.erb
|
|
118
129
|
- lib/generators/llm_meta_client/scaffold/templates/app/views/chats/create.turbo_stream.erb
|
|
119
130
|
- lib/generators/llm_meta_client/scaffold/templates/app/views/chats/edit.html.erb
|
|
120
131
|
- lib/generators/llm_meta_client/scaffold/templates/app/views/chats/new.html.erb
|
|
@@ -163,7 +174,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
163
174
|
- !ruby/object:Gem::Version
|
|
164
175
|
version: '0'
|
|
165
176
|
requirements: []
|
|
166
|
-
rubygems_version:
|
|
177
|
+
rubygems_version: 3.6.9
|
|
167
178
|
specification_version: 4
|
|
168
179
|
summary: A Rails Engine for integrating multiple LLM providers into your application.
|
|
169
180
|
test_files: []
|