ruby_llm_swarm 1.9.1 → 1.9.2

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.
@@ -1,383 +1,385 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module RubyLLM
4
- module ActiveRecord
5
- # Adds chat and message persistence capabilities to ActiveRecord models.
6
- module ActsAsLegacy
7
- extend ActiveSupport::Concern
8
-
9
- class_methods do # rubocop:disable Metrics/BlockLength
10
- def acts_as_chat(message_class: 'Message', tool_call_class: 'ToolCall')
11
- include ChatLegacyMethods
12
-
13
- @message_class = message_class.to_s
14
- @tool_call_class = tool_call_class.to_s
15
-
16
- has_many :messages,
17
- -> { order(created_at: :asc) },
18
- class_name: @message_class,
19
- inverse_of: :chat,
20
- dependent: :destroy
21
-
22
- delegate :add_message, to: :to_llm
23
- end
3
+ if defined?(ActiveRecord::Base)
4
+ module RubyLLM
5
+ module ActiveRecord
6
+ # Adds chat and message persistence capabilities to ActiveRecord models.
7
+ module ActsAsLegacy
8
+ extend ActiveSupport::Concern
9
+
10
+ class_methods do # rubocop:disable Metrics/BlockLength
11
+ def acts_as_chat(message_class: 'Message', tool_call_class: 'ToolCall')
12
+ include ChatLegacyMethods
13
+
14
+ @message_class = message_class.to_s
15
+ @tool_call_class = tool_call_class.to_s
16
+
17
+ has_many :messages,
18
+ -> { order(created_at: :asc) },
19
+ class_name: @message_class,
20
+ inverse_of: :chat,
21
+ dependent: :destroy
24
22
 
25
- def acts_as_message(chat_class: 'Chat',
26
- chat_foreign_key: nil,
27
- tool_call_class: 'ToolCall',
28
- tool_call_foreign_key: nil,
29
- touch_chat: false)
30
- include MessageLegacyMethods
23
+ delegate :add_message, to: :to_llm
24
+ end
31
25
 
32
- @chat_class = chat_class.to_s
33
- @chat_foreign_key = chat_foreign_key || ActiveSupport::Inflector.foreign_key(@chat_class)
26
+ def acts_as_message(chat_class: 'Chat',
27
+ chat_foreign_key: nil,
28
+ tool_call_class: 'ToolCall',
29
+ tool_call_foreign_key: nil,
30
+ touch_chat: false)
31
+ include MessageLegacyMethods
34
32
 
35
- @tool_call_class = tool_call_class.to_s
36
- @tool_call_foreign_key = tool_call_foreign_key || ActiveSupport::Inflector.foreign_key(@tool_call_class)
33
+ @chat_class = chat_class.to_s
34
+ @chat_foreign_key = chat_foreign_key || ActiveSupport::Inflector.foreign_key(@chat_class)
37
35
 
38
- belongs_to :chat,
39
- class_name: @chat_class,
40
- foreign_key: @chat_foreign_key,
41
- inverse_of: :messages,
42
- touch: touch_chat
36
+ @tool_call_class = tool_call_class.to_s
37
+ @tool_call_foreign_key = tool_call_foreign_key || ActiveSupport::Inflector.foreign_key(@tool_call_class)
43
38
 
44
- has_many :tool_calls,
45
- class_name: @tool_call_class,
46
- dependent: :destroy
39
+ belongs_to :chat,
40
+ class_name: @chat_class,
41
+ foreign_key: @chat_foreign_key,
42
+ inverse_of: :messages,
43
+ touch: touch_chat
47
44
 
48
- belongs_to :parent_tool_call,
45
+ has_many :tool_calls,
49
46
  class_name: @tool_call_class,
50
- foreign_key: @tool_call_foreign_key,
51
- optional: true,
52
- inverse_of: :result
53
-
54
- has_many :tool_results,
55
- through: :tool_calls,
56
- source: :result,
57
- class_name: @message_class
47
+ dependent: :destroy
58
48
 
59
- delegate :tool_call?, :tool_result?, to: :to_llm
60
- end
49
+ belongs_to :parent_tool_call,
50
+ class_name: @tool_call_class,
51
+ foreign_key: @tool_call_foreign_key,
52
+ optional: true,
53
+ inverse_of: :result
61
54
 
62
- def acts_as_tool_call(message_class: 'Message', message_foreign_key: nil, result_foreign_key: nil)
63
- @message_class = message_class.to_s
64
- @message_foreign_key = message_foreign_key || ActiveSupport::Inflector.foreign_key(@message_class)
65
- @result_foreign_key = result_foreign_key || ActiveSupport::Inflector.foreign_key(name)
55
+ has_many :tool_results,
56
+ through: :tool_calls,
57
+ source: :result,
58
+ class_name: @message_class
66
59
 
67
- belongs_to :message,
68
- class_name: @message_class,
69
- foreign_key: @message_foreign_key,
70
- inverse_of: :tool_calls
60
+ delegate :tool_call?, :tool_result?, to: :to_llm
61
+ end
71
62
 
72
- has_one :result,
73
- class_name: @message_class,
74
- foreign_key: @result_foreign_key,
75
- inverse_of: :parent_tool_call,
76
- dependent: :nullify
63
+ def acts_as_tool_call(message_class: 'Message', message_foreign_key: nil, result_foreign_key: nil)
64
+ @message_class = message_class.to_s
65
+ @message_foreign_key = message_foreign_key || ActiveSupport::Inflector.foreign_key(@message_class)
66
+ @result_foreign_key = result_foreign_key || ActiveSupport::Inflector.foreign_key(name)
67
+
68
+ belongs_to :message,
69
+ class_name: @message_class,
70
+ foreign_key: @message_foreign_key,
71
+ inverse_of: :tool_calls
72
+
73
+ has_one :result,
74
+ class_name: @message_class,
75
+ foreign_key: @result_foreign_key,
76
+ inverse_of: :parent_tool_call,
77
+ dependent: :nullify
78
+ end
77
79
  end
78
80
  end
79
- end
80
81
 
81
- # Methods mixed into chat models.
82
- module ChatLegacyMethods
83
- extend ActiveSupport::Concern
82
+ # Methods mixed into chat models.
83
+ module ChatLegacyMethods
84
+ extend ActiveSupport::Concern
84
85
 
85
- class_methods do
86
- attr_reader :tool_call_class
87
- end
86
+ class_methods do
87
+ attr_reader :tool_call_class
88
+ end
88
89
 
89
- def to_llm(context: nil)
90
- # model_id is a string that RubyLLM can resolve
91
- @chat ||= if context
92
- context.chat(model: model_id)
93
- else
94
- RubyLLM.chat(model: model_id)
95
- end
96
- @chat.reset_messages!
90
+ def to_llm(context: nil)
91
+ # model_id is a string that RubyLLM can resolve
92
+ @chat ||= if context
93
+ context.chat(model: model_id)
94
+ else
95
+ RubyLLM.chat(model: model_id)
96
+ end
97
+ @chat.reset_messages!
98
+
99
+ messages.each do |msg|
100
+ @chat.add_message(msg.to_llm)
101
+ end
97
102
 
98
- messages.each do |msg|
99
- @chat.add_message(msg.to_llm)
103
+ setup_persistence_callbacks
100
104
  end
101
105
 
102
- setup_persistence_callbacks
103
- end
104
-
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)
106
+ def with_instructions(instructions, replace: false)
107
+ transaction do
108
+ messages.where(role: :system).destroy_all if replace
109
+ messages.create!(role: :system, content: instructions)
110
+ end
111
+ to_llm.with_instructions(instructions)
112
+ self
109
113
  end
110
- to_llm.with_instructions(instructions)
111
- self
112
- end
113
114
 
114
- def with_tool(...)
115
- to_llm.with_tool(...)
116
- self
117
- end
115
+ def with_tool(...)
116
+ to_llm.with_tool(...)
117
+ self
118
+ end
118
119
 
119
- def with_tools(...)
120
- to_llm.with_tools(...)
121
- self
122
- end
120
+ def with_tools(...)
121
+ to_llm.with_tools(...)
122
+ self
123
+ end
123
124
 
124
- def with_model(...)
125
- update(model_id: to_llm.with_model(...).model.id)
126
- self
127
- end
125
+ def with_model(...)
126
+ update(model_id: to_llm.with_model(...).model.id)
127
+ self
128
+ end
128
129
 
129
- def with_temperature(...)
130
- to_llm.with_temperature(...)
131
- self
132
- end
130
+ def with_temperature(...)
131
+ to_llm.with_temperature(...)
132
+ self
133
+ end
133
134
 
134
- def with_context(context)
135
- to_llm(context: context)
136
- self
137
- end
135
+ def with_context(context)
136
+ to_llm(context: context)
137
+ self
138
+ end
138
139
 
139
- def with_params(...)
140
- to_llm.with_params(...)
141
- self
142
- end
140
+ def with_params(...)
141
+ to_llm.with_params(...)
142
+ self
143
+ end
143
144
 
144
- def with_headers(...)
145
- to_llm.with_headers(...)
146
- self
147
- end
145
+ def with_headers(...)
146
+ to_llm.with_headers(...)
147
+ self
148
+ end
148
149
 
149
- def with_schema(...)
150
- to_llm.with_schema(...)
151
- self
152
- end
150
+ def with_schema(...)
151
+ to_llm.with_schema(...)
152
+ self
153
+ end
153
154
 
154
- def on_new_message(&)
155
- to_llm.on_new_message(&)
156
- self
157
- end
155
+ def on_new_message(&)
156
+ to_llm.on_new_message(&)
157
+ self
158
+ end
158
159
 
159
- def on_end_message(&)
160
- to_llm.on_end_message(&)
161
- self
162
- end
160
+ def on_end_message(&)
161
+ to_llm.on_end_message(&)
162
+ self
163
+ end
163
164
 
164
- def on_tool_call(...)
165
- to_llm.on_tool_call(...)
166
- self
167
- end
165
+ def on_tool_call(...)
166
+ to_llm.on_tool_call(...)
167
+ self
168
+ end
168
169
 
169
- def on_tool_result(...)
170
- to_llm.on_tool_result(...)
171
- self
172
- end
170
+ def on_tool_result(...)
171
+ to_llm.on_tool_result(...)
172
+ self
173
+ end
173
174
 
174
- def create_user_message(content, with: nil)
175
- message_record = messages.create!(role: :user, content: content)
176
- persist_content(message_record, with) if with.present?
177
- message_record
178
- end
175
+ def create_user_message(content, with: nil)
176
+ message_record = messages.create!(role: :user, content: content)
177
+ persist_content(message_record, with) if with.present?
178
+ message_record
179
+ end
179
180
 
180
- def ask(message, with: nil, &)
181
- create_user_message(message, with:)
182
- complete(&)
183
- end
181
+ def ask(message, with: nil, &)
182
+ create_user_message(message, with:)
183
+ complete(&)
184
+ end
184
185
 
185
- alias say ask
186
+ alias say ask
186
187
 
187
- def complete(...)
188
- to_llm.complete(...)
189
- rescue RubyLLM::Error => e
190
- cleanup_failed_messages if @message&.persisted? && @message.content.blank?
191
- cleanup_orphaned_tool_results
192
- raise e
193
- end
188
+ def complete(...)
189
+ to_llm.complete(...)
190
+ rescue RubyLLM::Error => e
191
+ cleanup_failed_messages if @message&.persisted? && @message.content.blank?
192
+ cleanup_orphaned_tool_results
193
+ raise e
194
+ end
194
195
 
195
- private
196
+ private
196
197
 
197
- def cleanup_failed_messages
198
- RubyLLM.logger.warn "RubyLLM: API call failed, destroying message: #{@message.id}"
199
- @message.destroy
200
- end
198
+ def cleanup_failed_messages
199
+ RubyLLM.logger.warn "RubyLLM: API call failed, destroying message: #{@message.id}"
200
+ @message.destroy
201
+ end
201
202
 
202
- def cleanup_orphaned_tool_results # rubocop:disable Metrics/PerceivedComplexity
203
- messages.reload
204
- last = messages.order(:id).last
203
+ def cleanup_orphaned_tool_results # rubocop:disable Metrics/PerceivedComplexity
204
+ messages.reload
205
+ last = messages.order(:id).last
205
206
 
206
- return unless last&.tool_call? || last&.tool_result?
207
+ return unless last&.tool_call? || last&.tool_result?
207
208
 
208
- if last.tool_call?
209
- last.destroy
210
- elsif last.tool_result?
211
- tool_call_message = last.parent_tool_call.message
212
- expected_results = tool_call_message.tool_calls.pluck(:id)
213
- actual_results = tool_call_message.tool_results.pluck(:tool_call_id)
209
+ if last.tool_call?
210
+ last.destroy
211
+ elsif last.tool_result?
212
+ tool_call_message = last.parent_tool_call.message
213
+ expected_results = tool_call_message.tool_calls.pluck(:id)
214
+ actual_results = tool_call_message.tool_results.pluck(:tool_call_id)
214
215
 
215
- if expected_results.sort != actual_results.sort
216
- tool_call_message.tool_results.each(&:destroy)
217
- tool_call_message.destroy
216
+ if expected_results.sort != actual_results.sort
217
+ tool_call_message.tool_results.each(&:destroy)
218
+ tool_call_message.destroy
219
+ end
218
220
  end
219
221
  end
220
- end
221
222
 
222
- def setup_persistence_callbacks
223
- return @chat if @chat.instance_variable_get(:@_persistence_callbacks_setup)
223
+ def setup_persistence_callbacks
224
+ return @chat if @chat.instance_variable_get(:@_persistence_callbacks_setup)
224
225
 
225
- @chat.on_new_message { persist_new_message }
226
- @chat.on_end_message { |msg| persist_message_completion(msg) }
226
+ @chat.on_new_message { persist_new_message }
227
+ @chat.on_end_message { |msg| persist_message_completion(msg) }
227
228
 
228
- @chat.instance_variable_set(:@_persistence_callbacks_setup, true)
229
- @chat
230
- end
229
+ @chat.instance_variable_set(:@_persistence_callbacks_setup, true)
230
+ @chat
231
+ end
231
232
 
232
- def persist_new_message
233
- @message = messages.create!(role: :assistant, content: '')
234
- end
233
+ def persist_new_message
234
+ @message = messages.create!(role: :assistant, content: '')
235
+ end
235
236
 
236
- def persist_message_completion(message) # rubocop:disable Metrics/PerceivedComplexity
237
- return unless message
237
+ def persist_message_completion(message) # rubocop:disable Metrics/PerceivedComplexity
238
+ return unless message
238
239
 
239
- tool_call_id = find_tool_call_id(message.tool_call_id) if message.tool_call_id
240
+ tool_call_id = find_tool_call_id(message.tool_call_id) if message.tool_call_id
240
241
 
241
- transaction do
242
- content = message.content
243
- attachments_to_persist = nil
242
+ transaction do
243
+ content = message.content
244
+ attachments_to_persist = nil
244
245
 
245
- if content.is_a?(RubyLLM::Content)
246
- attachments_to_persist = content.attachments if content.attachments.any?
247
- content = content.text
248
- elsif content.is_a?(Hash) || content.is_a?(Array)
249
- content = content.to_json
250
- end
246
+ if content.is_a?(RubyLLM::Content)
247
+ attachments_to_persist = content.attachments if content.attachments.any?
248
+ content = content.text
249
+ elsif content.is_a?(Hash) || content.is_a?(Array)
250
+ content = content.to_json
251
+ end
251
252
 
252
- @message.update!(
253
- role: message.role,
254
- content: content,
255
- model_id: message.model_id,
256
- input_tokens: message.input_tokens,
257
- output_tokens: message.output_tokens
258
- )
259
- @message.write_attribute(@message.class.tool_call_foreign_key, tool_call_id) if tool_call_id
260
- @message.save!
253
+ @message.update!(
254
+ role: message.role,
255
+ content: content,
256
+ model_id: message.model_id,
257
+ input_tokens: message.input_tokens,
258
+ output_tokens: message.output_tokens
259
+ )
260
+ @message.write_attribute(@message.class.tool_call_foreign_key, tool_call_id) if tool_call_id
261
+ @message.save!
261
262
 
262
- persist_content(@message, attachments_to_persist) if attachments_to_persist
263
- persist_tool_calls(message.tool_calls) if message.tool_calls.present?
263
+ persist_content(@message, attachments_to_persist) if attachments_to_persist
264
+ persist_tool_calls(message.tool_calls) if message.tool_calls.present?
265
+ end
264
266
  end
265
- end
266
267
 
267
- def persist_tool_calls(tool_calls)
268
- tool_calls.each_value do |tool_call|
269
- attributes = tool_call.to_h
270
- attributes[:tool_call_id] = attributes.delete(:id)
271
- @message.tool_calls.create!(**attributes)
268
+ def persist_tool_calls(tool_calls)
269
+ tool_calls.each_value do |tool_call|
270
+ attributes = tool_call.to_h
271
+ attributes[:tool_call_id] = attributes.delete(:id)
272
+ @message.tool_calls.create!(**attributes)
273
+ end
272
274
  end
273
- end
274
275
 
275
- def find_tool_call_id(tool_call_id)
276
- self.class.tool_call_class.constantize.find_by(tool_call_id: tool_call_id)&.id
277
- end
276
+ def find_tool_call_id(tool_call_id)
277
+ self.class.tool_call_class.constantize.find_by(tool_call_id: tool_call_id)&.id
278
+ end
278
279
 
279
- def persist_content(message_record, attachments)
280
- return unless message_record.respond_to?(:attachments)
280
+ def persist_content(message_record, attachments)
281
+ return unless message_record.respond_to?(:attachments)
281
282
 
282
- attachables = prepare_for_active_storage(attachments)
283
- message_record.attachments.attach(attachables) if attachables.any?
284
- end
283
+ attachables = prepare_for_active_storage(attachments)
284
+ message_record.attachments.attach(attachables) if attachables.any?
285
+ end
285
286
 
286
- def prepare_for_active_storage(attachments)
287
- Utils.to_safe_array(attachments).filter_map do |attachment|
288
- case attachment
289
- when ActionDispatch::Http::UploadedFile, ActiveStorage::Blob
290
- attachment
291
- when ActiveStorage::Attached::One, ActiveStorage::Attached::Many
292
- attachment.blobs
293
- when Hash
294
- attachment.values.map { |v| prepare_for_active_storage(v) }
295
- else
296
- convert_to_active_storage_format(attachment)
297
- end
298
- end.flatten.compact
299
- end
287
+ def prepare_for_active_storage(attachments)
288
+ Utils.to_safe_array(attachments).filter_map do |attachment|
289
+ case attachment
290
+ when ActionDispatch::Http::UploadedFile, ActiveStorage::Blob
291
+ attachment
292
+ when ActiveStorage::Attached::One, ActiveStorage::Attached::Many
293
+ attachment.blobs
294
+ when Hash
295
+ attachment.values.map { |v| prepare_for_active_storage(v) }
296
+ else
297
+ convert_to_active_storage_format(attachment)
298
+ end
299
+ end.flatten.compact
300
+ end
300
301
 
301
- def convert_to_active_storage_format(source)
302
- return if source.blank?
302
+ def convert_to_active_storage_format(source)
303
+ return if source.blank?
303
304
 
304
- attachment = source.is_a?(RubyLLM::Attachment) ? source : RubyLLM::Attachment.new(source)
305
+ attachment = source.is_a?(RubyLLM::Attachment) ? source : RubyLLM::Attachment.new(source)
305
306
 
306
- {
307
- io: StringIO.new(attachment.content),
308
- filename: attachment.filename,
309
- content_type: attachment.mime_type
310
- }
311
- rescue StandardError => e
312
- RubyLLM.logger.warn "Failed to process attachment #{source}: #{e.message}"
313
- nil
307
+ {
308
+ io: StringIO.new(attachment.content),
309
+ filename: attachment.filename,
310
+ content_type: attachment.mime_type
311
+ }
312
+ rescue StandardError => e
313
+ RubyLLM.logger.warn "Failed to process attachment #{source}: #{e.message}"
314
+ nil
315
+ end
314
316
  end
315
- end
316
317
 
317
- # Methods mixed into message models.
318
- module MessageLegacyMethods
319
- extend ActiveSupport::Concern
318
+ # Methods mixed into message models.
319
+ module MessageLegacyMethods
320
+ extend ActiveSupport::Concern
320
321
 
321
- class_methods do
322
- attr_reader :chat_class, :tool_call_class, :chat_foreign_key, :tool_call_foreign_key
323
- end
324
-
325
- def to_llm
326
- RubyLLM::Message.new(
327
- role: role.to_sym,
328
- content: extract_content,
329
- tool_calls: extract_tool_calls,
330
- tool_call_id: extract_tool_call_id,
331
- input_tokens: input_tokens,
332
- output_tokens: output_tokens,
333
- model_id: model_id
334
- )
335
- end
322
+ class_methods do
323
+ attr_reader :chat_class, :tool_call_class, :chat_foreign_key, :tool_call_foreign_key
324
+ end
336
325
 
337
- private
326
+ def to_llm
327
+ RubyLLM::Message.new(
328
+ role: role.to_sym,
329
+ content: extract_content,
330
+ tool_calls: extract_tool_calls,
331
+ tool_call_id: extract_tool_call_id,
332
+ input_tokens: input_tokens,
333
+ output_tokens: output_tokens,
334
+ model_id: model_id
335
+ )
336
+ end
338
337
 
339
- def extract_tool_calls
340
- tool_calls.to_h do |tool_call|
341
- [
342
- tool_call.tool_call_id,
343
- RubyLLM::ToolCall.new(
344
- id: tool_call.tool_call_id,
345
- name: tool_call.name,
346
- arguments: tool_call.arguments
347
- )
348
- ]
338
+ private
339
+
340
+ def extract_tool_calls
341
+ tool_calls.to_h do |tool_call|
342
+ [
343
+ tool_call.tool_call_id,
344
+ RubyLLM::ToolCall.new(
345
+ id: tool_call.tool_call_id,
346
+ name: tool_call.name,
347
+ arguments: tool_call.arguments
348
+ )
349
+ ]
350
+ end
349
351
  end
350
- end
351
352
 
352
- def extract_tool_call_id
353
- parent_tool_call&.tool_call_id
354
- end
353
+ def extract_tool_call_id
354
+ parent_tool_call&.tool_call_id
355
+ end
355
356
 
356
- def extract_content
357
- return content unless respond_to?(:attachments) && attachments.attached?
357
+ def extract_content
358
+ return content unless respond_to?(:attachments) && attachments.attached?
358
359
 
359
- RubyLLM::Content.new(content).tap do |content_obj|
360
- @_tempfiles = []
360
+ RubyLLM::Content.new(content).tap do |content_obj|
361
+ @_tempfiles = []
361
362
 
362
- attachments.each do |attachment|
363
- tempfile = download_attachment(attachment)
364
- content_obj.add_attachment(tempfile, filename: attachment.filename.to_s)
363
+ attachments.each do |attachment|
364
+ tempfile = download_attachment(attachment)
365
+ content_obj.add_attachment(tempfile, filename: attachment.filename.to_s)
366
+ end
365
367
  end
366
368
  end
367
- end
368
369
 
369
- def download_attachment(attachment)
370
- ext = File.extname(attachment.filename.to_s)
371
- basename = File.basename(attachment.filename.to_s, ext)
372
- tempfile = Tempfile.new([basename, ext])
373
- tempfile.binmode
370
+ def download_attachment(attachment)
371
+ ext = File.extname(attachment.filename.to_s)
372
+ basename = File.basename(attachment.filename.to_s, ext)
373
+ tempfile = Tempfile.new([basename, ext])
374
+ tempfile.binmode
374
375
 
375
- attachment.download { |chunk| tempfile.write(chunk) }
376
+ attachment.download { |chunk| tempfile.write(chunk) }
376
377
 
377
- tempfile.flush
378
- tempfile.rewind
379
- @_tempfiles << tempfile
380
- tempfile
378
+ tempfile.flush
379
+ tempfile.rewind
380
+ @_tempfiles << tempfile
381
+ tempfile
382
+ end
381
383
  end
382
384
  end
383
385
  end