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.
Files changed (170) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +27 -8
  3. data/lib/generators/ruby_llm/agent/agent_generator.rb +36 -0
  4. data/lib/generators/ruby_llm/agent/templates/agent.rb.tt +6 -0
  5. data/lib/generators/ruby_llm/agent/templates/instructions.txt.erb.tt +0 -0
  6. data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +110 -41
  7. data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +14 -15
  8. data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +8 -11
  9. data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +2 -2
  10. data/lib/generators/ruby_llm/chat_ui/templates/helpers/messages_helper.rb.tt +25 -0
  11. data/lib/generators/ruby_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +2 -2
  12. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_chat.html.erb.tt +16 -0
  13. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +31 -0
  14. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/index.html.erb.tt +31 -0
  15. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/new.html.erb.tt +9 -0
  16. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +27 -0
  17. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +14 -0
  18. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_content.html.erb.tt +1 -0
  19. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_error.html.erb.tt +13 -0
  20. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_form.html.erb.tt +23 -0
  21. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +10 -0
  22. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool.html.erb.tt +2 -0
  23. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +4 -0
  24. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +14 -0
  25. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +13 -0
  26. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_results/_default.html.erb.tt +21 -0
  27. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/_model.html.erb.tt +17 -0
  28. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/index.html.erb.tt +40 -0
  29. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/show.html.erb.tt +27 -0
  30. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +2 -2
  31. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +2 -2
  32. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/index.html.erb.tt +19 -7
  33. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/new.html.erb.tt +1 -1
  34. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +5 -3
  35. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +9 -0
  36. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_content.html.erb.tt +1 -1
  37. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_error.html.erb.tt +8 -0
  38. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_form.html.erb.tt +1 -1
  39. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_system.html.erb.tt +6 -0
  40. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool.html.erb.tt +2 -0
  41. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +4 -7
  42. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_user.html.erb.tt +9 -0
  43. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +5 -7
  44. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +8 -0
  45. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_results/_default.html.erb.tt +16 -0
  46. data/lib/generators/ruby_llm/chat_ui/templates/views/models/_model.html.erb.tt +11 -12
  47. data/lib/generators/ruby_llm/chat_ui/templates/views/models/index.html.erb.tt +27 -17
  48. data/lib/generators/ruby_llm/chat_ui/templates/views/models/show.html.erb.tt +3 -4
  49. data/lib/generators/ruby_llm/generator_helpers.rb +37 -17
  50. data/lib/generators/ruby_llm/install/install_generator.rb +22 -18
  51. data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +1 -1
  52. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +4 -1
  53. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +4 -10
  54. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +2 -1
  55. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +2 -2
  56. data/lib/generators/ruby_llm/schema/schema_generator.rb +26 -0
  57. data/lib/generators/ruby_llm/schema/templates/schema.rb.tt +2 -0
  58. data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +9 -0
  59. data/lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt +13 -0
  60. data/lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt +13 -0
  61. data/lib/generators/ruby_llm/tool/tool_generator.rb +96 -0
  62. data/lib/generators/ruby_llm/upgrade_to_v1_10/templates/add_v1_10_message_columns.rb.tt +19 -0
  63. data/lib/generators/ruby_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +50 -0
  64. data/lib/generators/ruby_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +7 -0
  65. data/lib/generators/ruby_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +49 -0
  66. data/lib/generators/ruby_llm/upgrade_to_v1_7/templates/migration.rb.tt +1 -1
  67. data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +2 -4
  68. data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +1 -1
  69. data/lib/ruby_llm/active_record/acts_as.rb +10 -4
  70. data/lib/ruby_llm/active_record/acts_as_legacy.rb +132 -27
  71. data/lib/ruby_llm/active_record/chat_methods.rb +132 -28
  72. data/lib/ruby_llm/active_record/message_methods.rb +58 -8
  73. data/lib/ruby_llm/active_record/model_methods.rb +1 -1
  74. data/lib/ruby_llm/active_record/payload_helpers.rb +26 -0
  75. data/lib/ruby_llm/active_record/tool_call_methods.rb +15 -0
  76. data/lib/ruby_llm/agent.rb +365 -0
  77. data/lib/ruby_llm/aliases.json +199 -62
  78. data/lib/ruby_llm/attachment.rb +15 -4
  79. data/lib/ruby_llm/chat.rb +150 -22
  80. data/lib/ruby_llm/configuration.rb +65 -65
  81. data/lib/ruby_llm/connection.rb +11 -7
  82. data/lib/ruby_llm/content.rb +6 -2
  83. data/lib/ruby_llm/error.rb +37 -1
  84. data/lib/ruby_llm/message.rb +43 -15
  85. data/lib/ruby_llm/model/info.rb +15 -13
  86. data/lib/ruby_llm/models.json +37560 -14094
  87. data/lib/ruby_llm/models.rb +321 -38
  88. data/lib/ruby_llm/models_schema.json +2 -2
  89. data/lib/ruby_llm/provider.rb +26 -4
  90. data/lib/ruby_llm/providers/anthropic/capabilities.rb +5 -119
  91. data/lib/ruby_llm/providers/anthropic/chat.rb +149 -17
  92. data/lib/ruby_llm/providers/anthropic/media.rb +2 -2
  93. data/lib/ruby_llm/providers/anthropic/models.rb +3 -9
  94. data/lib/ruby_llm/providers/anthropic/streaming.rb +25 -1
  95. data/lib/ruby_llm/providers/anthropic/tools.rb +20 -0
  96. data/lib/ruby_llm/providers/anthropic.rb +5 -1
  97. data/lib/ruby_llm/providers/azure/chat.rb +29 -0
  98. data/lib/ruby_llm/providers/azure/embeddings.rb +24 -0
  99. data/lib/ruby_llm/providers/azure/media.rb +45 -0
  100. data/lib/ruby_llm/providers/azure/models.rb +14 -0
  101. data/lib/ruby_llm/providers/azure.rb +148 -0
  102. data/lib/ruby_llm/providers/bedrock/auth.rb +122 -0
  103. data/lib/ruby_llm/providers/bedrock/chat.rb +357 -28
  104. data/lib/ruby_llm/providers/bedrock/media.rb +62 -33
  105. data/lib/ruby_llm/providers/bedrock/models.rb +107 -62
  106. data/lib/ruby_llm/providers/bedrock/streaming.rb +309 -8
  107. data/lib/ruby_llm/providers/bedrock.rb +69 -52
  108. data/lib/ruby_llm/providers/deepseek/capabilities.rb +4 -114
  109. data/lib/ruby_llm/providers/deepseek.rb +5 -1
  110. data/lib/ruby_llm/providers/dify/chat.rb +82 -7
  111. data/lib/ruby_llm/providers/dify/media.rb +2 -2
  112. data/lib/ruby_llm/providers/dify/streaming.rb +26 -4
  113. data/lib/ruby_llm/providers/dify.rb +4 -0
  114. data/lib/ruby_llm/providers/gemini/capabilities.rb +45 -207
  115. data/lib/ruby_llm/providers/gemini/chat.rb +88 -6
  116. data/lib/ruby_llm/providers/gemini/images.rb +1 -1
  117. data/lib/ruby_llm/providers/gemini/models.rb +2 -4
  118. data/lib/ruby_llm/providers/gemini/streaming.rb +34 -2
  119. data/lib/ruby_llm/providers/gemini/tools.rb +35 -3
  120. data/lib/ruby_llm/providers/gemini.rb +4 -0
  121. data/lib/ruby_llm/providers/gpustack/capabilities.rb +20 -0
  122. data/lib/ruby_llm/providers/gpustack/chat.rb +1 -1
  123. data/lib/ruby_llm/providers/gpustack.rb +8 -0
  124. data/lib/ruby_llm/providers/mistral/capabilities.rb +8 -0
  125. data/lib/ruby_llm/providers/mistral/chat.rb +59 -1
  126. data/lib/ruby_llm/providers/mistral.rb +4 -0
  127. data/lib/ruby_llm/providers/ollama/capabilities.rb +20 -0
  128. data/lib/ruby_llm/providers/ollama/chat.rb +1 -1
  129. data/lib/ruby_llm/providers/ollama.rb +11 -1
  130. data/lib/ruby_llm/providers/openai/capabilities.rb +96 -192
  131. data/lib/ruby_llm/providers/openai/chat.rb +101 -7
  132. data/lib/ruby_llm/providers/openai/media.rb +5 -2
  133. data/lib/ruby_llm/providers/openai/models.rb +2 -4
  134. data/lib/ruby_llm/providers/openai/streaming.rb +11 -3
  135. data/lib/ruby_llm/providers/openai/temperature.rb +28 -0
  136. data/lib/ruby_llm/providers/openai/tools.rb +27 -2
  137. data/lib/ruby_llm/providers/openai.rb +11 -1
  138. data/lib/ruby_llm/providers/openrouter/chat.rb +168 -0
  139. data/lib/ruby_llm/providers/openrouter/images.rb +69 -0
  140. data/lib/ruby_llm/providers/openrouter/streaming.rb +74 -0
  141. data/lib/ruby_llm/providers/openrouter.rb +37 -1
  142. data/lib/ruby_llm/providers/perplexity/capabilities.rb +34 -99
  143. data/lib/ruby_llm/providers/perplexity/models.rb +12 -14
  144. data/lib/ruby_llm/providers/perplexity.rb +4 -0
  145. data/lib/ruby_llm/providers/vertexai/models.rb +1 -1
  146. data/lib/ruby_llm/providers/vertexai.rb +23 -7
  147. data/lib/ruby_llm/providers/xai/chat.rb +15 -0
  148. data/lib/ruby_llm/providers/xai/models.rb +75 -0
  149. data/lib/ruby_llm/providers/xai.rb +32 -0
  150. data/lib/ruby_llm/stream_accumulator.rb +120 -18
  151. data/lib/ruby_llm/streaming.rb +82 -60
  152. data/lib/ruby_llm/thinking.rb +49 -0
  153. data/lib/ruby_llm/tokens.rb +47 -0
  154. data/lib/ruby_llm/tool.rb +49 -4
  155. data/lib/ruby_llm/tool_call.rb +6 -3
  156. data/lib/ruby_llm/version.rb +1 -1
  157. data/lib/ruby_llm.rb +14 -8
  158. data/lib/tasks/models.rake +62 -23
  159. data/lib/tasks/release.rake +1 -1
  160. data/lib/tasks/ruby_llm.rake +9 -1
  161. data/lib/tasks/vcr.rake +33 -1
  162. metadata +67 -16
  163. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_message.html.erb.tt +0 -13
  164. data/lib/ruby_llm/providers/bedrock/capabilities.rb +0 -167
  165. data/lib/ruby_llm/providers/bedrock/signing.rb +0 -831
  166. data/lib/ruby_llm/providers/bedrock/streaming/base.rb +0 -51
  167. data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +0 -71
  168. data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +0 -67
  169. data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +0 -80
  170. 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.each do |msg|
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: false)
106
- transaction do
107
- messages.where(role: :system).destroy_all if replace
108
- messages.create!(role: :system, content: instructions)
109
- end
110
- to_llm.with_instructions(instructions)
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 create_user_message(content, with: nil)
189
- message_record = messages.create!(role: :user, content: content)
190
- persist_content(message_record, with) if with.present?
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 ask(message, with: nil, &)
195
- create_user_message(message, with:)
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) # rubocop:disable Metrics/PerceivedComplexity
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
- @message.tool_calls.create!(**attributes)
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.each do |msg|
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: false)
94
- transaction do
95
- messages_association.where(role: :system).destroy_all if replace
96
- messages_association.create!(role: :system, content: instructions)
97
- end
98
- to_llm.with_instructions(instructions)
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(model.model_id, provider: model.provider.to_sym, assume_exists:)
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 create_user_message(content, with: nil)
177
- content_text, attachments, content_raw = prepare_content_for_storage(content)
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.build(role: :user)
180
- message_record.content = content_text
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 ask(message, with: nil, &)
191
- create_user_message(message, with:)
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.message
222
- expected_results = tool_call_message.tool_calls.pluck(:id)
223
- actual_results = tool_call_message.tool_results.pluck(:tool_call_id)
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
- @message.tool_calls_association.create!(**attributes)
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
- io: StringIO.new(attachment.content),
335
- filename: attachment.filename,
336
- content_type: attachment.mime_type
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