ruby_llm 1.11.0 → 1.12.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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +12 -0
  3. data/lib/ruby_llm/active_record/acts_as.rb +0 -2
  4. data/lib/ruby_llm/active_record/acts_as_legacy.rb +97 -27
  5. data/lib/ruby_llm/active_record/chat_methods.rb +73 -19
  6. data/lib/ruby_llm/agent.rb +326 -0
  7. data/lib/ruby_llm/aliases.json +47 -29
  8. data/lib/ruby_llm/chat.rb +27 -3
  9. data/lib/ruby_llm/configuration.rb +3 -0
  10. data/lib/ruby_llm/content.rb +6 -0
  11. data/lib/ruby_llm/models.json +19090 -5190
  12. data/lib/ruby_llm/models.rb +35 -6
  13. data/lib/ruby_llm/provider.rb +8 -0
  14. data/lib/ruby_llm/providers/azure/chat.rb +29 -0
  15. data/lib/ruby_llm/providers/azure/embeddings.rb +24 -0
  16. data/lib/ruby_llm/providers/azure/media.rb +45 -0
  17. data/lib/ruby_llm/providers/azure/models.rb +14 -0
  18. data/lib/ruby_llm/providers/azure.rb +56 -0
  19. data/lib/ruby_llm/providers/bedrock/auth.rb +122 -0
  20. data/lib/ruby_llm/providers/bedrock/chat.rb +296 -64
  21. data/lib/ruby_llm/providers/bedrock/media.rb +62 -33
  22. data/lib/ruby_llm/providers/bedrock/models.rb +88 -65
  23. data/lib/ruby_llm/providers/bedrock/streaming.rb +305 -8
  24. data/lib/ruby_llm/providers/bedrock.rb +61 -52
  25. data/lib/ruby_llm/version.rb +1 -1
  26. data/lib/ruby_llm.rb +4 -0
  27. data/lib/tasks/models.rake +10 -5
  28. data/lib/tasks/vcr.rake +32 -0
  29. metadata +17 -17
  30. data/lib/ruby_llm/providers/bedrock/capabilities.rb +0 -167
  31. data/lib/ruby_llm/providers/bedrock/signing.rb +0 -831
  32. data/lib/ruby_llm/providers/bedrock/streaming/base.rb +0 -51
  33. data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +0 -128
  34. data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +0 -67
  35. data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +0 -85
  36. data/lib/ruby_llm/providers/bedrock/streaming/prelude_handling.rb +0 -78
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c6cabf287dc7cd62616a9061131291f404c391be59f1468ccdb3b7f7cb9d2cb5
4
- data.tar.gz: bf1123d01a7ddadcbfde46a2ffb6e378928bcbcc0b1cdb0b927b7905f0534c12
3
+ metadata.gz: 6f2aa1f16058fca83243f2b098b0a3f454fb9383e410a00b04955cd5b48cbf54
4
+ data.tar.gz: fb4591fe16b50449dc1baf90f77a2b92baa986a51b9573f5c0b79dc231d9a9b9
5
5
  SHA512:
6
- metadata.gz: 12c1f267367279716c8e5e7fe6ef44327e4921b4e24516313f99954775a4c51d7e874d42184f466f8eb71ebe4f814b14a93b63b043c46096e7be4d804a15fc76
7
- data.tar.gz: dd2d797e49e3c1741108b63f191ccc3252d1fec1868851802ceb33b65afec4609498cbdb8ee516800e48f04220fdd4179b43abb645177e8ed14ea25c1036916d
6
+ metadata.gz: 687200d2c127d604e0bbff56c888ef5bb5ab2938b585ec2f8959f77b74ad35ae0d0dbad588068ccccd3219ab0aa4208dbc47b8436606c7b12ec1de10cf2928c2
7
+ data.tar.gz: 9aab7e7a79aa98b2772e4a01f4269c17a1969755276fde9b0fb4b3311a81dd4ff3a4a1e4e4c4a1f0fa863a162caaa4f3d3cd05cc044cfc5116ceaaa878963737
data/README.md CHANGED
@@ -95,6 +95,17 @@ end
95
95
  chat.with_tool(Weather).ask "What's the weather in Berlin?"
96
96
  ```
97
97
 
98
+ ```ruby
99
+ # Define an agent with instructions + tools
100
+ class WeatherAssistant < RubyLLM::Agent
101
+ model "gpt-4.1-nano"
102
+ instructions "Be concise and always use tools for weather."
103
+ tools Weather
104
+ end
105
+
106
+ WeatherAssistant.new.ask "What's the weather in Berlin?"
107
+ ```
108
+
98
109
  ```ruby
99
110
  # Get structured output
100
111
  class ProductSchema < RubyLLM::Schema
@@ -118,6 +129,7 @@ response = chat.with_schema(ProductSchema).ask "Analyze this product", with: "pr
118
129
  * **Embeddings:** Generate embeddings with `RubyLLM.embed`
119
130
  * **Moderation:** Content safety with `RubyLLM.moderate`
120
131
  * **Tools:** Let AI call your Ruby methods
132
+ * **Agents:** Reusable assistants with `RubyLLM::Agent`
121
133
  * **Structured output:** JSON schemas that just work
122
134
  * **Streaming:** Real-time responses with blocks
123
135
  * **Rails:** ActiveRecord integration with `acts_as_chat`
@@ -53,8 +53,6 @@ module RubyLLM
53
53
  foreign_key: model_foreign_key,
54
54
  optional: true
55
55
 
56
- delegate :add_message, to: :to_llm
57
-
58
56
  define_method :messages_association do
59
57
  send(messages_association_name)
60
58
  end
@@ -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',
@@ -95,19 +93,19 @@ module RubyLLM
95
93
  end
96
94
  @chat.reset_messages!
97
95
 
98
- messages.each do |msg|
96
+ ordered_messages = order_messages_for_llm(messages.to_a)
97
+ ordered_messages.each do |msg|
99
98
  @chat.add_message(msg.to_llm)
100
99
  end
101
100
 
102
101
  setup_persistence_callbacks
103
102
  end
104
103
 
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)
104
+ def with_instructions(instructions, append: false, replace: nil)
105
+ append = append_instructions?(append:, replace:)
106
+ persist_system_instruction(instructions, append:)
107
+
108
+ to_llm.with_instructions(instructions, append:, replace:)
111
109
  self
112
110
  end
113
111
 
@@ -185,14 +183,34 @@ module RubyLLM
185
183
  self
186
184
  end
187
185
 
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?
186
+ def add_message(message_or_attributes)
187
+ llm_message = message_or_attributes.is_a?(RubyLLM::Message) ? message_or_attributes : RubyLLM::Message.new(message_or_attributes)
188
+ content, attachments_to_persist = prepare_content_for_storage(llm_message.content)
189
+
190
+ attrs = { role: llm_message.role, content: }
191
+ tool_call_foreign_key = messages.klass.tool_call_foreign_key
192
+ if llm_message.tool_call_id && tool_call_foreign_key
193
+ tool_call_id = find_tool_call_id(llm_message.tool_call_id)
194
+ attrs[tool_call_foreign_key] = tool_call_id if tool_call_id
195
+ end
196
+
197
+ message_record = messages.create!(attrs)
198
+ persist_content(message_record, attachments_to_persist) if attachments_to_persist.present?
199
+ persist_tool_calls(llm_message.tool_calls, message_record:) if llm_message.tool_calls.present?
200
+
191
201
  message_record
192
202
  end
193
203
 
194
- def ask(message, with: nil, &)
195
- create_user_message(message, with:)
204
+ def create_user_message(content, with: nil)
205
+ RubyLLM.logger.warn(
206
+ '`create_user_message` is deprecated and will be removed in RubyLLM 2.0. ' \
207
+ 'Use `add_message(role: :user, content: ...)` instead.'
208
+ )
209
+ add_message(role: :user, content: build_content(content, with))
210
+ end
211
+
212
+ def ask(message = nil, with: nil, &)
213
+ add_message(role: :user, content: build_content(message, with))
196
214
  complete(&)
197
215
  end
198
216
 
@@ -233,6 +251,40 @@ module RubyLLM
233
251
  end
234
252
  end
235
253
 
254
+ def replace_persisted_system_instructions(instructions)
255
+ system_messages = messages.where(role: :system).order(:id).to_a
256
+
257
+ if system_messages.empty?
258
+ messages.create!(role: :system, content: instructions)
259
+ return
260
+ end
261
+
262
+ primary_message = system_messages.shift
263
+ primary_message.update!(content: instructions) if primary_message.content != instructions
264
+ system_messages.each(&:destroy!)
265
+ end
266
+
267
+ def append_instructions?(append:, replace:)
268
+ return append if replace.nil?
269
+
270
+ append || (replace == false)
271
+ end
272
+
273
+ def persist_system_instruction(instructions, append:)
274
+ transaction do
275
+ if append
276
+ messages.create!(role: :system, content: instructions)
277
+ else
278
+ replace_persisted_system_instructions(instructions)
279
+ end
280
+ end
281
+ end
282
+
283
+ def order_messages_for_llm(messages)
284
+ system_messages, non_system_messages = messages.partition { |msg| msg.role.to_s == 'system' }
285
+ system_messages + non_system_messages
286
+ end
287
+
236
288
  def setup_persistence_callbacks
237
289
  return @chat if @chat.instance_variable_get(:@_persistence_callbacks_setup)
238
290
 
@@ -247,21 +299,13 @@ module RubyLLM
247
299
  @message = messages.create!(role: :assistant, content: '')
248
300
  end
249
301
 
250
- def persist_message_completion(message) # rubocop:disable Metrics/PerceivedComplexity
302
+ def persist_message_completion(message)
251
303
  return unless message
252
304
 
253
305
  tool_call_id = find_tool_call_id(message.tool_call_id) if message.tool_call_id
254
306
 
255
307
  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
308
+ content, attachments_to_persist = prepare_content_for_storage(message.content)
265
309
 
266
310
  @message.update!(
267
311
  role: message.role,
@@ -278,14 +322,17 @@ module RubyLLM
278
322
  end
279
323
  end
280
324
 
281
- def persist_tool_calls(tool_calls)
282
- supports_thought_signature = tool_calls.klass.column_names.include?('thought_signature')
325
+ def persist_tool_calls(tool_calls, message_record: @message)
326
+ tool_call_assoc = message_record.respond_to?(:tool_calls) ? message_record.tool_calls : nil
327
+ return unless tool_call_assoc
328
+
329
+ supports_thought_signature = tool_call_assoc.klass.column_names.include?('thought_signature')
283
330
 
284
331
  tool_calls.each_value do |tool_call|
285
332
  attributes = tool_call.to_h
286
333
  attributes.delete(:thought_signature) unless supports_thought_signature
287
334
  attributes[:tool_call_id] = attributes.delete(:id)
288
- @message.tool_calls.create!(**attributes)
335
+ tool_call_assoc.create!(**attributes)
289
336
  end
290
337
  end
291
338
 
@@ -329,6 +376,29 @@ module RubyLLM
329
376
  RubyLLM.logger.warn "Failed to process attachment #{source}: #{e.message}"
330
377
  nil
331
378
  end
379
+
380
+ def build_content(message, attachments)
381
+ return message if content_like?(message)
382
+
383
+ RubyLLM::Content.new(message, attachments)
384
+ end
385
+
386
+ def content_like?(object)
387
+ object.is_a?(RubyLLM::Content) || object.is_a?(RubyLLM::Content::Raw)
388
+ end
389
+
390
+ def prepare_content_for_storage(content)
391
+ attachments = nil
392
+
393
+ if content.is_a?(RubyLLM::Content)
394
+ attachments = content.attachments if content.attachments.any?
395
+ [content.text, attachments]
396
+ elsif content.is_a?(Hash) || content.is_a?(Array)
397
+ [content.to_json, attachments]
398
+ else
399
+ [content, attachments]
400
+ end
401
+ end
332
402
  end
333
403
 
334
404
  # Methods mixed into message models.
@@ -83,19 +83,19 @@ module RubyLLM
83
83
  )
84
84
  @chat.reset_messages!
85
85
 
86
- messages_association.each do |msg|
86
+ ordered_messages = order_messages_for_llm(messages_association.to_a)
87
+ ordered_messages.each do |msg|
87
88
  @chat.add_message(msg.to_llm)
88
89
  end
89
90
 
90
91
  setup_persistence_callbacks
91
92
  end
92
93
 
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)
94
+ def with_instructions(instructions, append: false, replace: nil)
95
+ append = append_instructions?(append:, replace:)
96
+ persist_system_instruction(instructions, append:)
97
+
98
+ to_llm.with_instructions(instructions, append:, replace:)
99
99
  self
100
100
  end
101
101
 
@@ -178,22 +178,32 @@ module RubyLLM
178
178
  self
179
179
  end
180
180
 
181
- def create_user_message(content, with: nil)
182
- content_text, attachments, content_raw = prepare_content_for_storage(content)
181
+ def add_message(message_or_attributes)
182
+ llm_message = message_or_attributes.is_a?(RubyLLM::Message) ? message_or_attributes : RubyLLM::Message.new(message_or_attributes)
183
+ content_text, attachments, content_raw = prepare_content_for_storage(llm_message.content)
184
+
185
+ attrs = { role: llm_message.role, content: content_text }
186
+ parent_tool_call_assoc = messages_association.klass.reflect_on_association(:parent_tool_call)
187
+ if parent_tool_call_assoc && llm_message.tool_call_id
188
+ tool_call_id = find_tool_call_id(llm_message.tool_call_id)
189
+ attrs[parent_tool_call_assoc.foreign_key] = tool_call_id if tool_call_id
190
+ end
183
191
 
184
- message_record = messages_association.build(role: :user)
185
- message_record.content = content_text
186
- message_record.content_raw = content_raw if message_record.respond_to?(:content_raw=)
187
- message_record.save!
192
+ message_record = messages_association.create!(attrs)
193
+ message_record.update!(content_raw:) if message_record.respond_to?(:content_raw=)
188
194
 
189
- persist_content(message_record, with) if with.present?
190
195
  persist_content(message_record, attachments) if attachments.present?
196
+ persist_tool_calls(llm_message.tool_calls, message_record:) if llm_message.tool_calls.present?
191
197
 
192
198
  message_record
193
199
  end
194
200
 
195
- def ask(message, with: nil, &)
196
- create_user_message(message, with:)
201
+ def create_user_message(content, with: nil)
202
+ add_message(role: :user, content: build_content(content, with))
203
+ end
204
+
205
+ def ask(message = nil, with: nil, &)
206
+ add_message(role: :user, content: build_content(message, with))
197
207
  complete(&)
198
208
  end
199
209
 
@@ -244,6 +254,40 @@ module RubyLLM
244
254
  @chat
245
255
  end
246
256
 
257
+ def replace_persisted_system_instructions(instructions)
258
+ system_messages = messages_association.where(role: :system).order(:id).to_a
259
+
260
+ if system_messages.empty?
261
+ messages_association.create!(role: :system, content: instructions)
262
+ return
263
+ end
264
+
265
+ primary_message = system_messages.shift
266
+ primary_message.update!(content: instructions) if primary_message.content != instructions
267
+ system_messages.each(&:destroy!)
268
+ end
269
+
270
+ def append_instructions?(append:, replace:)
271
+ return append if replace.nil?
272
+
273
+ append || (replace == false)
274
+ end
275
+
276
+ def persist_system_instruction(instructions, append:)
277
+ transaction do
278
+ if append
279
+ messages_association.create!(role: :system, content: instructions)
280
+ else
281
+ replace_persisted_system_instructions(instructions)
282
+ end
283
+ end
284
+ end
285
+
286
+ def order_messages_for_llm(messages)
287
+ system_messages, non_system_messages = messages.partition { |msg| msg.role.to_s == 'system' }
288
+ system_messages + non_system_messages
289
+ end
290
+
247
291
  def persist_new_message
248
292
  @message = messages_association.create!(role: :assistant, content: '')
249
293
  end
@@ -289,15 +333,15 @@ module RubyLLM
289
333
  end
290
334
  # rubocop:enable Metrics/PerceivedComplexity
291
335
 
292
- def persist_tool_calls(tool_calls)
293
- tool_call_klass = @message.tool_calls_association.klass
336
+ def persist_tool_calls(tool_calls, message_record: @message)
337
+ tool_call_klass = message_record.tool_calls_association.klass
294
338
  supports_thought_signature = tool_call_klass.column_names.include?('thought_signature')
295
339
 
296
340
  tool_calls.each_value do |tool_call|
297
341
  attributes = tool_call.to_h
298
342
  attributes.delete(:thought_signature) unless supports_thought_signature
299
343
  attributes[:tool_call_id] = attributes.delete(:id)
300
- @message.tool_calls_association.create!(**attributes)
344
+ message_record.tool_calls_association.create!(**attributes)
301
345
  end
302
346
  end
303
347
 
@@ -352,6 +396,16 @@ module RubyLLM
352
396
  nil
353
397
  end
354
398
 
399
+ def build_content(message, attachments)
400
+ return message if content_like?(message)
401
+
402
+ RubyLLM::Content.new(message, attachments)
403
+ end
404
+
405
+ def content_like?(object)
406
+ object.is_a?(RubyLLM::Content) || object.is_a?(RubyLLM::Content::Raw)
407
+ end
408
+
355
409
  def prepare_content_for_storage(content)
356
410
  attachments = nil
357
411
  content_raw = nil