ruby_llm 1.15.0 → 1.16.0
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 +5 -4
- data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +1 -1
- data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +3 -3
- data/lib/ruby_llm/active_record/acts_as.rb +1 -26
- data/lib/ruby_llm/active_record/acts_as_legacy.rb +71 -4
- data/lib/ruby_llm/active_record/chat_methods.rb +2 -2
- data/lib/ruby_llm/active_record/message_methods.rb +70 -3
- data/lib/ruby_llm/agent.rb +1 -0
- data/lib/ruby_llm/aliases.json +78 -75
- data/lib/ruby_llm/aliases.rb +3 -0
- data/lib/ruby_llm/attachment.rb +34 -17
- data/lib/ruby_llm/chat.rb +176 -47
- data/lib/ruby_llm/configuration.rb +14 -1
- data/lib/ruby_llm/connection.rb +36 -7
- data/lib/ruby_llm/content.rb +15 -1
- data/lib/ruby_llm/deprecator.rb +24 -0
- data/lib/ruby_llm/embedding.rb +31 -1
- data/lib/ruby_llm/error.rb +11 -75
- data/lib/ruby_llm/error_middleware.rb +81 -0
- data/lib/ruby_llm/image.rb +2 -0
- data/lib/ruby_llm/instrumentation.rb +36 -0
- data/lib/ruby_llm/mime_type.rb +25 -0
- data/lib/ruby_llm/model/info.rb +36 -2
- data/lib/ruby_llm/model/pricing.rb +19 -9
- data/lib/ruby_llm/model/pricing_tier.rb +20 -9
- data/lib/ruby_llm/model_registry.rb +39 -0
- data/lib/ruby_llm/models.json +18225 -19144
- data/lib/ruby_llm/models.rb +95 -30
- data/lib/ruby_llm/provider.rb +11 -2
- data/lib/ruby_llm/providers/anthropic/chat.rb +49 -15
- data/lib/ruby_llm/providers/anthropic/models.rb +2 -0
- data/lib/ruby_llm/providers/anthropic/streaming.rb +2 -0
- data/lib/ruby_llm/providers/anthropic/tools.rb +28 -2
- data/lib/ruby_llm/providers/azure/media.rb +1 -1
- data/lib/ruby_llm/providers/bedrock/auth.rb +1 -0
- data/lib/ruby_llm/providers/bedrock/chat.rb +2 -0
- data/lib/ruby_llm/providers/bedrock/media.rb +21 -3
- data/lib/ruby_llm/providers/bedrock/models.rb +1 -1
- data/lib/ruby_llm/providers/bedrock/streaming.rb +6 -0
- data/lib/ruby_llm/providers/bedrock.rb +2 -2
- data/lib/ruby_llm/providers/deepseek/capabilities.rb +43 -0
- data/lib/ruby_llm/providers/deepseek/chat.rb +9 -0
- data/lib/ruby_llm/providers/gemini/chat.rb +2 -3
- data/lib/ruby_llm/providers/gemini/media.rb +16 -9
- data/lib/ruby_llm/providers/gemini/streaming.rb +2 -0
- data/lib/ruby_llm/providers/gemini/tools.rb +2 -0
- data/lib/ruby_llm/providers/gpustack/chat.rb +8 -1
- data/lib/ruby_llm/providers/gpustack/models.rb +2 -0
- data/lib/ruby_llm/providers/mistral/capabilities.rb +1 -1
- data/lib/ruby_llm/providers/mistral/chat.rb +1 -1
- data/lib/ruby_llm/providers/mistral/media.rb +55 -0
- data/lib/ruby_llm/providers/mistral/models.rb +2 -0
- data/lib/ruby_llm/providers/mistral.rb +2 -2
- data/lib/ruby_llm/providers/ollama/chat.rb +8 -1
- data/lib/ruby_llm/providers/openai/chat.rb +16 -1
- data/lib/ruby_llm/providers/openai/images.rb +9 -9
- data/lib/ruby_llm/providers/openai/media.rb +40 -16
- data/lib/ruby_llm/providers/openai/streaming.rb +2 -0
- data/lib/ruby_llm/providers/openai/tools.rb +2 -0
- data/lib/ruby_llm/providers/openai/transcription.rb +1 -0
- data/lib/ruby_llm/providers/openrouter/chat.rb +6 -2
- data/lib/ruby_llm/providers/perplexity/chat.rb +11 -0
- data/lib/ruby_llm/providers/perplexity/media.rb +62 -0
- data/lib/ruby_llm/providers/perplexity.rb +2 -2
- data/lib/ruby_llm/providers/vertexai.rb +5 -1
- data/lib/ruby_llm/providers/xai/chat.rb +9 -0
- data/lib/ruby_llm/providers/xai/models.rb +15 -27
- data/lib/ruby_llm/providers/xai.rb +2 -2
- data/lib/ruby_llm/railtie.rb +5 -1
- data/lib/ruby_llm/stream_accumulator.rb +45 -30
- data/lib/ruby_llm/streaming.rb +4 -0
- data/lib/ruby_llm/tool_concurrency.rb +105 -0
- data/lib/ruby_llm/transcription.rb +2 -1
- data/lib/ruby_llm/utils.rb +39 -0
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/ruby_llm.rb +9 -2
- data/lib/tasks/models.rake +32 -4
- data/lib/tasks/release.rake +50 -23
- metadata +17 -10
data/lib/ruby_llm/attachment.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'base64'
|
|
3
4
|
require 'pathname'
|
|
4
5
|
require 'uri'
|
|
5
6
|
|
|
@@ -8,6 +9,16 @@ module RubyLLM
|
|
|
8
9
|
class Attachment
|
|
9
10
|
attr_reader :source, :filename, :mime_type
|
|
10
11
|
|
|
12
|
+
DOCUMENT_EXTENSIONS = %w[
|
|
13
|
+
doc docx dot key numbers odp ods odt pages pot pps ppt pptx rtf xls xlsx
|
|
14
|
+
].freeze
|
|
15
|
+
ACTIVE_STORAGE_CLASS_NAMES = %w[
|
|
16
|
+
ActiveStorage::Blob
|
|
17
|
+
ActiveStorage::Attachment
|
|
18
|
+
ActiveStorage::Attached::One
|
|
19
|
+
ActiveStorage::Attached::Many
|
|
20
|
+
].freeze
|
|
21
|
+
|
|
11
22
|
def initialize(source, filename: nil)
|
|
12
23
|
@source = source
|
|
13
24
|
@source = source_type_cast
|
|
@@ -29,12 +40,7 @@ module RubyLLM
|
|
|
29
40
|
end
|
|
30
41
|
|
|
31
42
|
def active_storage?
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
@source.is_a?(ActiveStorage::Blob) ||
|
|
35
|
-
@source.is_a?(ActiveStorage::Attachment) ||
|
|
36
|
-
@source.is_a?(ActiveStorage::Attached::One) ||
|
|
37
|
-
@source.is_a?(ActiveStorage::Attached::Many)
|
|
43
|
+
ACTIVE_STORAGE_CLASS_NAMES.any? { |class_name| source_is_a?(class_name) }
|
|
38
44
|
end
|
|
39
45
|
|
|
40
46
|
def content
|
|
@@ -83,6 +89,7 @@ module RubyLLM
|
|
|
83
89
|
return :audio if audio?
|
|
84
90
|
return :pdf if pdf?
|
|
85
91
|
return :text if text?
|
|
92
|
+
return :document if document?
|
|
86
93
|
|
|
87
94
|
:unknown
|
|
88
95
|
end
|
|
@@ -114,6 +121,17 @@ module RubyLLM
|
|
|
114
121
|
RubyLLM::MimeType.pdf? mime_type
|
|
115
122
|
end
|
|
116
123
|
|
|
124
|
+
def document?
|
|
125
|
+
return false if pdf? || text?
|
|
126
|
+
|
|
127
|
+
RubyLLM::MimeType.document?(mime_type) || DOCUMENT_EXTENSIONS.include?(extension)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def extension
|
|
131
|
+
extension = File.extname(filename.to_s).delete_prefix('.').downcase
|
|
132
|
+
extension.empty? ? nil : extension
|
|
133
|
+
end
|
|
134
|
+
|
|
117
135
|
def text?
|
|
118
136
|
RubyLLM::MimeType.text? mime_type
|
|
119
137
|
end
|
|
@@ -147,8 +165,6 @@ module RubyLLM
|
|
|
147
165
|
end
|
|
148
166
|
|
|
149
167
|
def load_content_from_active_storage
|
|
150
|
-
return unless defined?(ActiveStorage)
|
|
151
|
-
|
|
152
168
|
@content = active_storage_blob&.download
|
|
153
169
|
end
|
|
154
170
|
|
|
@@ -185,23 +201,24 @@ module RubyLLM
|
|
|
185
201
|
end
|
|
186
202
|
|
|
187
203
|
def extract_filename_from_active_storage
|
|
188
|
-
return 'attachment' unless defined?(ActiveStorage)
|
|
189
|
-
|
|
190
204
|
active_storage_blob&.filename&.to_s || 'attachment'
|
|
191
205
|
end
|
|
192
206
|
|
|
193
207
|
def active_storage_content_type
|
|
194
|
-
return unless defined?(ActiveStorage)
|
|
195
|
-
|
|
196
208
|
active_storage_blob&.content_type
|
|
197
209
|
end
|
|
198
210
|
|
|
199
211
|
def active_storage_blob
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
212
|
+
return @source if source_is_a?('ActiveStorage::Blob')
|
|
213
|
+
return @source.blob if source_is_a?('ActiveStorage::Attachment')
|
|
214
|
+
return @source.blob if source_is_a?('ActiveStorage::Attached::One')
|
|
215
|
+
|
|
216
|
+
@source.blobs.first if source_is_a?('ActiveStorage::Attached::Many')
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def source_is_a?(class_name)
|
|
220
|
+
klass = RubyLLM::Utils.safe_constantize(class_name)
|
|
221
|
+
klass ? @source.is_a?(klass) : false
|
|
205
222
|
end
|
|
206
223
|
end
|
|
207
224
|
end
|
data/lib/ruby_llm/chat.rb
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
3
5
|
module RubyLLM
|
|
4
6
|
# Represents a conversation with an AI model
|
|
5
7
|
class Chat
|
|
6
8
|
include Enumerable
|
|
7
9
|
|
|
8
|
-
attr_reader :model, :messages, :tools, :tool_prefs, :params, :headers, :schema
|
|
10
|
+
attr_reader :model, :messages, :tools, :tool_prefs, :params, :headers, :schema, :concurrency
|
|
9
11
|
|
|
10
12
|
def initialize(model: nil, provider: nil, assume_model_exists: false, context: nil)
|
|
11
13
|
if assume_model_exists && !provider
|
|
@@ -20,6 +22,7 @@ module RubyLLM
|
|
|
20
22
|
@messages = []
|
|
21
23
|
@tools = {}
|
|
22
24
|
@tool_prefs = { choice: nil, calls: nil }
|
|
25
|
+
@concurrency = normalize_tool_concurrency(@config.tool_concurrency)
|
|
23
26
|
@params = {}
|
|
24
27
|
@headers = {}
|
|
25
28
|
@schema = nil
|
|
@@ -52,19 +55,21 @@ module RubyLLM
|
|
|
52
55
|
self
|
|
53
56
|
end
|
|
54
57
|
|
|
55
|
-
def with_tool(tool, choice: nil, calls: nil)
|
|
58
|
+
def with_tool(tool, choice: nil, calls: nil, concurrency: @concurrency)
|
|
56
59
|
unless tool.nil?
|
|
57
60
|
tool_instance = tool.is_a?(Class) ? tool.new : tool
|
|
58
61
|
@tools[tool_instance.name.to_sym] = tool_instance
|
|
59
62
|
end
|
|
60
63
|
update_tool_options(choice:, calls:)
|
|
64
|
+
update_tool_concurrency(concurrency)
|
|
61
65
|
self
|
|
62
66
|
end
|
|
63
67
|
|
|
64
|
-
def with_tools(*tools, replace: false, choice: nil, calls: nil)
|
|
68
|
+
def with_tools(*tools, replace: false, choice: nil, calls: nil, concurrency: @concurrency)
|
|
65
69
|
@tools.clear if replace
|
|
66
70
|
tools.compact.each { |tool| with_tool tool }
|
|
67
71
|
update_tool_options(choice:, calls:)
|
|
72
|
+
update_tool_concurrency(concurrency)
|
|
68
73
|
self
|
|
69
74
|
end
|
|
70
75
|
|
|
@@ -150,41 +155,11 @@ module RubyLLM
|
|
|
150
155
|
end
|
|
151
156
|
|
|
152
157
|
def cost
|
|
153
|
-
Cost.aggregate(messages.map(
|
|
158
|
+
Cost.aggregate(messages.map { |message| message.cost(model: message.model_info || model) })
|
|
154
159
|
end
|
|
155
160
|
|
|
156
161
|
def complete(&)
|
|
157
|
-
|
|
158
|
-
messages,
|
|
159
|
-
tools: @tools,
|
|
160
|
-
tool_prefs: @tool_prefs,
|
|
161
|
-
temperature: @temperature,
|
|
162
|
-
model: @model,
|
|
163
|
-
params: @params,
|
|
164
|
-
headers: @headers,
|
|
165
|
-
schema: @schema,
|
|
166
|
-
thinking: @thinking,
|
|
167
|
-
&wrap_streaming_block(&)
|
|
168
|
-
)
|
|
169
|
-
|
|
170
|
-
run_callbacks(:before_message, :new_message) unless block_given?
|
|
171
|
-
|
|
172
|
-
if @schema && response.content.is_a?(String) && !response.tool_call?
|
|
173
|
-
begin
|
|
174
|
-
response.content = JSON.parse(response.content)
|
|
175
|
-
rescue JSON::ParserError
|
|
176
|
-
# If parsing fails, keep content as string
|
|
177
|
-
end
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
add_message response
|
|
181
|
-
run_callbacks(:after_message, :end_message, response)
|
|
182
|
-
|
|
183
|
-
if response.tool_call?
|
|
184
|
-
handle_tool_calls(response, &)
|
|
185
|
-
else
|
|
186
|
-
response
|
|
187
|
-
end
|
|
162
|
+
instrument_completion(&)
|
|
188
163
|
end
|
|
189
164
|
|
|
190
165
|
def add_message(message_or_attributes)
|
|
@@ -193,6 +168,7 @@ module RubyLLM
|
|
|
193
168
|
message
|
|
194
169
|
end
|
|
195
170
|
|
|
171
|
+
# Mutates this chat by removing all in-memory messages.
|
|
196
172
|
def reset_messages!
|
|
197
173
|
@messages.clear
|
|
198
174
|
end
|
|
@@ -243,6 +219,87 @@ module RubyLLM
|
|
|
243
219
|
self
|
|
244
220
|
end
|
|
245
221
|
|
|
222
|
+
def complete_once(&)
|
|
223
|
+
response = provider_completion(&)
|
|
224
|
+
|
|
225
|
+
run_callbacks(:before_message, :new_message) unless block_given?
|
|
226
|
+
|
|
227
|
+
normalize_schema_response(response)
|
|
228
|
+
|
|
229
|
+
add_message response
|
|
230
|
+
run_callbacks(:after_message, :end_message, response)
|
|
231
|
+
|
|
232
|
+
if response.tool_call?
|
|
233
|
+
handle_tool_calls(response, &)
|
|
234
|
+
else
|
|
235
|
+
response
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def instrument_completion(&block)
|
|
240
|
+
result = nil
|
|
241
|
+
streaming = block_given?
|
|
242
|
+
payload = {
|
|
243
|
+
chat: self,
|
|
244
|
+
provider: @provider.slug,
|
|
245
|
+
provider_class: @provider.class.name,
|
|
246
|
+
model: @model.id,
|
|
247
|
+
model_info: @model,
|
|
248
|
+
input_messages: messages.dup,
|
|
249
|
+
message_count: messages.size,
|
|
250
|
+
tools: tools.keys,
|
|
251
|
+
tool_choice: tool_prefs[:choice],
|
|
252
|
+
tool_call_limit: tool_prefs[:calls],
|
|
253
|
+
temperature: @temperature,
|
|
254
|
+
params: params,
|
|
255
|
+
schema: schema,
|
|
256
|
+
thinking: @thinking,
|
|
257
|
+
streaming: streaming
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
RubyLLM.instrument('chat.ruby_llm', payload, config: @config) do |event|
|
|
261
|
+
result = complete_once(&block)
|
|
262
|
+
event[:response] = result
|
|
263
|
+
event[:messages_after] = messages.dup
|
|
264
|
+
event[:response_role] = result.role if result.respond_to?(:role)
|
|
265
|
+
|
|
266
|
+
if result.respond_to?(:tool_call?)
|
|
267
|
+
event[:response_model] = result.model_id
|
|
268
|
+
event[:tool_call] = result.tool_call?
|
|
269
|
+
event[:tool_calls] = result.tool_calls
|
|
270
|
+
event[:input_tokens] = result.input_tokens
|
|
271
|
+
event[:output_tokens] = result.output_tokens
|
|
272
|
+
event[:cached_tokens] = result.cached_tokens
|
|
273
|
+
event[:cache_creation_tokens] = result.cache_creation_tokens
|
|
274
|
+
event[:thinking_tokens] = result.thinking_tokens
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
result
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def provider_completion(&)
|
|
281
|
+
@provider.complete(
|
|
282
|
+
messages,
|
|
283
|
+
tools: @tools,
|
|
284
|
+
tool_prefs: @tool_prefs,
|
|
285
|
+
temperature: @temperature,
|
|
286
|
+
model: @model,
|
|
287
|
+
params: @params,
|
|
288
|
+
headers: @headers,
|
|
289
|
+
schema: @schema,
|
|
290
|
+
thinking: @thinking,
|
|
291
|
+
&wrap_streaming_block(&)
|
|
292
|
+
)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def normalize_schema_response(response)
|
|
296
|
+
return unless @schema && response.content.is_a?(String) && !response.tool_call?
|
|
297
|
+
|
|
298
|
+
response.content = JSON.parse(response.content)
|
|
299
|
+
rescue JSON::ParserError
|
|
300
|
+
# If parsing fails, keep content as string.
|
|
301
|
+
end
|
|
302
|
+
|
|
246
303
|
def set_legacy_callback(name, legacy_name, additive_name, &block)
|
|
247
304
|
warn_legacy_callback_deprecation(legacy_name, additive_name) if block
|
|
248
305
|
|
|
@@ -251,7 +308,7 @@ module RubyLLM
|
|
|
251
308
|
end
|
|
252
309
|
|
|
253
310
|
def warn_legacy_callback_deprecation(legacy_name, additive_name)
|
|
254
|
-
RubyLLM.
|
|
311
|
+
RubyLLM.deprecator.warn(
|
|
255
312
|
"`#{legacy_name}` is deprecated and will be removed in RubyLLM 2.0. " \
|
|
256
313
|
"Use `#{additive_name}` instead."
|
|
257
314
|
)
|
|
@@ -273,23 +330,60 @@ module RubyLLM
|
|
|
273
330
|
end
|
|
274
331
|
|
|
275
332
|
def handle_tool_calls(response, &)
|
|
333
|
+
halt_result = if concurrency
|
|
334
|
+
handle_concurrent_tool_calls(response.tool_calls)
|
|
335
|
+
else
|
|
336
|
+
handle_sequential_tool_calls(response.tool_calls)
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
reset_tool_choice if forced_tool_choice?
|
|
340
|
+
halt_result || complete(&)
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def handle_sequential_tool_calls(tool_calls)
|
|
276
344
|
halt_result = nil
|
|
277
345
|
|
|
278
|
-
|
|
346
|
+
tool_calls.each_value do |tool_call|
|
|
279
347
|
run_callbacks(:before_message, :new_message)
|
|
280
|
-
|
|
281
|
-
result
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
content = content_like?(tool_payload) ? tool_payload : tool_payload.to_s
|
|
285
|
-
message = add_message role: :tool, content:, tool_call_id: tool_call.id
|
|
286
|
-
run_callbacks(:after_message, :end_message, message)
|
|
348
|
+
result = execute_tool_with_callbacks(tool_call)
|
|
349
|
+
add_tool_result_message(tool_call, result)
|
|
350
|
+
halt_result = result if result.is_a?(Tool::Halt)
|
|
351
|
+
end
|
|
287
352
|
|
|
353
|
+
halt_result
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def handle_concurrent_tool_calls(tool_calls)
|
|
357
|
+
halt_result = nil
|
|
358
|
+
|
|
359
|
+
execute_tools_concurrently(tool_calls) do |tool_call, result|
|
|
360
|
+
run_callbacks(:before_message, :new_message)
|
|
361
|
+
add_tool_result_message(tool_call, result)
|
|
288
362
|
halt_result = result if result.is_a?(Tool::Halt)
|
|
289
363
|
end
|
|
290
364
|
|
|
291
|
-
|
|
292
|
-
|
|
365
|
+
halt_result
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def execute_tools_concurrently(tool_calls, &on_result)
|
|
369
|
+
ToolConcurrency.run(concurrency, tool_calls, on_result:) do |tool_call|
|
|
370
|
+
execute_tool_with_callbacks(tool_call)
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def execute_tool_with_callbacks(tool_call)
|
|
375
|
+
run_callbacks(:before_tool_call, :tool_call, tool_call)
|
|
376
|
+
result = execute_tool tool_call
|
|
377
|
+
run_callbacks(:after_tool_result, :tool_result, result)
|
|
378
|
+
result
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def add_tool_result_message(tool_call, result)
|
|
382
|
+
tool_payload = result.is_a?(Tool::Halt) ? result.content : result
|
|
383
|
+
content = content_like?(tool_payload) ? tool_payload : tool_payload.to_s
|
|
384
|
+
message = add_message role: :tool, content:, tool_call_id: tool_call.id
|
|
385
|
+
run_callbacks(:after_message, :end_message, message)
|
|
386
|
+
message
|
|
293
387
|
end
|
|
294
388
|
|
|
295
389
|
def execute_tool(tool_call)
|
|
@@ -302,7 +396,26 @@ module RubyLLM
|
|
|
302
396
|
end
|
|
303
397
|
|
|
304
398
|
args = tool_call.arguments
|
|
305
|
-
|
|
399
|
+
payload = {
|
|
400
|
+
chat: self,
|
|
401
|
+
provider: @provider.slug,
|
|
402
|
+
provider_class: @provider.class.name,
|
|
403
|
+
model: @model.id,
|
|
404
|
+
model_info: @model,
|
|
405
|
+
tool: tool,
|
|
406
|
+
tool_call: tool_call,
|
|
407
|
+
tool_name: tool.name,
|
|
408
|
+
tool_arguments: args,
|
|
409
|
+
tool_call_id: tool_call.id
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
RubyLLM.instrument('tool_call.ruby_llm', payload, config: @config) do |event|
|
|
413
|
+
result = tool.call(args)
|
|
414
|
+
event[:result] = result
|
|
415
|
+
event[:result_content] = result.is_a?(Tool::Halt) ? result.content : result
|
|
416
|
+
event[:result_class] = result.class.name
|
|
417
|
+
result
|
|
418
|
+
end
|
|
306
419
|
end
|
|
307
420
|
|
|
308
421
|
def update_tool_options(choice:, calls:)
|
|
@@ -320,6 +433,22 @@ module RubyLLM
|
|
|
320
433
|
@tool_prefs[:calls] = normalize_calls(calls) unless calls.nil?
|
|
321
434
|
end
|
|
322
435
|
|
|
436
|
+
def update_tool_concurrency(concurrency)
|
|
437
|
+
@concurrency = normalize_tool_concurrency(concurrency)
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def normalize_tool_concurrency(concurrency)
|
|
441
|
+
return nil if concurrency.nil? || concurrency == false
|
|
442
|
+
return :threads if concurrency == true
|
|
443
|
+
|
|
444
|
+
normalized = concurrency.to_sym
|
|
445
|
+
return normalized if ToolConcurrency.supported?(normalized)
|
|
446
|
+
|
|
447
|
+
raise ArgumentError,
|
|
448
|
+
"Unknown tool concurrency: #{concurrency.inspect}. " \
|
|
449
|
+
"Available modes: #{ToolConcurrency.modes.join(', ')}"
|
|
450
|
+
end
|
|
451
|
+
|
|
323
452
|
def normalize_calls(calls)
|
|
324
453
|
case calls
|
|
325
454
|
when :many, 'many'
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'logger'
|
|
4
|
+
|
|
3
5
|
module RubyLLM
|
|
4
6
|
# Global configuration for RubyLLM
|
|
5
7
|
class Configuration
|
|
@@ -9,7 +11,13 @@ module RubyLLM
|
|
|
9
11
|
key = key.to_sym
|
|
10
12
|
return if options.include?(key)
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
attr_reader key
|
|
15
|
+
|
|
16
|
+
define_method("#{key}=") do |value|
|
|
17
|
+
value = nil if value.is_a?(String) && value.strip.empty?
|
|
18
|
+
instance_variable_set(:"@#{key}", value)
|
|
19
|
+
end
|
|
20
|
+
|
|
13
21
|
option_keys << key
|
|
14
22
|
defaults[key] = default
|
|
15
23
|
end
|
|
@@ -42,6 +50,7 @@ module RubyLLM
|
|
|
42
50
|
option :model_registry_class, 'Model'
|
|
43
51
|
|
|
44
52
|
option :use_new_acts_as, false
|
|
53
|
+
option :model_registry_source, nil
|
|
45
54
|
|
|
46
55
|
option :request_timeout, 300
|
|
47
56
|
option :max_retries, 3
|
|
@@ -49,8 +58,12 @@ module RubyLLM
|
|
|
49
58
|
option :retry_backoff_factor, 2
|
|
50
59
|
option :retry_interval_randomness, 0.5
|
|
51
60
|
option :http_proxy, nil
|
|
61
|
+
option :tool_concurrency, false
|
|
52
62
|
|
|
53
63
|
option :logger, nil
|
|
64
|
+
option :instrumenter, nil
|
|
65
|
+
option :deprecation_behavior, :warn
|
|
66
|
+
option :faraday_adapter, :net_http
|
|
54
67
|
option :log_file, -> { $stdout }
|
|
55
68
|
option :log_level, -> { ENV['RUBYLLM_DEBUG'] ? Logger::DEBUG : Logger::INFO }
|
|
56
69
|
option :log_stream_debug, -> { ENV['RUBYLLM_STREAM_DEBUG'] == 'true' }
|
data/lib/ruby_llm/connection.rb
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'faraday'
|
|
4
|
+
require 'faraday/multipart'
|
|
5
|
+
require 'faraday/retry'
|
|
6
|
+
require 'ruby_llm/error_middleware'
|
|
7
|
+
require 'timeout'
|
|
8
|
+
|
|
3
9
|
module RubyLLM
|
|
4
10
|
# Connection class for managing API connections to various providers.
|
|
5
11
|
class Connection
|
|
@@ -33,16 +39,20 @@ module RubyLLM
|
|
|
33
39
|
end
|
|
34
40
|
|
|
35
41
|
def post(url, payload, &)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
42
|
+
instrument_request(:post, url) do
|
|
43
|
+
@connection.post url, payload do |req|
|
|
44
|
+
req.headers.merge! provider_headers
|
|
45
|
+
yield req if block_given?
|
|
46
|
+
end
|
|
39
47
|
end
|
|
40
48
|
end
|
|
41
49
|
|
|
42
50
|
def get(url, &)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
51
|
+
instrument_request(:get, url) do
|
|
52
|
+
@connection.get url do |req|
|
|
53
|
+
req.headers.merge! provider_headers
|
|
54
|
+
yield req if block_given?
|
|
55
|
+
end
|
|
46
56
|
end
|
|
47
57
|
end
|
|
48
58
|
|
|
@@ -52,6 +62,24 @@ module RubyLLM
|
|
|
52
62
|
|
|
53
63
|
private
|
|
54
64
|
|
|
65
|
+
def provider_headers
|
|
66
|
+
@provider.respond_to?(:headers) ? @provider.headers : {}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def instrument_request(method, url)
|
|
70
|
+
payload = {
|
|
71
|
+
provider: @provider.respond_to?(:slug) ? @provider.slug : @provider.class.name,
|
|
72
|
+
method: method,
|
|
73
|
+
url: url
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
RubyLLM.instrument('request.ruby_llm', payload, config: @config) do
|
|
77
|
+
response = yield
|
|
78
|
+
payload[:status] = response.status if response.respond_to?(:status)
|
|
79
|
+
response
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
55
83
|
def setup_timeout(faraday)
|
|
56
84
|
faraday.options.timeout = @config.request_timeout
|
|
57
85
|
end
|
|
@@ -89,7 +117,8 @@ module RubyLLM
|
|
|
89
117
|
faraday.request :multipart
|
|
90
118
|
faraday.request :json
|
|
91
119
|
faraday.response :json
|
|
92
|
-
|
|
120
|
+
adapter = @config.respond_to?(:faraday_adapter) ? @config.faraday_adapter : :net_http
|
|
121
|
+
faraday.adapter(adapter || :net_http)
|
|
93
122
|
faraday.use :llm_errors, provider: @provider
|
|
94
123
|
end
|
|
95
124
|
|
data/lib/ruby_llm/content.rb
CHANGED
|
@@ -14,7 +14,7 @@ module RubyLLM
|
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def add_attachment(source, filename: nil)
|
|
17
|
-
@attachments <<
|
|
17
|
+
@attachments << build_attachment(source, filename:)
|
|
18
18
|
self
|
|
19
19
|
end
|
|
20
20
|
|
|
@@ -26,6 +26,10 @@ module RubyLLM
|
|
|
26
26
|
end
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
+
def empty?
|
|
30
|
+
attachments.empty? && (@text.nil? || (@text.respond_to?(:empty?) && @text.empty?))
|
|
31
|
+
end
|
|
32
|
+
|
|
29
33
|
# For Rails serialization
|
|
30
34
|
def to_h
|
|
31
35
|
{ text: @text, attachments: @attachments.map(&:to_h) }
|
|
@@ -33,6 +37,16 @@ module RubyLLM
|
|
|
33
37
|
|
|
34
38
|
private
|
|
35
39
|
|
|
40
|
+
def build_attachment(source, filename: nil)
|
|
41
|
+
if source.is_a?(Attachment)
|
|
42
|
+
return source unless filename
|
|
43
|
+
|
|
44
|
+
return Attachment.new(source.source, filename:)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
Attachment.new(source, filename:)
|
|
48
|
+
end
|
|
49
|
+
|
|
36
50
|
def process_attachments_array_or_string(attachments)
|
|
37
51
|
Utils.to_safe_array(attachments).each do |file|
|
|
38
52
|
next if blank_attachment_entry?(file)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
# Owns RubyLLM deprecation warnings so applications and tests can decide how
|
|
5
|
+
# aggressively to handle compatibility paths.
|
|
6
|
+
class Deprecator
|
|
7
|
+
def warn(message)
|
|
8
|
+
case RubyLLM.config.deprecation_behavior
|
|
9
|
+
when :silence
|
|
10
|
+
nil
|
|
11
|
+
when :raise
|
|
12
|
+
raise DeprecationError, message
|
|
13
|
+
else
|
|
14
|
+
RubyLLM.logger.warn(message)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def deprecate(name, replacement:, removal:)
|
|
19
|
+
warn("#{name} is deprecated and will be removed in RubyLLM #{removal}. Use #{replacement} instead.")
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class DeprecationError < StandardError; end
|
|
24
|
+
end
|
data/lib/ruby_llm/embedding.rb
CHANGED
|
@@ -23,7 +23,37 @@ module RubyLLM
|
|
|
23
23
|
config: config)
|
|
24
24
|
model_id = model.id
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
payload = {
|
|
27
|
+
provider: provider_instance.slug,
|
|
28
|
+
provider_class: provider_instance.class.name,
|
|
29
|
+
model: model_id,
|
|
30
|
+
model_info: model,
|
|
31
|
+
input: text,
|
|
32
|
+
dimensions: dimensions
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
RubyLLM.instrument('embedding.ruby_llm', payload, config: config) do |event|
|
|
36
|
+
result = provider_instance.embed(text, model: model_id, dimensions:)
|
|
37
|
+
event[:result] = result
|
|
38
|
+
event[:response_model] = result.model
|
|
39
|
+
event[:input_tokens] = result.input_tokens
|
|
40
|
+
event[:embedding_dimensions] = vector_dimensions(result.vectors)
|
|
41
|
+
event[:embedding_count] = embedding_count(result.vectors)
|
|
42
|
+
result
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.vector_dimensions(vectors)
|
|
47
|
+
return unless vectors.is_a?(Array)
|
|
48
|
+
|
|
49
|
+
vector = vectors.first.is_a?(Array) ? vectors.first : vectors
|
|
50
|
+
vector.length if vector.respond_to?(:length)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.embedding_count(vectors)
|
|
54
|
+
return unless vectors.is_a?(Array)
|
|
55
|
+
|
|
56
|
+
vectors.first.is_a?(Array) ? vectors.size : 1
|
|
27
57
|
end
|
|
28
58
|
end
|
|
29
59
|
end
|