dify_llm 1.9.1 → 1.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +27 -8
- data/lib/generators/ruby_llm/agent/agent_generator.rb +36 -0
- data/lib/generators/ruby_llm/agent/templates/agent.rb.tt +6 -0
- data/lib/generators/ruby_llm/agent/templates/instructions.txt.erb.tt +0 -0
- data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +110 -41
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +14 -15
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +8 -11
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/helpers/messages_helper.rb.tt +25 -0
- data/lib/generators/ruby_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_chat.html.erb.tt +16 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +31 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/index.html.erb.tt +31 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/new.html.erb.tt +9 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +27 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +14 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_content.html.erb.tt +1 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_error.html.erb.tt +13 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_form.html.erb.tt +23 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +10 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool.html.erb.tt +2 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +4 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +14 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +13 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_results/_default.html.erb.tt +21 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/_model.html.erb.tt +17 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/index.html.erb.tt +40 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/show.html.erb.tt +27 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/index.html.erb.tt +19 -7
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/new.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +5 -3
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +9 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_content.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_error.html.erb.tt +8 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_form.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_system.html.erb.tt +6 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool.html.erb.tt +2 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +4 -7
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_user.html.erb.tt +9 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +5 -7
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +8 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_results/_default.html.erb.tt +16 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/_model.html.erb.tt +11 -12
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/index.html.erb.tt +27 -17
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/show.html.erb.tt +3 -4
- data/lib/generators/ruby_llm/generator_helpers.rb +37 -17
- data/lib/generators/ruby_llm/install/install_generator.rb +22 -18
- data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +1 -1
- data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +4 -1
- data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +4 -10
- data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +2 -1
- data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +2 -2
- data/lib/generators/ruby_llm/schema/schema_generator.rb +26 -0
- data/lib/generators/ruby_llm/schema/templates/schema.rb.tt +2 -0
- data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +9 -0
- data/lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt +13 -0
- data/lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt +13 -0
- data/lib/generators/ruby_llm/tool/tool_generator.rb +96 -0
- data/lib/generators/ruby_llm/upgrade_to_v1_10/templates/add_v1_10_message_columns.rb.tt +19 -0
- data/lib/generators/ruby_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +50 -0
- data/lib/generators/ruby_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +7 -0
- data/lib/generators/ruby_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +49 -0
- data/lib/generators/ruby_llm/upgrade_to_v1_7/templates/migration.rb.tt +1 -1
- data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +2 -4
- data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +1 -1
- data/lib/ruby_llm/active_record/acts_as.rb +10 -4
- data/lib/ruby_llm/active_record/acts_as_legacy.rb +132 -27
- data/lib/ruby_llm/active_record/chat_methods.rb +132 -28
- data/lib/ruby_llm/active_record/message_methods.rb +58 -8
- data/lib/ruby_llm/active_record/model_methods.rb +1 -1
- data/lib/ruby_llm/active_record/payload_helpers.rb +26 -0
- data/lib/ruby_llm/active_record/tool_call_methods.rb +15 -0
- data/lib/ruby_llm/agent.rb +365 -0
- data/lib/ruby_llm/aliases.json +199 -62
- data/lib/ruby_llm/attachment.rb +15 -4
- data/lib/ruby_llm/chat.rb +150 -22
- data/lib/ruby_llm/configuration.rb +65 -65
- data/lib/ruby_llm/connection.rb +11 -7
- data/lib/ruby_llm/content.rb +6 -2
- data/lib/ruby_llm/error.rb +37 -1
- data/lib/ruby_llm/message.rb +43 -15
- data/lib/ruby_llm/model/info.rb +15 -13
- data/lib/ruby_llm/models.json +37560 -14094
- data/lib/ruby_llm/models.rb +321 -38
- data/lib/ruby_llm/models_schema.json +2 -2
- data/lib/ruby_llm/provider.rb +26 -4
- data/lib/ruby_llm/providers/anthropic/capabilities.rb +5 -119
- data/lib/ruby_llm/providers/anthropic/chat.rb +149 -17
- data/lib/ruby_llm/providers/anthropic/media.rb +2 -2
- data/lib/ruby_llm/providers/anthropic/models.rb +3 -9
- data/lib/ruby_llm/providers/anthropic/streaming.rb +25 -1
- data/lib/ruby_llm/providers/anthropic/tools.rb +20 -0
- data/lib/ruby_llm/providers/anthropic.rb +5 -1
- data/lib/ruby_llm/providers/azure/chat.rb +29 -0
- data/lib/ruby_llm/providers/azure/embeddings.rb +24 -0
- data/lib/ruby_llm/providers/azure/media.rb +45 -0
- data/lib/ruby_llm/providers/azure/models.rb +14 -0
- data/lib/ruby_llm/providers/azure.rb +148 -0
- data/lib/ruby_llm/providers/bedrock/auth.rb +122 -0
- data/lib/ruby_llm/providers/bedrock/chat.rb +357 -28
- data/lib/ruby_llm/providers/bedrock/media.rb +62 -33
- data/lib/ruby_llm/providers/bedrock/models.rb +107 -62
- data/lib/ruby_llm/providers/bedrock/streaming.rb +309 -8
- data/lib/ruby_llm/providers/bedrock.rb +69 -52
- data/lib/ruby_llm/providers/deepseek/capabilities.rb +4 -114
- data/lib/ruby_llm/providers/deepseek.rb +5 -1
- data/lib/ruby_llm/providers/dify/chat.rb +82 -7
- data/lib/ruby_llm/providers/dify/media.rb +2 -2
- data/lib/ruby_llm/providers/dify/streaming.rb +26 -4
- data/lib/ruby_llm/providers/dify.rb +4 -0
- data/lib/ruby_llm/providers/gemini/capabilities.rb +45 -207
- data/lib/ruby_llm/providers/gemini/chat.rb +88 -6
- data/lib/ruby_llm/providers/gemini/images.rb +1 -1
- data/lib/ruby_llm/providers/gemini/models.rb +2 -4
- data/lib/ruby_llm/providers/gemini/streaming.rb +34 -2
- data/lib/ruby_llm/providers/gemini/tools.rb +35 -3
- data/lib/ruby_llm/providers/gemini.rb +4 -0
- data/lib/ruby_llm/providers/gpustack/capabilities.rb +20 -0
- data/lib/ruby_llm/providers/gpustack/chat.rb +1 -1
- data/lib/ruby_llm/providers/gpustack.rb +8 -0
- data/lib/ruby_llm/providers/mistral/capabilities.rb +8 -0
- data/lib/ruby_llm/providers/mistral/chat.rb +59 -1
- data/lib/ruby_llm/providers/mistral.rb +4 -0
- data/lib/ruby_llm/providers/ollama/capabilities.rb +20 -0
- data/lib/ruby_llm/providers/ollama/chat.rb +1 -1
- data/lib/ruby_llm/providers/ollama.rb +11 -1
- data/lib/ruby_llm/providers/openai/capabilities.rb +96 -192
- data/lib/ruby_llm/providers/openai/chat.rb +101 -7
- data/lib/ruby_llm/providers/openai/media.rb +5 -2
- data/lib/ruby_llm/providers/openai/models.rb +2 -4
- data/lib/ruby_llm/providers/openai/streaming.rb +11 -3
- data/lib/ruby_llm/providers/openai/temperature.rb +28 -0
- data/lib/ruby_llm/providers/openai/tools.rb +27 -2
- data/lib/ruby_llm/providers/openai.rb +11 -1
- data/lib/ruby_llm/providers/openrouter/chat.rb +168 -0
- data/lib/ruby_llm/providers/openrouter/images.rb +69 -0
- data/lib/ruby_llm/providers/openrouter/streaming.rb +74 -0
- data/lib/ruby_llm/providers/openrouter.rb +37 -1
- data/lib/ruby_llm/providers/perplexity/capabilities.rb +34 -99
- data/lib/ruby_llm/providers/perplexity/models.rb +12 -14
- data/lib/ruby_llm/providers/perplexity.rb +4 -0
- data/lib/ruby_llm/providers/vertexai/models.rb +1 -1
- data/lib/ruby_llm/providers/vertexai.rb +23 -7
- data/lib/ruby_llm/providers/xai/chat.rb +15 -0
- data/lib/ruby_llm/providers/xai/models.rb +75 -0
- data/lib/ruby_llm/providers/xai.rb +32 -0
- data/lib/ruby_llm/stream_accumulator.rb +120 -18
- data/lib/ruby_llm/streaming.rb +82 -60
- data/lib/ruby_llm/thinking.rb +49 -0
- data/lib/ruby_llm/tokens.rb +47 -0
- data/lib/ruby_llm/tool.rb +49 -4
- data/lib/ruby_llm/tool_call.rb +6 -3
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/ruby_llm.rb +14 -8
- data/lib/tasks/models.rake +62 -23
- data/lib/tasks/release.rake +1 -1
- data/lib/tasks/ruby_llm.rake +9 -1
- data/lib/tasks/vcr.rake +33 -1
- metadata +67 -16
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_message.html.erb.tt +0 -13
- data/lib/ruby_llm/providers/bedrock/capabilities.rb +0 -167
- data/lib/ruby_llm/providers/bedrock/signing.rb +0 -831
- data/lib/ruby_llm/providers/bedrock/streaming/base.rb +0 -51
- data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +0 -71
- data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +0 -67
- data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +0 -80
- data/lib/ruby_llm/providers/bedrock/streaming/prelude_handling.rb +0 -78
|
@@ -18,8 +18,6 @@ module RubyLLM
|
|
|
18
18
|
class_name: @message_class,
|
|
19
19
|
inverse_of: :chat,
|
|
20
20
|
dependent: :destroy
|
|
21
|
-
|
|
22
|
-
delegate :add_message, to: :to_llm
|
|
23
21
|
end
|
|
24
22
|
|
|
25
23
|
def acts_as_message(chat_class: 'Chat',
|
|
@@ -60,6 +58,8 @@ module RubyLLM
|
|
|
60
58
|
end
|
|
61
59
|
|
|
62
60
|
def acts_as_tool_call(message_class: 'Message', message_foreign_key: nil, result_foreign_key: nil)
|
|
61
|
+
include RubyLLM::ActiveRecord::ToolCallMethods
|
|
62
|
+
|
|
63
63
|
@message_class = message_class.to_s
|
|
64
64
|
@message_foreign_key = message_foreign_key || ActiveSupport::Inflector.foreign_key(@message_class)
|
|
65
65
|
@result_foreign_key = result_foreign_key || ActiveSupport::Inflector.foreign_key(name)
|
|
@@ -95,19 +95,28 @@ module RubyLLM
|
|
|
95
95
|
end
|
|
96
96
|
@chat.reset_messages!
|
|
97
97
|
|
|
98
|
-
messages.
|
|
98
|
+
ordered_messages = order_messages_for_llm(messages.to_a)
|
|
99
|
+
ordered_messages.each do |msg|
|
|
99
100
|
@chat.add_message(msg.to_llm)
|
|
100
101
|
end
|
|
102
|
+
reapply_runtime_instructions(@chat)
|
|
101
103
|
|
|
102
104
|
setup_persistence_callbacks
|
|
103
105
|
end
|
|
104
106
|
|
|
105
|
-
def with_instructions(instructions, replace:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
107
|
+
def with_instructions(instructions, append: false, replace: nil)
|
|
108
|
+
append = append_instructions?(append:, replace:)
|
|
109
|
+
persist_system_instruction(instructions, append:)
|
|
110
|
+
|
|
111
|
+
to_llm.with_instructions(instructions, append:, replace:)
|
|
112
|
+
self
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def with_runtime_instructions(instructions, append: false, replace: nil)
|
|
116
|
+
append = append_instructions?(append:, replace:)
|
|
117
|
+
store_runtime_instruction(instructions, append:)
|
|
118
|
+
|
|
119
|
+
to_llm.with_instructions(instructions, append:, replace:)
|
|
111
120
|
self
|
|
112
121
|
end
|
|
113
122
|
|
|
@@ -185,14 +194,34 @@ module RubyLLM
|
|
|
185
194
|
self
|
|
186
195
|
end
|
|
187
196
|
|
|
188
|
-
def
|
|
189
|
-
|
|
190
|
-
|
|
197
|
+
def add_message(message_or_attributes)
|
|
198
|
+
llm_message = message_or_attributes.is_a?(RubyLLM::Message) ? message_or_attributes : RubyLLM::Message.new(message_or_attributes)
|
|
199
|
+
content, attachments_to_persist = prepare_content_for_storage(llm_message.content)
|
|
200
|
+
|
|
201
|
+
attrs = { role: llm_message.role, content: }
|
|
202
|
+
tool_call_foreign_key = messages.klass.tool_call_foreign_key
|
|
203
|
+
if llm_message.tool_call_id && tool_call_foreign_key
|
|
204
|
+
tool_call_id = find_tool_call_id(llm_message.tool_call_id)
|
|
205
|
+
attrs[tool_call_foreign_key] = tool_call_id if tool_call_id
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
message_record = messages.create!(attrs)
|
|
209
|
+
persist_content(message_record, attachments_to_persist) if attachments_to_persist.present?
|
|
210
|
+
persist_tool_calls(llm_message.tool_calls, message_record:) if llm_message.tool_calls.present?
|
|
211
|
+
|
|
191
212
|
message_record
|
|
192
213
|
end
|
|
193
214
|
|
|
194
|
-
def
|
|
195
|
-
|
|
215
|
+
def create_user_message(content, with: nil)
|
|
216
|
+
RubyLLM.logger.warn(
|
|
217
|
+
'`create_user_message` is deprecated and will be removed in RubyLLM 2.0. ' \
|
|
218
|
+
'Use `add_message(role: :user, content: ...)` instead.'
|
|
219
|
+
)
|
|
220
|
+
add_message(role: :user, content: build_content(content, with))
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def ask(message = nil, with: nil, &)
|
|
224
|
+
add_message(role: :user, content: build_content(message, with))
|
|
196
225
|
complete(&)
|
|
197
226
|
end
|
|
198
227
|
|
|
@@ -233,6 +262,60 @@ module RubyLLM
|
|
|
233
262
|
end
|
|
234
263
|
end
|
|
235
264
|
|
|
265
|
+
def replace_persisted_system_instructions(instructions)
|
|
266
|
+
system_messages = messages.where(role: :system).order(:id).to_a
|
|
267
|
+
|
|
268
|
+
if system_messages.empty?
|
|
269
|
+
messages.create!(role: :system, content: instructions)
|
|
270
|
+
return
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
primary_message = system_messages.shift
|
|
274
|
+
primary_message.update!(content: instructions) if primary_message.content != instructions
|
|
275
|
+
system_messages.each(&:destroy!)
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def append_instructions?(append:, replace:)
|
|
279
|
+
return append if replace.nil?
|
|
280
|
+
|
|
281
|
+
append || (replace == false)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def persist_system_instruction(instructions, append:)
|
|
285
|
+
transaction do
|
|
286
|
+
if append
|
|
287
|
+
messages.create!(role: :system, content: instructions)
|
|
288
|
+
else
|
|
289
|
+
replace_persisted_system_instructions(instructions)
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def order_messages_for_llm(messages)
|
|
295
|
+
system_messages, non_system_messages = messages.partition { |msg| msg.role.to_s == 'system' }
|
|
296
|
+
system_messages + non_system_messages
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def runtime_instructions
|
|
300
|
+
@runtime_instructions ||= []
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def store_runtime_instruction(instructions, append:)
|
|
304
|
+
if append
|
|
305
|
+
runtime_instructions << instructions
|
|
306
|
+
else
|
|
307
|
+
@runtime_instructions = [instructions]
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def reapply_runtime_instructions(chat)
|
|
312
|
+
return if runtime_instructions.empty?
|
|
313
|
+
|
|
314
|
+
first, *rest = runtime_instructions
|
|
315
|
+
chat.with_instructions(first)
|
|
316
|
+
rest.each { |instruction| chat.with_instructions(instruction, append: true) }
|
|
317
|
+
end
|
|
318
|
+
|
|
236
319
|
def setup_persistence_callbacks
|
|
237
320
|
return @chat if @chat.instance_variable_get(:@_persistence_callbacks_setup)
|
|
238
321
|
|
|
@@ -247,21 +330,13 @@ module RubyLLM
|
|
|
247
330
|
@message = messages.create!(role: :assistant, content: '')
|
|
248
331
|
end
|
|
249
332
|
|
|
250
|
-
def persist_message_completion(message)
|
|
333
|
+
def persist_message_completion(message)
|
|
251
334
|
return unless message
|
|
252
335
|
|
|
253
336
|
tool_call_id = find_tool_call_id(message.tool_call_id) if message.tool_call_id
|
|
254
337
|
|
|
255
338
|
transaction do
|
|
256
|
-
content = message.content
|
|
257
|
-
attachments_to_persist = nil
|
|
258
|
-
|
|
259
|
-
if content.is_a?(RubyLLM::Content)
|
|
260
|
-
attachments_to_persist = content.attachments if content.attachments.any?
|
|
261
|
-
content = content.text
|
|
262
|
-
elsif content.is_a?(Hash) || content.is_a?(Array)
|
|
263
|
-
content = content.to_json
|
|
264
|
-
end
|
|
339
|
+
content, attachments_to_persist = prepare_content_for_storage(message.content)
|
|
265
340
|
|
|
266
341
|
@message.update!(
|
|
267
342
|
role: message.role,
|
|
@@ -278,11 +353,17 @@ module RubyLLM
|
|
|
278
353
|
end
|
|
279
354
|
end
|
|
280
355
|
|
|
281
|
-
def persist_tool_calls(tool_calls)
|
|
356
|
+
def persist_tool_calls(tool_calls, message_record: @message)
|
|
357
|
+
tool_call_assoc = message_record.respond_to?(:tool_calls) ? message_record.tool_calls : nil
|
|
358
|
+
return unless tool_call_assoc
|
|
359
|
+
|
|
360
|
+
supports_thought_signature = tool_call_assoc.klass.column_names.include?('thought_signature')
|
|
361
|
+
|
|
282
362
|
tool_calls.each_value do |tool_call|
|
|
283
363
|
attributes = tool_call.to_h
|
|
364
|
+
attributes.delete(:thought_signature) unless supports_thought_signature
|
|
284
365
|
attributes[:tool_call_id] = attributes.delete(:id)
|
|
285
|
-
|
|
366
|
+
tool_call_assoc.create!(**attributes)
|
|
286
367
|
end
|
|
287
368
|
end
|
|
288
369
|
|
|
@@ -326,6 +407,29 @@ module RubyLLM
|
|
|
326
407
|
RubyLLM.logger.warn "Failed to process attachment #{source}: #{e.message}"
|
|
327
408
|
nil
|
|
328
409
|
end
|
|
410
|
+
|
|
411
|
+
def build_content(message, attachments)
|
|
412
|
+
return message if content_like?(message)
|
|
413
|
+
|
|
414
|
+
RubyLLM::Content.new(message, attachments)
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def content_like?(object)
|
|
418
|
+
object.is_a?(RubyLLM::Content) || object.is_a?(RubyLLM::Content::Raw)
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def prepare_content_for_storage(content)
|
|
422
|
+
attachments = nil
|
|
423
|
+
|
|
424
|
+
if content.is_a?(RubyLLM::Content)
|
|
425
|
+
attachments = content.attachments if content.attachments.any?
|
|
426
|
+
[content.text, attachments]
|
|
427
|
+
elsif content.is_a?(Hash) || content.is_a?(Array)
|
|
428
|
+
[content.to_json, attachments]
|
|
429
|
+
else
|
|
430
|
+
[content, attachments]
|
|
431
|
+
end
|
|
432
|
+
end
|
|
329
433
|
end
|
|
330
434
|
|
|
331
435
|
# Methods mixed into message models.
|
|
@@ -357,7 +461,8 @@ module RubyLLM
|
|
|
357
461
|
RubyLLM::ToolCall.new(
|
|
358
462
|
id: tool_call.tool_call_id,
|
|
359
463
|
name: tool_call.name,
|
|
360
|
-
arguments: tool_call.arguments
|
|
464
|
+
arguments: tool_call.arguments,
|
|
465
|
+
thought_signature: tool_call.try(:thought_signature)
|
|
361
466
|
)
|
|
362
467
|
]
|
|
363
468
|
end
|
|
@@ -79,23 +79,33 @@ module RubyLLM
|
|
|
79
79
|
model_record = model_association
|
|
80
80
|
@chat ||= (context || RubyLLM).chat(
|
|
81
81
|
model: model_record.model_id,
|
|
82
|
-
provider: model_record.provider.to_sym
|
|
82
|
+
provider: model_record.provider.to_sym,
|
|
83
|
+
assume_model_exists: assume_model_exists || false
|
|
83
84
|
)
|
|
84
85
|
@chat.reset_messages!
|
|
85
86
|
|
|
86
|
-
messages_association.
|
|
87
|
+
ordered_messages = order_messages_for_llm(messages_association.to_a)
|
|
88
|
+
ordered_messages.each do |msg|
|
|
87
89
|
@chat.add_message(msg.to_llm)
|
|
88
90
|
end
|
|
91
|
+
reapply_runtime_instructions(@chat)
|
|
89
92
|
|
|
90
93
|
setup_persistence_callbacks
|
|
91
94
|
end
|
|
92
95
|
|
|
93
|
-
def with_instructions(instructions, replace:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
96
|
+
def with_instructions(instructions, append: false, replace: nil)
|
|
97
|
+
append = append_instructions?(append:, replace:)
|
|
98
|
+
persist_system_instruction(instructions, append:)
|
|
99
|
+
|
|
100
|
+
to_llm.with_instructions(instructions, append:, replace:)
|
|
101
|
+
self
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def with_runtime_instructions(instructions, append: false, replace: nil)
|
|
105
|
+
append = append_instructions?(append:, replace:)
|
|
106
|
+
store_runtime_instruction(instructions, append:)
|
|
107
|
+
|
|
108
|
+
to_llm.with_instructions(instructions, append:, replace:)
|
|
99
109
|
self
|
|
100
110
|
end
|
|
101
111
|
|
|
@@ -115,7 +125,7 @@ module RubyLLM
|
|
|
115
125
|
self.assume_model_exists = assume_exists
|
|
116
126
|
resolve_model_from_strings
|
|
117
127
|
save!
|
|
118
|
-
to_llm.with_model(
|
|
128
|
+
to_llm.with_model(model_association.model_id, provider: model_association.provider.to_sym, assume_exists:)
|
|
119
129
|
self
|
|
120
130
|
end
|
|
121
131
|
|
|
@@ -124,6 +134,11 @@ module RubyLLM
|
|
|
124
134
|
self
|
|
125
135
|
end
|
|
126
136
|
|
|
137
|
+
def with_thinking(...)
|
|
138
|
+
to_llm.with_thinking(...)
|
|
139
|
+
self
|
|
140
|
+
end
|
|
141
|
+
|
|
127
142
|
def with_params(...)
|
|
128
143
|
to_llm.with_params(...)
|
|
129
144
|
self
|
|
@@ -173,22 +188,32 @@ module RubyLLM
|
|
|
173
188
|
self
|
|
174
189
|
end
|
|
175
190
|
|
|
176
|
-
def
|
|
177
|
-
|
|
191
|
+
def add_message(message_or_attributes)
|
|
192
|
+
llm_message = message_or_attributes.is_a?(RubyLLM::Message) ? message_or_attributes : RubyLLM::Message.new(message_or_attributes)
|
|
193
|
+
content_text, attachments, content_raw = prepare_content_for_storage(llm_message.content)
|
|
194
|
+
|
|
195
|
+
attrs = { role: llm_message.role, content: content_text }
|
|
196
|
+
parent_tool_call_assoc = messages_association.klass.reflect_on_association(:parent_tool_call)
|
|
197
|
+
if parent_tool_call_assoc && llm_message.tool_call_id
|
|
198
|
+
tool_call_id = find_tool_call_id(llm_message.tool_call_id)
|
|
199
|
+
attrs[parent_tool_call_assoc.foreign_key] = tool_call_id if tool_call_id
|
|
200
|
+
end
|
|
178
201
|
|
|
179
|
-
message_record = messages_association.
|
|
180
|
-
message_record.
|
|
181
|
-
message_record.content_raw = content_raw if message_record.respond_to?(:content_raw=)
|
|
182
|
-
message_record.save!
|
|
202
|
+
message_record = messages_association.create!(attrs)
|
|
203
|
+
message_record.update!(content_raw:) if message_record.respond_to?(:content_raw=)
|
|
183
204
|
|
|
184
|
-
persist_content(message_record, with) if with.present?
|
|
185
205
|
persist_content(message_record, attachments) if attachments.present?
|
|
206
|
+
persist_tool_calls(llm_message.tool_calls, message_record:) if llm_message.tool_calls.present?
|
|
186
207
|
|
|
187
208
|
message_record
|
|
188
209
|
end
|
|
189
210
|
|
|
190
|
-
def
|
|
191
|
-
|
|
211
|
+
def create_user_message(content, with: nil)
|
|
212
|
+
add_message(role: :user, content: build_content(content, with))
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def ask(message = nil, with: nil, &)
|
|
216
|
+
add_message(role: :user, content: build_content(message, with))
|
|
192
217
|
complete(&)
|
|
193
218
|
end
|
|
194
219
|
|
|
@@ -218,9 +243,10 @@ module RubyLLM
|
|
|
218
243
|
if last.tool_call?
|
|
219
244
|
last.destroy
|
|
220
245
|
elsif last.tool_result?
|
|
221
|
-
tool_call_message = last.parent_tool_call.
|
|
222
|
-
expected_results = tool_call_message.
|
|
223
|
-
|
|
246
|
+
tool_call_message = last.parent_tool_call.message_association
|
|
247
|
+
expected_results = tool_call_message.tool_calls_association.pluck(:id)
|
|
248
|
+
fk_column = tool_call_message.class.reflections['tool_results'].foreign_key
|
|
249
|
+
actual_results = tool_call_message.tool_results.pluck(fk_column)
|
|
224
250
|
|
|
225
251
|
if expected_results.sort != actual_results.sort
|
|
226
252
|
tool_call_message.tool_results.each(&:destroy)
|
|
@@ -239,6 +265,60 @@ module RubyLLM
|
|
|
239
265
|
@chat
|
|
240
266
|
end
|
|
241
267
|
|
|
268
|
+
def replace_persisted_system_instructions(instructions)
|
|
269
|
+
system_messages = messages_association.where(role: :system).order(:id).to_a
|
|
270
|
+
|
|
271
|
+
if system_messages.empty?
|
|
272
|
+
messages_association.create!(role: :system, content: instructions)
|
|
273
|
+
return
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
primary_message = system_messages.shift
|
|
277
|
+
primary_message.update!(content: instructions) if primary_message.content != instructions
|
|
278
|
+
system_messages.each(&:destroy!)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def append_instructions?(append:, replace:)
|
|
282
|
+
return append if replace.nil?
|
|
283
|
+
|
|
284
|
+
append || (replace == false)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def persist_system_instruction(instructions, append:)
|
|
288
|
+
transaction do
|
|
289
|
+
if append
|
|
290
|
+
messages_association.create!(role: :system, content: instructions)
|
|
291
|
+
else
|
|
292
|
+
replace_persisted_system_instructions(instructions)
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def order_messages_for_llm(messages)
|
|
298
|
+
system_messages, non_system_messages = messages.partition { |msg| msg.role.to_s == 'system' }
|
|
299
|
+
system_messages + non_system_messages
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def runtime_instructions
|
|
303
|
+
@runtime_instructions ||= []
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def store_runtime_instruction(instructions, append:)
|
|
307
|
+
if append
|
|
308
|
+
runtime_instructions << instructions
|
|
309
|
+
else
|
|
310
|
+
@runtime_instructions = [instructions]
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def reapply_runtime_instructions(chat)
|
|
315
|
+
return if runtime_instructions.empty?
|
|
316
|
+
|
|
317
|
+
first, *rest = runtime_instructions
|
|
318
|
+
chat.with_instructions(first)
|
|
319
|
+
rest.each { |instruction| chat.with_instructions(instruction, append: true) }
|
|
320
|
+
end
|
|
321
|
+
|
|
242
322
|
def persist_new_message
|
|
243
323
|
@message = messages_association.create!(role: :assistant, content: '')
|
|
244
324
|
end
|
|
@@ -262,6 +342,9 @@ module RubyLLM
|
|
|
262
342
|
if @message.has_attribute?(:cache_creation_tokens)
|
|
263
343
|
attrs[:cache_creation_tokens] = message.cache_creation_tokens
|
|
264
344
|
end
|
|
345
|
+
attrs[:thinking_text] = message.thinking&.text if @message.has_attribute?(:thinking_text)
|
|
346
|
+
attrs[:thinking_signature] = message.thinking&.signature if @message.has_attribute?(:thinking_signature)
|
|
347
|
+
attrs[:thinking_tokens] = message.thinking_tokens if @message.has_attribute?(:thinking_tokens)
|
|
265
348
|
|
|
266
349
|
# Add model association dynamically
|
|
267
350
|
attrs[self.class.model_association_name] = model_association
|
|
@@ -281,11 +364,15 @@ module RubyLLM
|
|
|
281
364
|
end
|
|
282
365
|
# rubocop:enable Metrics/PerceivedComplexity
|
|
283
366
|
|
|
284
|
-
def persist_tool_calls(tool_calls)
|
|
367
|
+
def persist_tool_calls(tool_calls, message_record: @message)
|
|
368
|
+
tool_call_klass = message_record.tool_calls_association.klass
|
|
369
|
+
supports_thought_signature = tool_call_klass.column_names.include?('thought_signature')
|
|
370
|
+
|
|
285
371
|
tool_calls.each_value do |tool_call|
|
|
286
372
|
attributes = tool_call.to_h
|
|
373
|
+
attributes.delete(:thought_signature) unless supports_thought_signature
|
|
287
374
|
attributes[:tool_call_id] = attributes.delete(:id)
|
|
288
|
-
|
|
375
|
+
message_record.tool_calls_association.create!(**attributes)
|
|
289
376
|
end
|
|
290
377
|
end
|
|
291
378
|
|
|
@@ -330,16 +417,33 @@ module RubyLLM
|
|
|
330
417
|
|
|
331
418
|
attachment = source.is_a?(RubyLLM::Attachment) ? source : RubyLLM::Attachment.new(source)
|
|
332
419
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
420
|
+
if attachment.active_storage?
|
|
421
|
+
case attachment.source
|
|
422
|
+
when ActiveStorage::Blob then attachment.source
|
|
423
|
+
when ActiveStorage::Attached::One, ActiveStorage::Attached::Many then attachment.source.blobs
|
|
424
|
+
end
|
|
425
|
+
else
|
|
426
|
+
{
|
|
427
|
+
io: StringIO.new(attachment.content),
|
|
428
|
+
filename: attachment.filename,
|
|
429
|
+
content_type: attachment.mime_type
|
|
430
|
+
}
|
|
431
|
+
end
|
|
338
432
|
rescue StandardError => e
|
|
339
433
|
RubyLLM.logger.warn "Failed to process attachment #{source}: #{e.message}"
|
|
340
434
|
nil
|
|
341
435
|
end
|
|
342
436
|
|
|
437
|
+
def build_content(message, attachments)
|
|
438
|
+
return message if content_like?(message)
|
|
439
|
+
|
|
440
|
+
RubyLLM::Content.new(message, attachments)
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
def content_like?(object)
|
|
444
|
+
object.is_a?(RubyLLM::Content) || object.is_a?(RubyLLM::Content::Raw)
|
|
445
|
+
end
|
|
446
|
+
|
|
343
447
|
def prepare_content_for_storage(content)
|
|
344
448
|
attachments = nil
|
|
345
449
|
content_raw = nil
|
|
@@ -5,30 +5,79 @@ module RubyLLM
|
|
|
5
5
|
# Methods mixed into message models.
|
|
6
6
|
module MessageMethods
|
|
7
7
|
extend ActiveSupport::Concern
|
|
8
|
+
include PayloadHelpers
|
|
8
9
|
|
|
9
10
|
class_methods do
|
|
10
11
|
attr_reader :chat_class, :tool_call_class, :chat_foreign_key, :tool_call_foreign_key
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
def to_llm
|
|
14
|
-
cached = has_attribute?(:cached_tokens) ? self[:cached_tokens] : nil
|
|
15
|
-
cache_creation = has_attribute?(:cache_creation_tokens) ? self[:cache_creation_tokens] : nil
|
|
16
|
-
|
|
17
15
|
RubyLLM::Message.new(
|
|
18
16
|
role: role.to_sym,
|
|
19
17
|
content: extract_content,
|
|
18
|
+
thinking: thinking,
|
|
19
|
+
tokens: tokens,
|
|
20
20
|
tool_calls: extract_tool_calls,
|
|
21
21
|
tool_call_id: extract_tool_call_id,
|
|
22
|
-
input_tokens: input_tokens,
|
|
23
|
-
output_tokens: output_tokens,
|
|
24
|
-
cached_tokens: cached,
|
|
25
|
-
cache_creation_tokens: cache_creation,
|
|
26
22
|
model_id: model_association&.model_id
|
|
27
23
|
)
|
|
28
24
|
end
|
|
29
25
|
|
|
26
|
+
def thinking
|
|
27
|
+
RubyLLM::Thinking.build(
|
|
28
|
+
text: thinking_text_value,
|
|
29
|
+
signature: thinking_signature_value
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def tokens
|
|
34
|
+
RubyLLM::Tokens.build(
|
|
35
|
+
input: input_tokens,
|
|
36
|
+
output: output_tokens,
|
|
37
|
+
cached: cached_value,
|
|
38
|
+
cache_creation: cache_creation_value,
|
|
39
|
+
thinking: thinking_tokens_value
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def to_partial_path
|
|
44
|
+
partial_prefix = self.class.name.underscore.pluralize
|
|
45
|
+
role_partial = if to_llm.tool_call?
|
|
46
|
+
'tool_calls'
|
|
47
|
+
elsif role.to_s == 'tool'
|
|
48
|
+
'tool'
|
|
49
|
+
else
|
|
50
|
+
role.to_s.presence || 'assistant'
|
|
51
|
+
end
|
|
52
|
+
"#{partial_prefix}/#{role_partial}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def tool_error_message
|
|
56
|
+
payload_error_message(content)
|
|
57
|
+
end
|
|
58
|
+
|
|
30
59
|
private
|
|
31
60
|
|
|
61
|
+
def thinking_text_value
|
|
62
|
+
has_attribute?(:thinking_text) ? self[:thinking_text] : nil
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def thinking_signature_value
|
|
66
|
+
has_attribute?(:thinking_signature) ? self[:thinking_signature] : nil
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def cached_value
|
|
70
|
+
has_attribute?(:cached_tokens) ? self[:cached_tokens] : nil
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def cache_creation_value
|
|
74
|
+
has_attribute?(:cache_creation_tokens) ? self[:cache_creation_tokens] : nil
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def thinking_tokens_value
|
|
78
|
+
has_attribute?(:thinking_tokens) ? self[:thinking_tokens] : nil
|
|
79
|
+
end
|
|
80
|
+
|
|
32
81
|
def extract_tool_calls
|
|
33
82
|
tool_calls_association.to_h do |tool_call|
|
|
34
83
|
[
|
|
@@ -36,7 +85,8 @@ module RubyLLM
|
|
|
36
85
|
RubyLLM::ToolCall.new(
|
|
37
86
|
id: tool_call.tool_call_id,
|
|
38
87
|
name: tool_call.name,
|
|
39
|
-
arguments: tool_call.arguments
|
|
88
|
+
arguments: tool_call.arguments,
|
|
89
|
+
thought_signature: tool_call.try(:thought_signature)
|
|
40
90
|
)
|
|
41
91
|
]
|
|
42
92
|
end
|
|
@@ -77,7 +77,7 @@ module RubyLLM
|
|
|
77
77
|
delegate :supports?, :supports_vision?, :supports_functions?, :type,
|
|
78
78
|
:input_price_per_million, :output_price_per_million,
|
|
79
79
|
:function_calling?, :structured_output?, :batch?,
|
|
80
|
-
:reasoning?, :citations?, :streaming?, :provider_class,
|
|
80
|
+
:reasoning?, :citations?, :streaming?, :provider_class, :label,
|
|
81
81
|
to: :to_llm
|
|
82
82
|
end
|
|
83
83
|
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module ActiveRecord
|
|
5
|
+
# Shared helpers for parsing serialized payloads on ActiveRecord-backed models.
|
|
6
|
+
module PayloadHelpers
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def payload_error_message(value)
|
|
10
|
+
payload = parse_payload(value)
|
|
11
|
+
return unless payload.is_a?(Hash)
|
|
12
|
+
|
|
13
|
+
payload['error'] || payload[:error]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def parse_payload(value)
|
|
17
|
+
return value if value.is_a?(Hash) || value.is_a?(Array)
|
|
18
|
+
return if value.blank?
|
|
19
|
+
|
|
20
|
+
JSON.parse(value)
|
|
21
|
+
rescue JSON::ParserError
|
|
22
|
+
nil
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module ActiveRecord
|
|
5
|
+
# Methods mixed into tool call models.
|
|
6
|
+
module ToolCallMethods
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
include PayloadHelpers
|
|
9
|
+
|
|
10
|
+
def tool_error_message
|
|
11
|
+
payload_error_message(arguments)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|