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,349 +1,351 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module RubyLLM
4
- module ActiveRecord
5
- # Methods mixed into chat models.
6
- module ChatMethods
7
- extend ActiveSupport::Concern
8
-
9
- included do
10
- before_save :resolve_model_from_strings
11
- end
3
+ if defined?(ActiveRecord::Base)
4
+ module RubyLLM
5
+ module ActiveRecord
6
+ # Methods mixed into chat models.
7
+ module ChatMethods
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ before_save :resolve_model_from_strings
12
+ end
12
13
 
13
- attr_accessor :assume_model_exists, :context
14
+ attr_accessor :assume_model_exists, :context
14
15
 
15
- def model=(value)
16
- @model_string = value if value.is_a?(String)
17
- return if value.is_a?(String)
16
+ def model=(value)
17
+ @model_string = value if value.is_a?(String)
18
+ return if value.is_a?(String)
18
19
 
19
- if self.class.model_association_name == :model
20
- super
21
- else
22
- self.model_association = value
20
+ if self.class.model_association_name == :model
21
+ super
22
+ else
23
+ self.model_association = value
24
+ end
23
25
  end
24
- end
25
26
 
26
- def model_id=(value)
27
- @model_string = value
28
- end
27
+ def model_id=(value)
28
+ @model_string = value
29
+ end
29
30
 
30
- def model_id
31
- model_association&.model_id
32
- end
31
+ def model_id
32
+ model_association&.model_id
33
+ end
33
34
 
34
- def provider=(value)
35
- @provider_string = value
36
- end
35
+ def provider=(value)
36
+ @provider_string = value
37
+ end
37
38
 
38
- def provider
39
- model_association&.provider
40
- end
39
+ def provider
40
+ model_association&.provider
41
+ end
41
42
 
42
- private
43
-
44
- def resolve_model_from_strings # rubocop:disable Metrics/PerceivedComplexity
45
- config = context&.config || RubyLLM.config
46
- @model_string ||= config.default_model unless model_association
47
- return unless @model_string
48
-
49
- model_info, _provider = Models.resolve(
50
- @model_string,
51
- provider: @provider_string,
52
- assume_exists: assume_model_exists || false,
53
- config: config
54
- )
55
-
56
- model_class = self.class.model_class.constantize
57
- model_record = model_class.find_or_create_by!(
58
- model_id: model_info.id,
59
- provider: model_info.provider
60
- ) do |m|
61
- m.name = model_info.name || model_info.id
62
- m.family = model_info.family
63
- m.context_window = model_info.context_window
64
- m.max_output_tokens = model_info.max_output_tokens
65
- m.capabilities = model_info.capabilities || []
66
- m.modalities = model_info.modalities || {}
67
- m.pricing = model_info.pricing || {}
68
- m.metadata = model_info.metadata || {}
69
- end
70
-
71
- self.model_association = model_record
72
- @model_string = nil
73
- @provider_string = nil
74
- end
43
+ private
44
+
45
+ def resolve_model_from_strings # rubocop:disable Metrics/PerceivedComplexity
46
+ config = context&.config || RubyLLM.config
47
+ @model_string ||= config.default_model unless model_association
48
+ return unless @model_string
49
+
50
+ model_info, _provider = Models.resolve(
51
+ @model_string,
52
+ provider: @provider_string,
53
+ assume_exists: assume_model_exists || false,
54
+ config: config
55
+ )
56
+
57
+ model_class = self.class.model_class.constantize
58
+ model_record = model_class.find_or_create_by!(
59
+ model_id: model_info.id,
60
+ provider: model_info.provider
61
+ ) do |m|
62
+ m.name = model_info.name || model_info.id
63
+ m.family = model_info.family
64
+ m.context_window = model_info.context_window
65
+ m.max_output_tokens = model_info.max_output_tokens
66
+ m.capabilities = model_info.capabilities || []
67
+ m.modalities = model_info.modalities || {}
68
+ m.pricing = model_info.pricing || {}
69
+ m.metadata = model_info.metadata || {}
70
+ end
75
71
 
76
- public
72
+ self.model_association = model_record
73
+ @model_string = nil
74
+ @provider_string = nil
75
+ end
77
76
 
78
- def to_llm
79
- model_record = model_association
80
- @chat ||= (context || RubyLLM).chat(
81
- model: model_record.model_id,
82
- provider: model_record.provider.to_sym
83
- )
84
- @chat.reset_messages!
77
+ public
85
78
 
86
- messages_association.each do |msg|
87
- @chat.add_message(msg.to_llm)
88
- end
79
+ def to_llm
80
+ model_record = model_association
81
+ @chat ||= (context || RubyLLM).chat(
82
+ model: model_record.model_id,
83
+ provider: model_record.provider.to_sym
84
+ )
85
+ @chat.reset_messages!
89
86
 
90
- setup_persistence_callbacks
91
- end
87
+ messages_association.each do |msg|
88
+ @chat.add_message(msg.to_llm)
89
+ end
92
90
 
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)
91
+ setup_persistence_callbacks
97
92
  end
98
- to_llm.with_instructions(instructions)
99
- self
100
- end
101
93
 
102
- def with_tool(...)
103
- to_llm.with_tool(...)
104
- self
105
- end
94
+ def with_instructions(instructions, replace: false)
95
+ transaction do
96
+ messages_association.where(role: :system).destroy_all if replace
97
+ messages_association.create!(role: :system, content: instructions)
98
+ end
99
+ to_llm.with_instructions(instructions)
100
+ self
101
+ end
106
102
 
107
- def with_tools(...)
108
- to_llm.with_tools(...)
109
- self
110
- end
103
+ def with_tool(...)
104
+ to_llm.with_tool(...)
105
+ self
106
+ end
111
107
 
112
- def with_model(model_name, provider: nil, assume_exists: false)
113
- self.model = model_name
114
- self.provider = provider if provider
115
- self.assume_model_exists = assume_exists
116
- resolve_model_from_strings
117
- save!
118
- to_llm.with_model(model.model_id, provider: model.provider.to_sym, assume_exists:)
119
- self
120
- end
108
+ def with_tools(...)
109
+ to_llm.with_tools(...)
110
+ self
111
+ end
121
112
 
122
- def with_temperature(...)
123
- to_llm.with_temperature(...)
124
- self
125
- end
113
+ def with_model(model_name, provider: nil, assume_exists: false)
114
+ self.model = model_name
115
+ self.provider = provider if provider
116
+ self.assume_model_exists = assume_exists
117
+ resolve_model_from_strings
118
+ save!
119
+ to_llm.with_model(model.model_id, provider: model.provider.to_sym, assume_exists:)
120
+ self
121
+ end
126
122
 
127
- def with_params(...)
128
- to_llm.with_params(...)
129
- self
130
- end
123
+ def with_temperature(...)
124
+ to_llm.with_temperature(...)
125
+ self
126
+ end
131
127
 
132
- def with_headers(...)
133
- to_llm.with_headers(...)
134
- self
135
- end
128
+ def with_params(...)
129
+ to_llm.with_params(...)
130
+ self
131
+ end
136
132
 
137
- def with_schema(...)
138
- to_llm.with_schema(...)
139
- self
140
- end
133
+ def with_headers(...)
134
+ to_llm.with_headers(...)
135
+ self
136
+ end
141
137
 
142
- def on_new_message(&)
143
- to_llm.on_new_message(&)
144
- self
145
- end
138
+ def with_schema(...)
139
+ to_llm.with_schema(...)
140
+ self
141
+ end
146
142
 
147
- def on_end_message(&)
148
- to_llm.on_end_message(&)
149
- self
150
- end
143
+ def on_new_message(&)
144
+ to_llm.on_new_message(&)
145
+ self
146
+ end
151
147
 
152
- def on_tool_call(...)
153
- to_llm.on_tool_call(...)
154
- self
155
- end
148
+ def on_end_message(&)
149
+ to_llm.on_end_message(&)
150
+ self
151
+ end
156
152
 
157
- def on_tool_result(...)
158
- to_llm.on_tool_result(...)
159
- self
160
- end
153
+ def on_tool_call(...)
154
+ to_llm.on_tool_call(...)
155
+ self
156
+ end
161
157
 
162
- def create_user_message(content, with: nil)
163
- content_text, attachments, content_raw = prepare_content_for_storage(content)
158
+ def on_tool_result(...)
159
+ to_llm.on_tool_result(...)
160
+ self
161
+ end
164
162
 
165
- message_record = messages_association.build(role: :user)
166
- message_record.content = content_text
167
- message_record.content_raw = content_raw if message_record.respond_to?(:content_raw=)
168
- message_record.save!
163
+ def create_user_message(content, with: nil)
164
+ content_text, attachments, content_raw = prepare_content_for_storage(content)
169
165
 
170
- persist_content(message_record, with) if with.present?
171
- persist_content(message_record, attachments) if attachments.present?
166
+ message_record = messages_association.build(role: :user)
167
+ message_record.content = content_text
168
+ message_record.content_raw = content_raw if message_record.respond_to?(:content_raw=)
169
+ message_record.save!
172
170
 
173
- message_record
174
- end
171
+ persist_content(message_record, with) if with.present?
172
+ persist_content(message_record, attachments) if attachments.present?
175
173
 
176
- def ask(message, with: nil, &)
177
- create_user_message(message, with:)
178
- complete(&)
179
- end
174
+ message_record
175
+ end
180
176
 
181
- alias say ask
177
+ def ask(message, with: nil, &)
178
+ create_user_message(message, with:)
179
+ complete(&)
180
+ end
182
181
 
183
- def complete(...)
184
- to_llm.complete(...)
185
- rescue RubyLLM::Error => e
186
- cleanup_failed_messages if @message&.persisted? && @message.content.blank?
187
- cleanup_orphaned_tool_results
188
- raise e
189
- end
182
+ alias say ask
190
183
 
191
- private
184
+ def complete(...)
185
+ to_llm.complete(...)
186
+ rescue RubyLLM::Error => e
187
+ cleanup_failed_messages if @message&.persisted? && @message.content.blank?
188
+ cleanup_orphaned_tool_results
189
+ raise e
190
+ end
192
191
 
193
- def cleanup_failed_messages
194
- RubyLLM.logger.warn "RubyLLM: API call failed, destroying message: #{@message.id}"
195
- @message.destroy
196
- end
192
+ private
197
193
 
198
- def cleanup_orphaned_tool_results # rubocop:disable Metrics/PerceivedComplexity
199
- messages_association.reload
200
- last = messages_association.order(:id).last
194
+ def cleanup_failed_messages
195
+ RubyLLM.logger.warn "RubyLLM: API call failed, destroying message: #{@message.id}"
196
+ @message.destroy
197
+ end
201
198
 
202
- return unless last&.tool_call? || last&.tool_result?
199
+ def cleanup_orphaned_tool_results # rubocop:disable Metrics/PerceivedComplexity
200
+ messages_association.reload
201
+ last = messages_association.order(:id).last
203
202
 
204
- if last.tool_call?
205
- last.destroy
206
- elsif last.tool_result?
207
- tool_call_message = last.parent_tool_call.message
208
- expected_results = tool_call_message.tool_calls.pluck(:id)
209
- actual_results = tool_call_message.tool_results.pluck(:tool_call_id)
203
+ return unless last&.tool_call? || last&.tool_result?
210
204
 
211
- if expected_results.sort != actual_results.sort
212
- tool_call_message.tool_results.each(&:destroy)
213
- tool_call_message.destroy
205
+ if last.tool_call?
206
+ last.destroy
207
+ elsif last.tool_result?
208
+ tool_call_message = last.parent_tool_call.message
209
+ expected_results = tool_call_message.tool_calls.pluck(:id)
210
+ actual_results = tool_call_message.tool_results.pluck(:tool_call_id)
211
+
212
+ if expected_results.sort != actual_results.sort
213
+ tool_call_message.tool_results.each(&:destroy)
214
+ tool_call_message.destroy
215
+ end
214
216
  end
215
217
  end
216
- end
217
218
 
218
- def setup_persistence_callbacks
219
- return @chat if @chat.instance_variable_get(:@_persistence_callbacks_setup)
219
+ def setup_persistence_callbacks
220
+ return @chat if @chat.instance_variable_get(:@_persistence_callbacks_setup)
220
221
 
221
- @chat.on_new_message { persist_new_message }
222
- @chat.on_end_message { |msg| persist_message_completion(msg) }
222
+ @chat.on_new_message { persist_new_message }
223
+ @chat.on_end_message { |msg| persist_message_completion(msg) }
223
224
 
224
- @chat.instance_variable_set(:@_persistence_callbacks_setup, true)
225
- @chat
226
- end
225
+ @chat.instance_variable_set(:@_persistence_callbacks_setup, true)
226
+ @chat
227
+ end
227
228
 
228
- def persist_new_message
229
- @message = messages_association.create!(role: :assistant, content: '')
230
- end
229
+ def persist_new_message
230
+ @message = messages_association.create!(role: :assistant, content: '')
231
+ end
231
232
 
232
- # rubocop:disable Metrics/PerceivedComplexity
233
- def persist_message_completion(message)
234
- return unless message
233
+ # rubocop:disable Metrics/PerceivedComplexity
234
+ def persist_message_completion(message)
235
+ return unless message
235
236
 
236
- tool_call_id = find_tool_call_id(message.tool_call_id) if message.tool_call_id
237
+ tool_call_id = find_tool_call_id(message.tool_call_id) if message.tool_call_id
237
238
 
238
- transaction do
239
- content_text, attachments_to_persist, content_raw = prepare_content_for_storage(message.content)
239
+ transaction do
240
+ content_text, attachments_to_persist, content_raw = prepare_content_for_storage(message.content)
240
241
 
241
- attrs = {
242
- role: message.role,
243
- content: content_text,
244
- input_tokens: message.input_tokens,
245
- output_tokens: message.output_tokens
246
- }
247
- attrs[:cached_tokens] = message.cached_tokens if @message.has_attribute?(:cached_tokens)
248
- if @message.has_attribute?(:cache_creation_tokens)
249
- attrs[:cache_creation_tokens] = message.cache_creation_tokens
250
- end
242
+ attrs = {
243
+ role: message.role,
244
+ content: content_text,
245
+ input_tokens: message.input_tokens,
246
+ output_tokens: message.output_tokens
247
+ }
248
+ attrs[:cached_tokens] = message.cached_tokens if @message.has_attribute?(:cached_tokens)
249
+ if @message.has_attribute?(:cache_creation_tokens)
250
+ attrs[:cache_creation_tokens] = message.cache_creation_tokens
251
+ end
251
252
 
252
- # Add model association dynamically
253
- attrs[self.class.model_association_name] = model_association
253
+ # Add model association dynamically
254
+ attrs[self.class.model_association_name] = model_association
254
255
 
255
- if tool_call_id
256
- parent_tool_call_assoc = @message.class.reflect_on_association(:parent_tool_call)
257
- attrs[parent_tool_call_assoc.foreign_key] = tool_call_id
258
- end
256
+ if tool_call_id
257
+ parent_tool_call_assoc = @message.class.reflect_on_association(:parent_tool_call)
258
+ attrs[parent_tool_call_assoc.foreign_key] = tool_call_id
259
+ end
259
260
 
260
- @message.assign_attributes(attrs)
261
- @message.content_raw = content_raw if @message.respond_to?(:content_raw=)
262
- @message.save!
261
+ @message.assign_attributes(attrs)
262
+ @message.content_raw = content_raw if @message.respond_to?(:content_raw=)
263
+ @message.save!
263
264
 
264
- persist_content(@message, attachments_to_persist) if attachments_to_persist
265
- persist_tool_calls(message.tool_calls) if message.tool_calls.present?
265
+ persist_content(@message, attachments_to_persist) if attachments_to_persist
266
+ persist_tool_calls(message.tool_calls) if message.tool_calls.present?
267
+ end
266
268
  end
267
- end
268
- # rubocop:enable Metrics/PerceivedComplexity
269
+ # rubocop:enable Metrics/PerceivedComplexity
269
270
 
270
- def persist_tool_calls(tool_calls)
271
- tool_calls.each_value do |tool_call|
272
- attributes = tool_call.to_h
273
- attributes[:tool_call_id] = attributes.delete(:id)
274
- @message.tool_calls_association.create!(**attributes)
271
+ def persist_tool_calls(tool_calls)
272
+ tool_calls.each_value do |tool_call|
273
+ attributes = tool_call.to_h
274
+ attributes[:tool_call_id] = attributes.delete(:id)
275
+ @message.tool_calls_association.create!(**attributes)
276
+ end
275
277
  end
276
- end
277
278
 
278
- def find_tool_call_id(tool_call_id)
279
- messages = messages_association
280
- message_class = messages.klass
281
- tool_calls_assoc = message_class.tool_calls_association_name
282
- tool_call_table_name = message_class.reflect_on_association(tool_calls_assoc).table_name
279
+ def find_tool_call_id(tool_call_id)
280
+ messages = messages_association
281
+ message_class = messages.klass
282
+ tool_calls_assoc = message_class.tool_calls_association_name
283
+ tool_call_table_name = message_class.reflect_on_association(tool_calls_assoc).table_name
283
284
 
284
- message_with_tool_call = messages.joins(tool_calls_assoc)
285
- .find_by(tool_call_table_name => { tool_call_id: tool_call_id })
286
- return nil unless message_with_tool_call
285
+ message_with_tool_call = messages.joins(tool_calls_assoc)
286
+ .find_by(tool_call_table_name => { tool_call_id: tool_call_id })
287
+ return nil unless message_with_tool_call
287
288
 
288
- tool_call = message_with_tool_call.tool_calls_association.find_by(tool_call_id: tool_call_id)
289
- tool_call&.id
290
- end
289
+ tool_call = message_with_tool_call.tool_calls_association.find_by(tool_call_id: tool_call_id)
290
+ tool_call&.id
291
+ end
291
292
 
292
- def persist_content(message_record, attachments)
293
- return unless message_record.respond_to?(:attachments)
293
+ def persist_content(message_record, attachments)
294
+ return unless message_record.respond_to?(:attachments)
294
295
 
295
- attachables = prepare_for_active_storage(attachments)
296
- message_record.attachments.attach(attachables) if attachables.any?
297
- end
296
+ attachables = prepare_for_active_storage(attachments)
297
+ message_record.attachments.attach(attachables) if attachables.any?
298
+ end
298
299
 
299
- def prepare_for_active_storage(attachments)
300
- Utils.to_safe_array(attachments).filter_map do |attachment|
301
- case attachment
302
- when ActionDispatch::Http::UploadedFile, ActiveStorage::Blob
303
- attachment
304
- when ActiveStorage::Attached::One, ActiveStorage::Attached::Many
305
- attachment.blobs
306
- when Hash
307
- attachment.values.map { |v| prepare_for_active_storage(v) }
308
- else
309
- convert_to_active_storage_format(attachment)
310
- end
311
- end.flatten.compact
312
- end
300
+ def prepare_for_active_storage(attachments)
301
+ Utils.to_safe_array(attachments).filter_map do |attachment|
302
+ case attachment
303
+ when ActionDispatch::Http::UploadedFile, ActiveStorage::Blob
304
+ attachment
305
+ when ActiveStorage::Attached::One, ActiveStorage::Attached::Many
306
+ attachment.blobs
307
+ when Hash
308
+ attachment.values.map { |v| prepare_for_active_storage(v) }
309
+ else
310
+ convert_to_active_storage_format(attachment)
311
+ end
312
+ end.flatten.compact
313
+ end
313
314
 
314
- def convert_to_active_storage_format(source)
315
- return if source.blank?
315
+ def convert_to_active_storage_format(source)
316
+ return if source.blank?
316
317
 
317
- attachment = source.is_a?(RubyLLM::Attachment) ? source : RubyLLM::Attachment.new(source)
318
+ attachment = source.is_a?(RubyLLM::Attachment) ? source : RubyLLM::Attachment.new(source)
318
319
 
319
- {
320
- io: StringIO.new(attachment.content),
321
- filename: attachment.filename,
322
- content_type: attachment.mime_type
323
- }
324
- rescue StandardError => e
325
- RubyLLM.logger.warn "Failed to process attachment #{source}: #{e.message}"
326
- nil
327
- end
320
+ {
321
+ io: StringIO.new(attachment.content),
322
+ filename: attachment.filename,
323
+ content_type: attachment.mime_type
324
+ }
325
+ rescue StandardError => e
326
+ RubyLLM.logger.warn "Failed to process attachment #{source}: #{e.message}"
327
+ nil
328
+ end
328
329
 
329
- def prepare_content_for_storage(content)
330
- attachments = nil
331
- content_raw = nil
332
- content_text = content
330
+ def prepare_content_for_storage(content)
331
+ attachments = nil
332
+ content_raw = nil
333
+ content_text = content
334
+
335
+ case content
336
+ when RubyLLM::Content::Raw
337
+ content_raw = content.value
338
+ content_text = nil
339
+ when RubyLLM::Content
340
+ attachments = content.attachments if content.attachments.any?
341
+ content_text = content.text
342
+ when Hash, Array
343
+ content_raw = content
344
+ content_text = nil
345
+ end
333
346
 
334
- case content
335
- when RubyLLM::Content::Raw
336
- content_raw = content.value
337
- content_text = nil
338
- when RubyLLM::Content
339
- attachments = content.attachments if content.attachments.any?
340
- content_text = content.text
341
- when Hash, Array
342
- content_raw = content
343
- content_text = nil
347
+ [content_text, attachments, content_raw]
344
348
  end
345
-
346
- [content_text, attachments, content_raw]
347
349
  end
348
350
  end
349
351
  end