ruby_llm 1.12.0 → 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 +11 -5
- 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 +1 -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 -2
- 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/upgrade_to_v1_10_generator.rb +1 -1
- 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/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 +87 -20
- data/lib/ruby_llm/active_record/chat_methods.rb +80 -22
- data/lib/ruby_llm/active_record/message_methods.rb +17 -0
- 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 +50 -8
- data/lib/ruby_llm/aliases.json +60 -21
- data/lib/ruby_llm/attachment.rb +4 -1
- data/lib/ruby_llm/chat.rb +113 -12
- data/lib/ruby_llm/configuration.rb +65 -66
- 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 +5 -3
- data/lib/ruby_llm/model/info.rb +15 -13
- data/lib/ruby_llm/models.json +12279 -13517
- data/lib/ruby_llm/models.rb +16 -6
- data/lib/ruby_llm/provider.rb +10 -1
- data/lib/ruby_llm/providers/anthropic/capabilities.rb +5 -119
- data/lib/ruby_llm/providers/anthropic/chat.rb +22 -5
- data/lib/ruby_llm/providers/anthropic/models.rb +3 -9
- 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 +1 -1
- data/lib/ruby_llm/providers/azure/embeddings.rb +1 -1
- data/lib/ruby_llm/providers/azure/models.rb +1 -1
- data/lib/ruby_llm/providers/azure.rb +92 -0
- data/lib/ruby_llm/providers/bedrock/chat.rb +50 -5
- data/lib/ruby_llm/providers/bedrock/models.rb +17 -1
- data/lib/ruby_llm/providers/bedrock/streaming.rb +8 -4
- data/lib/ruby_llm/providers/bedrock.rb +9 -1
- data/lib/ruby_llm/providers/deepseek/capabilities.rb +4 -114
- data/lib/ruby_llm/providers/deepseek.rb +5 -1
- data/lib/ruby_llm/providers/gemini/capabilities.rb +45 -207
- data/lib/ruby_llm/providers/gemini/chat.rb +20 -4
- 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 +2 -1
- data/lib/ruby_llm/providers/gemini/tools.rb +19 -0
- 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.rb +8 -0
- data/lib/ruby_llm/providers/mistral/capabilities.rb +8 -0
- data/lib/ruby_llm/providers/mistral/chat.rb +2 -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.rb +11 -1
- data/lib/ruby_llm/providers/openai/capabilities.rb +95 -195
- data/lib/ruby_llm/providers/openai/chat.rb +15 -5
- data/lib/ruby_llm/providers/openai/media.rb +4 -1
- data/lib/ruby_llm/providers/openai/models.rb +2 -4
- data/lib/ruby_llm/providers/openai/temperature.rb +2 -2
- data/lib/ruby_llm/providers/openai/tools.rb +27 -2
- data/lib/ruby_llm/providers/openai.rb +10 -0
- data/lib/ruby_llm/providers/openrouter/chat.rb +19 -5
- data/lib/ruby_llm/providers/openrouter/images.rb +69 -0
- data/lib/ruby_llm/providers/openrouter.rb +35 -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 +18 -6
- data/lib/ruby_llm/providers/xai.rb +4 -0
- data/lib/ruby_llm/stream_accumulator.rb +10 -5
- data/lib/ruby_llm/streaming.rb +7 -7
- data/lib/ruby_llm/tool.rb +48 -3
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/tasks/models.rake +33 -7
- data/lib/tasks/release.rake +1 -1
- data/lib/tasks/ruby_llm.rake +9 -1
- data/lib/tasks/vcr.rake +1 -1
- metadata +56 -15
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_message.html.erb.tt +0 -13
|
@@ -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)
|
|
@@ -99,6 +99,7 @@ module RubyLLM
|
|
|
99
99
|
ordered_messages.each do |msg|
|
|
100
100
|
@chat.add_message(msg.to_llm)
|
|
101
101
|
end
|
|
102
|
+
reapply_runtime_instructions(@chat)
|
|
102
103
|
|
|
103
104
|
setup_persistence_callbacks
|
|
104
105
|
end
|
|
@@ -111,6 +112,14 @@ module RubyLLM
|
|
|
111
112
|
self
|
|
112
113
|
end
|
|
113
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:)
|
|
120
|
+
self
|
|
121
|
+
end
|
|
122
|
+
|
|
114
123
|
def with_tool(...)
|
|
115
124
|
to_llm.with_tool(...)
|
|
116
125
|
self
|
|
@@ -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
|
|
|
@@ -267,6 +296,26 @@ module RubyLLM
|
|
|
267
296
|
system_messages + non_system_messages
|
|
268
297
|
end
|
|
269
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
|
+
|
|
270
319
|
def setup_persistence_callbacks
|
|
271
320
|
return @chat if @chat.instance_variable_get(:@_persistence_callbacks_setup)
|
|
272
321
|
|
|
@@ -281,21 +330,13 @@ module RubyLLM
|
|
|
281
330
|
@message = messages.create!(role: :assistant, content: '')
|
|
282
331
|
end
|
|
283
332
|
|
|
284
|
-
def persist_message_completion(message)
|
|
333
|
+
def persist_message_completion(message)
|
|
285
334
|
return unless message
|
|
286
335
|
|
|
287
336
|
tool_call_id = find_tool_call_id(message.tool_call_id) if message.tool_call_id
|
|
288
337
|
|
|
289
338
|
transaction do
|
|
290
|
-
content = message.content
|
|
291
|
-
attachments_to_persist = nil
|
|
292
|
-
|
|
293
|
-
if content.is_a?(RubyLLM::Content)
|
|
294
|
-
attachments_to_persist = content.attachments if content.attachments.any?
|
|
295
|
-
content = content.text
|
|
296
|
-
elsif content.is_a?(Hash) || content.is_a?(Array)
|
|
297
|
-
content = content.to_json
|
|
298
|
-
end
|
|
339
|
+
content, attachments_to_persist = prepare_content_for_storage(message.content)
|
|
299
340
|
|
|
300
341
|
@message.update!(
|
|
301
342
|
role: message.role,
|
|
@@ -312,14 +353,17 @@ module RubyLLM
|
|
|
312
353
|
end
|
|
313
354
|
end
|
|
314
355
|
|
|
315
|
-
def persist_tool_calls(tool_calls)
|
|
316
|
-
|
|
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')
|
|
317
361
|
|
|
318
362
|
tool_calls.each_value do |tool_call|
|
|
319
363
|
attributes = tool_call.to_h
|
|
320
364
|
attributes.delete(:thought_signature) unless supports_thought_signature
|
|
321
365
|
attributes[:tool_call_id] = attributes.delete(:id)
|
|
322
|
-
|
|
366
|
+
tool_call_assoc.create!(**attributes)
|
|
323
367
|
end
|
|
324
368
|
end
|
|
325
369
|
|
|
@@ -363,6 +407,29 @@ module RubyLLM
|
|
|
363
407
|
RubyLLM.logger.warn "Failed to process attachment #{source}: #{e.message}"
|
|
364
408
|
nil
|
|
365
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
|
|
366
433
|
end
|
|
367
434
|
|
|
368
435
|
# Methods mixed into message models.
|
|
@@ -79,7 +79,8 @@ 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
|
|
|
@@ -87,6 +88,7 @@ module RubyLLM
|
|
|
87
88
|
ordered_messages.each do |msg|
|
|
88
89
|
@chat.add_message(msg.to_llm)
|
|
89
90
|
end
|
|
91
|
+
reapply_runtime_instructions(@chat)
|
|
90
92
|
|
|
91
93
|
setup_persistence_callbacks
|
|
92
94
|
end
|
|
@@ -99,6 +101,14 @@ module RubyLLM
|
|
|
99
101
|
self
|
|
100
102
|
end
|
|
101
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:)
|
|
109
|
+
self
|
|
110
|
+
end
|
|
111
|
+
|
|
102
112
|
def with_tool(...)
|
|
103
113
|
to_llm.with_tool(...)
|
|
104
114
|
self
|
|
@@ -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
|
|
|
@@ -178,22 +188,32 @@ module RubyLLM
|
|
|
178
188
|
self
|
|
179
189
|
end
|
|
180
190
|
|
|
181
|
-
def
|
|
182
|
-
|
|
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)
|
|
183
194
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
|
201
|
+
|
|
202
|
+
message_record = messages_association.create!(attrs)
|
|
203
|
+
message_record.update!(content_raw:) if message_record.respond_to?(:content_raw=)
|
|
188
204
|
|
|
189
|
-
persist_content(message_record, with) if with.present?
|
|
190
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?
|
|
191
207
|
|
|
192
208
|
message_record
|
|
193
209
|
end
|
|
194
210
|
|
|
195
|
-
def
|
|
196
|
-
|
|
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))
|
|
197
217
|
complete(&)
|
|
198
218
|
end
|
|
199
219
|
|
|
@@ -223,9 +243,10 @@ module RubyLLM
|
|
|
223
243
|
if last.tool_call?
|
|
224
244
|
last.destroy
|
|
225
245
|
elsif last.tool_result?
|
|
226
|
-
tool_call_message = last.parent_tool_call.
|
|
227
|
-
expected_results = tool_call_message.
|
|
228
|
-
|
|
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)
|
|
229
250
|
|
|
230
251
|
if expected_results.sort != actual_results.sort
|
|
231
252
|
tool_call_message.tool_results.each(&:destroy)
|
|
@@ -278,6 +299,26 @@ module RubyLLM
|
|
|
278
299
|
system_messages + non_system_messages
|
|
279
300
|
end
|
|
280
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
|
+
|
|
281
322
|
def persist_new_message
|
|
282
323
|
@message = messages_association.create!(role: :assistant, content: '')
|
|
283
324
|
end
|
|
@@ -323,15 +364,15 @@ module RubyLLM
|
|
|
323
364
|
end
|
|
324
365
|
# rubocop:enable Metrics/PerceivedComplexity
|
|
325
366
|
|
|
326
|
-
def persist_tool_calls(tool_calls)
|
|
327
|
-
tool_call_klass =
|
|
367
|
+
def persist_tool_calls(tool_calls, message_record: @message)
|
|
368
|
+
tool_call_klass = message_record.tool_calls_association.klass
|
|
328
369
|
supports_thought_signature = tool_call_klass.column_names.include?('thought_signature')
|
|
329
370
|
|
|
330
371
|
tool_calls.each_value do |tool_call|
|
|
331
372
|
attributes = tool_call.to_h
|
|
332
373
|
attributes.delete(:thought_signature) unless supports_thought_signature
|
|
333
374
|
attributes[:tool_call_id] = attributes.delete(:id)
|
|
334
|
-
|
|
375
|
+
message_record.tool_calls_association.create!(**attributes)
|
|
335
376
|
end
|
|
336
377
|
end
|
|
337
378
|
|
|
@@ -376,16 +417,33 @@ module RubyLLM
|
|
|
376
417
|
|
|
377
418
|
attachment = source.is_a?(RubyLLM::Attachment) ? source : RubyLLM::Attachment.new(source)
|
|
378
419
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
|
384
432
|
rescue StandardError => e
|
|
385
433
|
RubyLLM.logger.warn "Failed to process attachment #{source}: #{e.message}"
|
|
386
434
|
nil
|
|
387
435
|
end
|
|
388
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
|
+
|
|
389
447
|
def prepare_content_for_storage(content)
|
|
390
448
|
attachments = nil
|
|
391
449
|
content_raw = nil
|
|
@@ -5,6 +5,7 @@ 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
|
|
@@ -39,6 +40,22 @@ module RubyLLM
|
|
|
39
40
|
)
|
|
40
41
|
end
|
|
41
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
|
+
|
|
42
59
|
private
|
|
43
60
|
|
|
44
61
|
def thinking_text_value
|
|
@@ -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
|
data/lib/ruby_llm/agent.rb
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'erb'
|
|
4
|
+
require 'forwardable'
|
|
4
5
|
require 'pathname'
|
|
6
|
+
require 'ruby_llm/schema'
|
|
5
7
|
|
|
6
8
|
module RubyLLM
|
|
7
9
|
# Base class for simple, class-configured agents.
|
|
8
10
|
class Agent
|
|
11
|
+
extend Forwardable
|
|
9
12
|
include Enumerable
|
|
10
13
|
|
|
11
14
|
class << self
|
|
@@ -118,6 +121,7 @@ module RubyLLM
|
|
|
118
121
|
input_values, = partition_inputs(kwargs)
|
|
119
122
|
record = resolved_chat_model.find(id)
|
|
120
123
|
apply_configuration(record, input_values:, persist_instructions: false)
|
|
124
|
+
|
|
121
125
|
record
|
|
122
126
|
end
|
|
123
127
|
|
|
@@ -126,6 +130,7 @@ module RubyLLM
|
|
|
126
130
|
|
|
127
131
|
input_values, = partition_inputs(kwargs)
|
|
128
132
|
record = chat_or_id.is_a?(resolved_chat_model) ? chat_or_id : resolved_chat_model.find(chat_or_id)
|
|
133
|
+
apply_assume_model_exists(record)
|
|
129
134
|
runtime = runtime_context(chat: record, inputs: input_values)
|
|
130
135
|
instructions_value = resolved_instructions_value(record, runtime, inputs: input_values)
|
|
131
136
|
return record if instructions_value.nil?
|
|
@@ -136,7 +141,10 @@ module RubyLLM
|
|
|
136
141
|
|
|
137
142
|
def render_prompt(name, chat:, inputs:, locals:)
|
|
138
143
|
path = prompt_path_for(name)
|
|
139
|
-
|
|
144
|
+
unless File.exist?(path)
|
|
145
|
+
raise RubyLLM::PromptNotFoundError,
|
|
146
|
+
"Prompt file not found for #{self}: #{path}. Create the file or use inline instructions."
|
|
147
|
+
end
|
|
140
148
|
|
|
141
149
|
resolved_locals = resolve_prompt_locals(locals, runtime: runtime_context(chat:, inputs:), chat:, inputs:)
|
|
142
150
|
ERB.new(File.read(path)).result_with_hash(resolved_locals)
|
|
@@ -175,7 +183,10 @@ module RubyLLM
|
|
|
175
183
|
value = resolved_instructions_value(chat_object, runtime, inputs:)
|
|
176
184
|
return if value.nil?
|
|
177
185
|
|
|
178
|
-
instruction_target(chat_object, persist:)
|
|
186
|
+
target = instruction_target(chat_object, persist:)
|
|
187
|
+
return target.with_runtime_instructions(value) if use_runtime_instructions?(target, persist:)
|
|
188
|
+
|
|
189
|
+
target.with_instructions(value)
|
|
179
190
|
end
|
|
180
191
|
|
|
181
192
|
def apply_tools(llm_chat, runtime)
|
|
@@ -202,20 +213,40 @@ module RubyLLM
|
|
|
202
213
|
end
|
|
203
214
|
|
|
204
215
|
def apply_schema(llm_chat, runtime)
|
|
205
|
-
value =
|
|
216
|
+
value = resolved_schema_value(runtime)
|
|
206
217
|
llm_chat.with_schema(value) if value
|
|
207
218
|
end
|
|
208
219
|
|
|
220
|
+
def resolved_schema_value(runtime)
|
|
221
|
+
value = schema
|
|
222
|
+
return value unless value.is_a?(Proc)
|
|
223
|
+
|
|
224
|
+
evaluate(value, runtime)
|
|
225
|
+
rescue NoMethodError => e
|
|
226
|
+
raise unless e.receiver.equal?(runtime)
|
|
227
|
+
|
|
228
|
+
RubyLLM::Schema.create(&value)
|
|
229
|
+
end
|
|
230
|
+
|
|
209
231
|
def llm_chat_for(chat_object)
|
|
232
|
+
apply_assume_model_exists(chat_object)
|
|
210
233
|
chat_object.respond_to?(:to_llm) ? chat_object.to_llm : chat_object
|
|
211
234
|
end
|
|
212
235
|
|
|
236
|
+
def apply_assume_model_exists(chat_object)
|
|
237
|
+
return unless chat_kwargs.key?(:assume_model_exists) &&
|
|
238
|
+
resolved_chat_model &&
|
|
239
|
+
chat_object.is_a?(resolved_chat_model)
|
|
240
|
+
|
|
241
|
+
chat_object.assume_model_exists = chat_kwargs[:assume_model_exists]
|
|
242
|
+
end
|
|
243
|
+
|
|
213
244
|
def evaluate(value, runtime)
|
|
214
245
|
value.is_a?(Proc) ? runtime.instance_exec(&value) : value
|
|
215
246
|
end
|
|
216
247
|
|
|
217
248
|
def resolved_instructions_value(chat_object, runtime, inputs:)
|
|
218
|
-
value = evaluate(instructions, runtime)
|
|
249
|
+
value = evaluate(@instructions, runtime)
|
|
219
250
|
return value unless prompt_instruction?(value)
|
|
220
251
|
|
|
221
252
|
runtime.prompt(
|
|
@@ -232,10 +263,20 @@ module RubyLLM
|
|
|
232
263
|
if persist || !chat_object.respond_to?(:to_llm)
|
|
233
264
|
chat_object
|
|
234
265
|
else
|
|
235
|
-
chat_object
|
|
266
|
+
runtime_instruction_target(chat_object)
|
|
236
267
|
end
|
|
237
268
|
end
|
|
238
269
|
|
|
270
|
+
def runtime_instruction_target(chat_object)
|
|
271
|
+
return chat_object if chat_object.respond_to?(:with_runtime_instructions)
|
|
272
|
+
|
|
273
|
+
chat_object.to_llm
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def use_runtime_instructions?(target, persist:)
|
|
277
|
+
!persist && target.respond_to?(:with_runtime_instructions)
|
|
278
|
+
end
|
|
279
|
+
|
|
239
280
|
def resolve_prompt_locals(locals, runtime:, chat:, inputs:)
|
|
240
281
|
base = { chat: chat }.merge(inputs)
|
|
241
282
|
evaluated = locals.each_with_object({}) do |(key, value), acc|
|
|
@@ -316,8 +357,9 @@ module RubyLLM
|
|
|
316
357
|
|
|
317
358
|
attr_reader :chat
|
|
318
359
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
360
|
+
def_delegators :chat, :model, :messages, :tools, :params, :headers, :schema, :ask, :say, :with_tool, :with_tools,
|
|
361
|
+
:with_model, :with_temperature, :with_thinking, :with_context, :with_params, :with_headers,
|
|
362
|
+
:with_schema, :on_new_message, :on_end_message, :on_tool_call, :on_tool_result, :each, :complete,
|
|
363
|
+
:add_message, :reset_messages!
|
|
322
364
|
end
|
|
323
365
|
end
|