ruby_llm 1.14.1 → 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 +6 -7
- data/lib/generators/ruby_llm/generator_helpers.rb +8 -0
- data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +1 -1
- data/lib/generators/ruby_llm/tool/templates/tool.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 +4 -26
- data/lib/ruby_llm/active_record/acts_as_legacy.rb +123 -29
- data/lib/ruby_llm/active_record/chat_methods.rb +41 -24
- data/lib/ruby_llm/active_record/message_methods.rb +87 -4
- data/lib/ruby_llm/active_record/model_methods.rb +7 -9
- data/lib/ruby_llm/active_record/payload_helpers.rb +3 -0
- data/lib/ruby_llm/active_record/tool_call_methods.rb +3 -0
- data/lib/ruby_llm/agent.rb +4 -2
- data/lib/ruby_llm/aliases.json +108 -75
- data/lib/ruby_llm/aliases.rb +3 -0
- data/lib/ruby_llm/attachment.rb +41 -40
- data/lib/ruby_llm/chat.rb +229 -59
- 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/cost.rb +224 -0
- 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 +39 -4
- data/lib/ruby_llm/instrumentation.rb +36 -0
- data/lib/ruby_llm/message.rb +20 -0
- data/lib/ruby_llm/mime_type.rb +25 -0
- data/lib/ruby_llm/model/info.rb +53 -2
- data/lib/ruby_llm/model/pricing.rb +19 -9
- data/lib/ruby_llm/model/pricing_category.rb +13 -2
- 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 +17817 -13942
- data/lib/ruby_llm/models.rb +97 -31
- data/lib/ruby_llm/models_schema.json +3 -0
- data/lib/ruby_llm/provider.rb +20 -4
- 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 +32 -3
- 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 +26 -13
- 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 +10 -1
- 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 +10 -4
- data/lib/ruby_llm/providers/gemini/images.rb +2 -2
- data/lib/ruby_llm/providers/gemini/media.rb +16 -9
- data/lib/ruby_llm/providers/gemini/streaming.rb +6 -1
- data/lib/ruby_llm/providers/gemini/tools.rb +5 -1
- 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 +7 -2
- data/lib/ruby_llm/providers/mistral/chat.rb +56 -5
- 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/capabilities.rb +82 -12
- data/lib/ruby_llm/providers/openai/chat.rb +61 -7
- data/lib/ruby_llm/providers/openai/images.rb +58 -6
- data/lib/ruby_llm/providers/openai/media.rb +40 -16
- data/lib/ruby_llm/providers/openai/streaming.rb +7 -6
- 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 +36 -8
- data/lib/ruby_llm/providers/openrouter/images.rb +2 -2
- data/lib/ruby_llm/providers/openrouter/models.rb +1 -1
- data/lib/ruby_llm/providers/openrouter/streaming.rb +5 -6
- 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 +11 -1
- data/lib/ruby_llm/stream_accumulator.rb +45 -30
- data/lib/ruby_llm/streaming.rb +4 -0
- data/lib/ruby_llm/tokens.rb +8 -0
- data/lib/ruby_llm/tool.rb +24 -7
- 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 +11 -6
- data/lib/tasks/models.rake +45 -16
- data/lib/tasks/release.rake +50 -23
- metadata +35 -13
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
|
|
@@ -30,6 +33,7 @@ module RubyLLM
|
|
|
30
33
|
tool_call: nil,
|
|
31
34
|
tool_result: nil
|
|
32
35
|
}
|
|
36
|
+
@callbacks = Hash.new { |callbacks, name| callbacks[name] = [] }
|
|
33
37
|
end
|
|
34
38
|
|
|
35
39
|
def ask(message = nil, with: nil, &)
|
|
@@ -51,19 +55,21 @@ module RubyLLM
|
|
|
51
55
|
self
|
|
52
56
|
end
|
|
53
57
|
|
|
54
|
-
def with_tool(tool, choice: nil, calls: nil)
|
|
58
|
+
def with_tool(tool, choice: nil, calls: nil, concurrency: @concurrency)
|
|
55
59
|
unless tool.nil?
|
|
56
60
|
tool_instance = tool.is_a?(Class) ? tool.new : tool
|
|
57
61
|
@tools[tool_instance.name.to_sym] = tool_instance
|
|
58
62
|
end
|
|
59
63
|
update_tool_options(choice:, calls:)
|
|
64
|
+
update_tool_concurrency(concurrency)
|
|
60
65
|
self
|
|
61
66
|
end
|
|
62
67
|
|
|
63
|
-
def with_tools(*tools, replace: false, choice: nil, calls: nil)
|
|
68
|
+
def with_tools(*tools, replace: false, choice: nil, calls: nil, concurrency: @concurrency)
|
|
64
69
|
@tools.clear if replace
|
|
65
70
|
tools.compact.each { |tool| with_tool tool }
|
|
66
71
|
update_tool_options(choice:, calls:)
|
|
72
|
+
update_tool_concurrency(concurrency)
|
|
67
73
|
self
|
|
68
74
|
end
|
|
69
75
|
|
|
@@ -112,62 +118,48 @@ module RubyLLM
|
|
|
112
118
|
self
|
|
113
119
|
end
|
|
114
120
|
|
|
115
|
-
def on_new_message(&
|
|
116
|
-
|
|
117
|
-
self
|
|
121
|
+
def on_new_message(&)
|
|
122
|
+
set_legacy_callback(:new_message, :on_new_message, :before_message, &)
|
|
118
123
|
end
|
|
119
124
|
|
|
120
|
-
def on_end_message(&
|
|
121
|
-
|
|
122
|
-
self
|
|
125
|
+
def on_end_message(&)
|
|
126
|
+
set_legacy_callback(:end_message, :on_end_message, :after_message, &)
|
|
123
127
|
end
|
|
124
128
|
|
|
125
|
-
def on_tool_call(&
|
|
126
|
-
|
|
127
|
-
self
|
|
129
|
+
def on_tool_call(&)
|
|
130
|
+
set_legacy_callback(:tool_call, :on_tool_call, :before_tool_call, &)
|
|
128
131
|
end
|
|
129
132
|
|
|
130
|
-
def on_tool_result(&
|
|
131
|
-
|
|
132
|
-
self
|
|
133
|
+
def on_tool_result(&)
|
|
134
|
+
set_legacy_callback(:tool_result, :on_tool_result, :after_tool_result, &)
|
|
133
135
|
end
|
|
134
136
|
|
|
135
|
-
def
|
|
136
|
-
|
|
137
|
+
def before_message(&)
|
|
138
|
+
add_callback(:before_message, &)
|
|
137
139
|
end
|
|
138
140
|
|
|
139
|
-
def
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
tools: @tools,
|
|
143
|
-
tool_prefs: @tool_prefs,
|
|
144
|
-
temperature: @temperature,
|
|
145
|
-
model: @model,
|
|
146
|
-
params: @params,
|
|
147
|
-
headers: @headers,
|
|
148
|
-
schema: @schema,
|
|
149
|
-
thinking: @thinking,
|
|
150
|
-
&wrap_streaming_block(&)
|
|
151
|
-
)
|
|
141
|
+
def after_message(&)
|
|
142
|
+
add_callback(:after_message, &)
|
|
143
|
+
end
|
|
152
144
|
|
|
153
|
-
|
|
145
|
+
def before_tool_call(&)
|
|
146
|
+
add_callback(:before_tool_call, &)
|
|
147
|
+
end
|
|
154
148
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
rescue JSON::ParserError
|
|
159
|
-
# If parsing fails, keep content as string
|
|
160
|
-
end
|
|
161
|
-
end
|
|
149
|
+
def after_tool_result(&)
|
|
150
|
+
add_callback(:after_tool_result, &)
|
|
151
|
+
end
|
|
162
152
|
|
|
163
|
-
|
|
164
|
-
|
|
153
|
+
def each(&)
|
|
154
|
+
messages.each(&)
|
|
155
|
+
end
|
|
165
156
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
157
|
+
def cost
|
|
158
|
+
Cost.aggregate(messages.map { |message| message.cost(model: message.model_info || model) })
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def complete(&)
|
|
162
|
+
instrument_completion(&)
|
|
171
163
|
end
|
|
172
164
|
|
|
173
165
|
def add_message(message_or_attributes)
|
|
@@ -176,6 +168,7 @@ module RubyLLM
|
|
|
176
168
|
message
|
|
177
169
|
end
|
|
178
170
|
|
|
171
|
+
# Mutates this chat by removing all in-memory messages.
|
|
179
172
|
def reset_messages!
|
|
180
173
|
@messages.clear
|
|
181
174
|
end
|
|
@@ -221,34 +214,176 @@ module RubyLLM
|
|
|
221
214
|
sanitized.empty? ? 'response' : sanitized
|
|
222
215
|
end
|
|
223
216
|
|
|
217
|
+
def add_callback(name, &block)
|
|
218
|
+
@callbacks[name] << block if block
|
|
219
|
+
self
|
|
220
|
+
end
|
|
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
|
+
|
|
303
|
+
def set_legacy_callback(name, legacy_name, additive_name, &block)
|
|
304
|
+
warn_legacy_callback_deprecation(legacy_name, additive_name) if block
|
|
305
|
+
|
|
306
|
+
@on[name] = block
|
|
307
|
+
self
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def warn_legacy_callback_deprecation(legacy_name, additive_name)
|
|
311
|
+
RubyLLM.deprecator.warn(
|
|
312
|
+
"`#{legacy_name}` is deprecated and will be removed in RubyLLM 2.0. " \
|
|
313
|
+
"Use `#{additive_name}` instead."
|
|
314
|
+
)
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def run_callbacks(name, legacy_name, *args)
|
|
318
|
+
@callbacks[name].each { |callback| callback.call(*args) }
|
|
319
|
+
@on[legacy_name]&.call(*args)
|
|
320
|
+
end
|
|
321
|
+
|
|
224
322
|
def wrap_streaming_block(&block)
|
|
225
323
|
return nil unless block_given?
|
|
226
324
|
|
|
227
|
-
|
|
325
|
+
run_callbacks(:before_message, :new_message)
|
|
228
326
|
|
|
229
327
|
proc do |chunk|
|
|
230
328
|
block.call chunk
|
|
231
329
|
end
|
|
232
330
|
end
|
|
233
331
|
|
|
234
|
-
def handle_tool_calls(response, &)
|
|
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)
|
|
235
344
|
halt_result = nil
|
|
236
345
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
result
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
346
|
+
tool_calls.each_value do |tool_call|
|
|
347
|
+
run_callbacks(:before_message, :new_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
|
|
352
|
+
|
|
353
|
+
halt_result
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def handle_concurrent_tool_calls(tool_calls)
|
|
357
|
+
halt_result = nil
|
|
246
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)
|
|
247
362
|
halt_result = result if result.is_a?(Tool::Halt)
|
|
248
363
|
end
|
|
249
364
|
|
|
250
|
-
|
|
251
|
-
|
|
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
|
|
252
387
|
end
|
|
253
388
|
|
|
254
389
|
def execute_tool(tool_call)
|
|
@@ -261,7 +396,26 @@ module RubyLLM
|
|
|
261
396
|
end
|
|
262
397
|
|
|
263
398
|
args = tool_call.arguments
|
|
264
|
-
|
|
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
|
|
265
419
|
end
|
|
266
420
|
|
|
267
421
|
def update_tool_options(choice:, calls:)
|
|
@@ -279,6 +433,22 @@ module RubyLLM
|
|
|
279
433
|
@tool_prefs[:calls] = normalize_calls(calls) unless calls.nil?
|
|
280
434
|
end
|
|
281
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
|
+
|
|
282
452
|
def normalize_calls(calls)
|
|
283
453
|
case calls
|
|
284
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)
|