llm.rb 9.0.0 → 10.0.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.
data/lib/llm/agent.rb CHANGED
@@ -48,9 +48,9 @@ module LLM
48
48
  # The model identifier
49
49
  # @return [String, nil]
50
50
  # Returns the current model when no argument is provided
51
- def self.model(model = nil)
52
- return @model if model.nil?
53
- @model = model
51
+ def self.model(model = nil, &block)
52
+ return @model if model.nil? && !block
53
+ @model = block || model
54
54
  end
55
55
 
56
56
  ##
@@ -59,9 +59,9 @@ module LLM
59
59
  # One or more tools
60
60
  # @return [Array<LLM::Function>]
61
61
  # Returns the current tools when no argument is provided
62
- def self.tools(*tools)
63
- return @tools || [] if tools.empty?
64
- @tools = tools.flatten
62
+ def self.tools(*tools, &block)
63
+ return @tools || [] if tools.empty? && !block
64
+ @tools = block || tools.flatten
65
65
  end
66
66
 
67
67
  ##
@@ -70,9 +70,9 @@ module LLM
70
70
  # One or more skill directories
71
71
  # @return [Array<String>, nil]
72
72
  # Returns the current skills when no argument is provided
73
- def self.skills(*skills)
74
- return @skills if skills.empty?
75
- @skills = skills.flatten
73
+ def self.skills(*skills, &block)
74
+ return @skills if skills.empty? && !block
75
+ @skills = block || skills.flatten
76
76
  end
77
77
 
78
78
  ##
@@ -81,9 +81,9 @@ module LLM
81
81
  # The schema
82
82
  # @return [#to_json, nil]
83
83
  # Returns the current schema when no argument is provided
84
- def self.schema(schema = nil)
85
- return @schema if schema.nil?
86
- @schema = schema
84
+ def self.schema(schema = nil, &block)
85
+ return @schema if schema.nil? && !block
86
+ @schema = block || schema
87
87
  end
88
88
 
89
89
  ##
@@ -157,6 +157,19 @@ module LLM
157
157
  @stream = block || stream
158
158
  end
159
159
 
160
+ ##
161
+ # Set or get the tool names that require confirmation before they can run.
162
+ #
163
+ # @param [String, Symbol, Array<String, Symbol>, Proc] tool_names
164
+ # One or more tool names.
165
+ # @param [Proc] block
166
+ # An optional, lazy-evaluated Proc
167
+ # @return [Array<String>, Proc, nil]
168
+ def self.confirm(*tool_names, &block)
169
+ return @confirm if tool_names.empty? && !block
170
+ @confirm = block || tool_names.flatten.map(&:to_s)
171
+ end
172
+
160
173
  ##
161
174
  # @param [LLM::Provider] provider
162
175
  # A provider
@@ -168,17 +181,27 @@ module LLM
168
181
  # @option params [Array<LLM::Function>, nil] :tools Defaults to nil
169
182
  # @option params [Array<String>, nil] :skills Defaults to nil
170
183
  # @option params [#to_json, nil] :schema Defaults to nil
184
+ # @option params [Object, Proc, nil] :stream Optional stream override for this agent instance
171
185
  # @option params [LLM::Tracer, Proc, nil] :tracer Optional tracer override for this agent instance
172
186
  # @option params [Symbol, Array<Symbol>, nil] :concurrency Defaults to the agent class concurrency
173
187
  def initialize(llm, params = {})
174
- defaults = {model: self.class.model, tools: self.class.tools, skills: self.class.skills, schema: self.class.schema}.compact
175
- @concurrency = params.delete(:concurrency) || self.class.concurrency
176
188
  @llm = llm
177
- tracer = params.key?(:tracer) ? params.delete(:tracer) : self.class.tracer
178
- stream = params.key?(:stream) ? params.delete(:stream) : self.class.stream
179
- @tracer = resolve_option(tracer) unless tracer.nil?
180
- params[:stream] = resolve_option(stream) unless stream.nil?
181
- @ctx = LLM::Context.new(llm, defaults.merge({guard: true}).merge(params))
189
+ fields = %i[model skills schema tracer stream tools concurrency instructions confirm]
190
+ fields_ivar = %i[tracer concurrency instructions confirm]
191
+ fields.each do |field|
192
+ resolvable = params.key?(field) ? params.delete(field) : self.class.public_send(field)
193
+ resolve_symbol = !%i[concurrency confirm].include?(field)
194
+ resolved = resolvable != nil ? resolve_option(self, resolvable, resolve_symbol:) : resolvable
195
+ resolved = [*resolved].map(&:to_s) if field == :confirm && resolved
196
+ if field == :model
197
+ params[field] = resolved unless resolved.nil? || params.key?(field)
198
+ elsif resolved && !fields_ivar.include?(field)
199
+ params[field] ||= resolved
200
+ elsif fields_ivar.include?(field)
201
+ instance_variable_set(:"@#{field}", resolved)
202
+ end
203
+ end
204
+ @ctx = LLM::Context.new(llm, {guard: true}.merge(params))
182
205
  end
183
206
 
184
207
  ##
@@ -198,31 +221,10 @@ module LLM
198
221
  # response = agent.talk("Hello, what is your name?")
199
222
  # puts response.choices[0].content
200
223
  def talk(prompt, params = {})
201
- run_loop(:talk, prompt, params)
224
+ run_loop(prompt, params)
202
225
  end
203
226
  alias_method :chat, :talk
204
227
 
205
- ##
206
- # Maintain a conversation via the responses API.
207
- # This method immediately sends a request to the LLM and returns the response.
208
- #
209
- # @note Not all LLM providers support this API
210
- # @param prompt (see LLM::Provider#complete)
211
- # @param [Hash] params The params passed to the provider, including optional :stream, :tools, :schema etc.
212
- # @option params [Integer] :tool_attempts
213
- # The maxinum number of tool call iterations before the agent sends
214
- # in-band advisory tool errors back through the model (default 25).
215
- # Set to `nil` to disable advisory tool-limit returns.
216
- # @return [LLM::Response] Returns the LLM's response for this turn.
217
- # @example
218
- # llm = LLM.openai(key: ENV["KEY"])
219
- # agent = LLM::Agent.new(llm)
220
- # res = agent.respond("What is the capital of France?")
221
- # puts res.output_text
222
- def respond(prompt, params = {})
223
- run_loop(:respond, prompt, params)
224
- end
225
-
226
228
  ##
227
229
  # @return [LLM::Buffer<LLM::Message>]
228
230
  def messages
@@ -347,6 +349,13 @@ module LLM
347
349
  @ctx.context_window
348
350
  end
349
351
 
352
+ ##
353
+ # @see LLM::Context#params
354
+ # @return [Hash]
355
+ def params
356
+ @ctx.params
357
+ end
358
+
350
359
  ##
351
360
  # @see LLM::Context#to_h
352
361
  # @return [Hash]
@@ -383,19 +392,33 @@ module LLM
383
392
  end
384
393
  alias_method :restore, :deserialize
385
394
 
395
+ ##
396
+ # This method is called when confirmation is required before a tool can run.
397
+ #
398
+ # @param [LLM::Function] fn
399
+ # The pending function call. It can be cancelled through the
400
+ # {LLM::Function#cancel} method.
401
+ # @param [Symbol, Array<Symbol>] strategy
402
+ # The execution strategy that would be used for the tool call.
403
+ # @return [LLM::Function::Return]
404
+ # Return either `fn.spawn(strategy).wait` to approve execution or
405
+ # `fn.cancel(...)` to cancel the call.
406
+ def on_tool_confirmation(fn, strategy)
407
+ fn.cancel
408
+ end
409
+
386
410
  private
387
411
 
388
412
  ##
389
413
  # @return [LLM::Prompt]
390
414
  def apply_instructions(new_prompt)
391
- instr = self.class.instructions
392
- return new_prompt unless instr
415
+ return new_prompt unless @instructions
393
416
  if LLM::Prompt === new_prompt
394
- new_prompt.system(instr) if inject_instructions?(new_prompt)
417
+ new_prompt.system(@instructions) if inject_instructions?(new_prompt)
395
418
  new_prompt
396
419
  else
397
420
  prompt do
398
- _1.system(instr) if inject_instructions?
421
+ _1.system(@instructions) if inject_instructions?
399
422
  _1.user(new_prompt)
400
423
  end
401
424
  end
@@ -416,50 +439,46 @@ module LLM
416
439
  ##
417
440
  # @return [Array<LLM::Function::Return>]
418
441
  def call_functions
419
- case concurrency || :call
420
- when :call then wait(:call)
421
- when :thread, :task, :fiber, :fork, :ractor, Array then wait(concurrency)
422
- else raise ArgumentError, "Unknown concurrency: #{concurrency.inspect}. " \
423
- "Expected :call, :thread, :task, :fiber, :fork, :ractor, " \
424
- "or an array of the mentioned options"
442
+ strategy = concurrency || :call
443
+ return wait(strategy) unless @confirm&.any?
444
+ confirmables = @ctx.functions.select { @confirm.include?(_1.name.to_s) }
445
+ results = confirmables.map do |tool|
446
+ send(:on_tool_confirmation, tool, strategy)
425
447
  end
448
+ @ctx.functions? ? [*results, *wait(strategy)] : results
426
449
  end
427
450
 
428
- def run_loop(method, prompt, params)
429
- loop = proc do
451
+ ##
452
+ # Runs the tool loop
453
+ # @api private
454
+ def run_loop(prompt, params)
455
+ run = proc do
430
456
  max = params.key?(:tool_attempts) ? params.delete(:tool_attempts) : 25
431
457
  max = Integer(max) if max
432
458
  stream = params[:stream] || @ctx.params[:stream]
433
459
  stream.extra[:concurrency] = concurrency if LLM::Stream === stream
434
- res = @ctx.public_send(method, apply_instructions(prompt), params)
435
- loop do
436
- break unless @ctx.functions?
460
+ res = @ctx.talk(apply_instructions(prompt), params)
461
+ while @ctx.functions?
437
462
  if max
438
463
  max.times do
439
464
  break unless @ctx.functions?
440
- res = @ctx.public_send(method, call_functions, params)
465
+ res = @ctx.talk(call_functions, params)
441
466
  end
442
- break unless @ctx.functions?
443
- res = @ctx.public_send(method, @ctx.functions.map { rate_limit(_1) }, params)
467
+ res = @ctx.talk(@ctx.functions.map(&:rate_limit), params) if @ctx.functions?
444
468
  else
445
- res = @ctx.public_send(method, call_functions, params)
469
+ res = @ctx.talk(call_functions, params)
446
470
  end
447
471
  end
448
472
  res
449
473
  end
450
- @tracer ? @llm.with_tracer(@tracer, &loop) : loop.call
451
- end
452
-
453
- def rate_limit(function)
454
- LLM::Function::Return.new(function.id, function.name, {
455
- error: true,
456
- type: LLM::ToolLoopError.name,
457
- message: "tool loop rate limit reached"
458
- })
474
+ return run.call unless @tracer
475
+ @llm.with_tracer(@tracer, &run)
459
476
  end
460
477
 
461
- def resolve_option(option)
462
- Proc === option ? instance_exec(&option) : option
478
+ ##
479
+ # @api private
480
+ def resolve_option(...)
481
+ LLM::Utils.resolve_option(...)
463
482
  end
464
483
  end
465
484
  end
data/lib/llm/context.rb CHANGED
@@ -68,13 +68,6 @@ module LLM
68
68
  # @return [Symbol]
69
69
  attr_reader :mode
70
70
 
71
- ##
72
- # Returns the default params for this context
73
- # @return [Hash]
74
- def params
75
- @params.dup
76
- end
77
-
78
71
  ##
79
72
  # @param [LLM::Provider] llm
80
73
  # A provider
@@ -98,6 +91,13 @@ module LLM
98
91
  @messages = LLM::Buffer.new(llm)
99
92
  end
100
93
 
94
+ ##
95
+ # Returns the default params for this context
96
+ # @return [Hash]
97
+ def params
98
+ @params.dup
99
+ end
100
+
101
101
  ##
102
102
  # Returns a context compactor
103
103
  # This feature is inspired by the compaction approach developed by
@@ -191,14 +191,9 @@ module LLM
191
191
  # res = ctx.talk("Hello, what is your name?")
192
192
  # puts res.messages[0].content
193
193
  def talk(prompt, params = {})
194
- return respond(prompt, params) if mode == :responses
195
194
  @owner = @llm.request_owner
196
195
  compactor.compact!(prompt) if compactor.compact?(prompt)
197
- params = params.merge(messages: @messages.to_a)
198
- params = @params.merge(params)
199
- prompt, params = transform(prompt, params)
200
- bind!(params[:stream], params[:model], params[:tools])
201
- res = @llm.complete(prompt, params)
196
+ prompt, params, res = mode == :responses ? respond(prompt, params) : complete(prompt, params)
202
197
  self.compacted = false
203
198
  role = params[:role] || @llm.user_role
204
199
  role = @llm.tool_role if params[:role].nil? && [*prompt].grep(LLM::Function::Return).any?
@@ -208,35 +203,6 @@ module LLM
208
203
  end
209
204
  alias_method :chat, :talk
210
205
 
211
- ##
212
- # Interact with the context via the responses API.
213
- # This method immediately sends a request to the LLM and returns the response.
214
- #
215
- # @note Not all LLM providers support this API
216
- # @param prompt (see LLM::Provider#complete)
217
- # @param params The params, including optional :role (defaults to :user), :stream, :tools, :schema etc.
218
- # @return [LLM::Response] Returns the LLM's response for this turn.
219
- # @example
220
- # llm = LLM.openai(key: ENV["KEY"])
221
- # ctx = LLM::Context.new(llm)
222
- # res = ctx.respond("What is the capital of France?")
223
- # puts res.output_text
224
- def respond(prompt, params = {})
225
- @owner = @llm.request_owner
226
- compactor.compact!(prompt) if compactor.compact?(prompt)
227
- params = @params.merge(params)
228
- prompt, params = transform(prompt, params)
229
- bind!(params[:stream], params[:model], params[:tools])
230
- res_id = params[:store] == false ? nil : @messages.find(&:assistant?)&.response&.response_id
231
- params = params.merge(previous_response_id: res_id, input: @messages.to_a).compact
232
- res = @llm.responses.create(prompt, params)
233
- self.compacted = false
234
- role = params[:role] || @llm.user_role
235
- @messages.concat LLM::Prompt === prompt ? prompt.to_a : [LLM::Message.new(role, prompt)]
236
- @messages.concat [res.choices[-1]]
237
- res
238
- end
239
-
240
206
  ##
241
207
  # @return [String]
242
208
  def inspect
@@ -493,6 +459,9 @@ module LLM
493
459
 
494
460
  private
495
461
 
462
+ ##
463
+ # Binds runtime metadata onto an active stream.
464
+ # @api private
496
465
  def bind!(stream, model, tools)
497
466
  return unless LLM::Stream === stream
498
467
  @stream = stream
@@ -502,21 +471,33 @@ module LLM
502
471
  stream.extra[:tools] = tools
503
472
  end
504
473
 
474
+ ##
475
+ # Returns the bound stream queue, if available.
476
+ # @api private
505
477
  def queue
506
478
  return @queue if @queue
507
479
  stream.queue if LLM::Stream === stream
508
480
  end
509
481
 
482
+ ##
483
+ # Loads skill directories and adapts them into tools.
484
+ # @api private
510
485
  def load_skills(skills)
511
486
  [*skills].map { LLM::Skill.load(_1).to_tool(self) }
512
487
  end
513
488
 
489
+ ##
490
+ # Builds in-band guarded returns when the guard blocks tool work.
491
+ # @api private
514
492
  def guarded_returns
515
493
  warning = guard&.call(self)
516
494
  return unless warning
517
495
  functions.map { guarded_return_for(_1, warning) }
518
496
  end
519
497
 
498
+ ##
499
+ # Rewrites a prompt and params through the configured transformer.
500
+ # @api private
520
501
  def transform(prompt, params)
521
502
  return [prompt, params] unless transformer
522
503
  stream = params[:stream]
@@ -526,6 +507,32 @@ module LLM
526
507
  stream.on_transform_finish(self, transformer) if LLM::Stream === stream
527
508
  end
528
509
 
510
+ ##
511
+ # Executes a turn through the Responses API.
512
+ # @api private
513
+ def respond(prompt, params)
514
+ params = @params.merge(params)
515
+ prompt, params = transform(prompt, params)
516
+ bind!(params[:stream], params[:model], params[:tools])
517
+ res_id = params[:store] == false ? nil : @messages.find(&:assistant?)&.response&.response_id
518
+ params = params.merge(previous_response_id: res_id, input: @messages.to_a).compact
519
+ [prompt, params, @llm.responses.create(prompt, params)]
520
+ end
521
+
522
+ ##
523
+ # Executes a turn through the chat completions API.
524
+ # @api private
525
+ def complete(prompt, params)
526
+ params = params.merge(messages: @messages.to_a)
527
+ params = @params.merge(params)
528
+ prompt, params = transform(prompt, params)
529
+ bind!(params[:stream], params[:model], params[:tools])
530
+ [prompt, params, @llm.complete(prompt, params)]
531
+ end
532
+
533
+ ##
534
+ # Builds one guarded tool return for a blocked function call.
535
+ # @api private
529
536
  def guarded_return_for(function, warning)
530
537
  LLM::Function::Return.new(function.id, function.name, {
531
538
  error: true,
@@ -534,10 +541,4 @@ module LLM
534
541
  })
535
542
  end
536
543
  end
537
-
538
- # Backward-compatible alias
539
- Bot = Context
540
-
541
- # Scheduled for removal in v6.0
542
- deprecate_constant :Bot
543
544
  end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Function
4
+ ##
5
+ # The {LLM::Function::CallTask} class wraps a single direct function call
6
+ # behind the same task-like interface used by spawned concurrency modes.
7
+ class CallTask
8
+ ##
9
+ # @return [LLM::Function]
10
+ attr_reader :function
11
+
12
+ ##
13
+ # @param [LLM::Function] function
14
+ # @return [LLM::Function::CallTask]
15
+ def initialize(function)
16
+ @function = function
17
+ end
18
+
19
+ ##
20
+ # @return [Boolean]
21
+ def alive?
22
+ false
23
+ end
24
+
25
+ ##
26
+ # @return [nil]
27
+ def interrupt!
28
+ function.interrupt!
29
+ nil
30
+ end
31
+ alias_method :cancel!, :interrupt!
32
+
33
+ ##
34
+ # @return [LLM::Function::Return]
35
+ def wait
36
+ function.call
37
+ end
38
+ alias_method :value, :wait
39
+
40
+ ##
41
+ # @return [Class]
42
+ def group_class
43
+ LLM::Function::TaskGroup
44
+ end
45
+ end
46
+ end
data/lib/llm/function.rb CHANGED
@@ -33,6 +33,7 @@ class LLM::Function
33
33
  require_relative "function/tracing"
34
34
  require_relative "function/array"
35
35
  require_relative "function/call_group"
36
+ require_relative "function/call_task"
36
37
  require_relative "function/task"
37
38
  require_relative "function/thread_group"
38
39
  require_relative "function/fiber_group"
@@ -210,6 +211,7 @@ class LLM::Function
210
211
  #
211
212
  # @param [Symbol] strategy
212
213
  # Controls concurrency strategy:
214
+ # - `:call`: Call the function sequentially without spawning
213
215
  # - `:thread`: Use threads
214
216
  # - `:task`: Use async tasks (requires async gem)
215
217
  # - `:fork`: Use a forked child process (requires xchan.rb support)
@@ -221,6 +223,8 @@ class LLM::Function
221
223
  # Returns a task whose `#value` is an {LLM::Function::Return}.
222
224
  def spawn(strategy)
223
225
  task = case strategy
226
+ when :call
227
+ CallTask.new(self)
224
228
  when :task
225
229
  LLM.require "async" unless defined?(::Async)
226
230
  Async { call! }
@@ -241,7 +245,7 @@ class LLM::Function
241
245
  span = @tracer&.on_tool_start(id:, name:, arguments:, model:)
242
246
  Ractor::Task.new(@runner, id, name, arguments, tracer: @tracer, span:).spawn
243
247
  else
244
- raise ArgumentError, "Unknown strategy: #{strategy.inspect}. Expected :thread, :task, :fiber, :fork, or :ractor"
248
+ raise ArgumentError, "Unknown strategy: #{strategy.inspect}. Expected :call, :thread, :task, :fiber, :fork, or :ractor"
245
249
  end
246
250
  Task.new(task, self)
247
251
  ensure
@@ -295,6 +299,28 @@ class LLM::Function
295
299
  !@called && !@cancelled
296
300
  end
297
301
 
302
+ ##
303
+ # Returns an in-band error for an unresolved function call.
304
+ # @return [LLM::Function::Return]
305
+ def unavailable
306
+ Return.new(id, name, {
307
+ error: true,
308
+ type: LLM::NoSuchToolError.name,
309
+ message: "tool not found"
310
+ })
311
+ end
312
+
313
+ ##
314
+ # Returns an in-band error for a tool loop rate limit.
315
+ # @return [LLM::Function::Return]
316
+ def rate_limit
317
+ LLM::Function::Return.new(id, name, {
318
+ error: true,
319
+ type: LLM::ToolLoopError.name,
320
+ message: "tool loop rate limit reached"
321
+ })
322
+ end
323
+
298
324
  ##
299
325
  # @return [Hash]
300
326
  def adapt(provider)
data/lib/llm/provider.rb CHANGED
@@ -339,6 +339,13 @@ class LLM::Provider
339
339
  LLM::Stream === stream || stream.respond_to?(:<<)
340
340
  end
341
341
 
342
+ ##
343
+ # @return [Boolean]
344
+ # Returns true when an API key is configured
345
+ def key?
346
+ @key != nil && @key.to_s.strip.size > 0
347
+ end
348
+
342
349
  private
343
350
 
344
351
  def path(suffix)
@@ -105,14 +105,14 @@ class LLM::Anthropic
105
105
  end
106
106
 
107
107
  def resolve_tool(tool)
108
- registered = @stream.find_tool(tool["name"])
108
+ registered = @stream.__find__(tool["name"])
109
109
  fn = (registered || LLM::Function.new(tool["name"])).dup.tap do |fn|
110
110
  fn.id = tool["id"]
111
111
  fn.arguments = LLM::Anthropic.parse_tool_input(tool["input"])
112
112
  fn.tracer = @stream.extra[:tracer]
113
113
  fn.model = @stream.extra[:model]
114
114
  end
115
- [fn, (registered ? nil : @stream.tool_not_found(fn))]
115
+ [fn, (registered ? nil : fn.unavailable)]
116
116
  end
117
117
  end
118
118
  end
@@ -184,14 +184,14 @@ class LLM::Bedrock
184
184
 
185
185
  def resolve_tool(tool)
186
186
  payload = tool["toolUse"] || {}
187
- registered = @stream.find_tool(payload["name"])
187
+ registered = @stream.__find__(payload["name"])
188
188
  fn = (registered || LLM::Function.new(payload["name"])).dup.tap do |f|
189
189
  f.id = payload["toolUseId"]
190
190
  f.arguments = payload["input"] || {}
191
191
  f.tracer = @stream.extra[:tracer]
192
192
  f.model = @stream.extra[:model]
193
193
  end
194
- [fn, registered ? nil : @stream.tool_not_found(fn)]
194
+ [fn, registered ? nil : fn.unavailable]
195
195
  end
196
196
 
197
197
  def content
@@ -153,14 +153,14 @@ class LLM::Google
153
153
 
154
154
  def resolve_tool(part, cindex, pindex)
155
155
  call = part["functionCall"]
156
- registered = @stream.find_tool(call["name"])
156
+ registered = @stream.__find__(call["name"])
157
157
  fn = (registered || LLM::Function.new(call["name"])).dup.tap do |fn|
158
158
  fn.id = LLM::Google.tool_id(part:, cindex:, pindex:)
159
159
  fn.arguments = call["args"]
160
160
  fn.tracer = @stream.extra[:tracer]
161
161
  fn.model = @stream.extra[:model]
162
162
  end
163
- [fn, (registered ? nil : @stream.tool_not_found(fn))]
163
+ [fn, (registered ? nil : fn.unavailable)]
164
164
  end
165
165
  end
166
166
  end
@@ -269,14 +269,14 @@ class LLM::OpenAI
269
269
  # @group Resolvers
270
270
 
271
271
  def resolve_tool(tool, arguments)
272
- registered = @stream.find_tool(tool["name"])
272
+ registered = @stream.__find__(tool["name"])
273
273
  fn = (registered || LLM::Function.new(tool["name"])).dup.tap do |fn|
274
274
  fn.id = tool["call_id"]
275
275
  fn.arguments = arguments
276
276
  fn.tracer = @stream.extra[:tracer]
277
277
  fn.model = @stream.extra[:model]
278
278
  end
279
- [fn, (registered ? nil : @stream.tool_not_found(fn))]
279
+ [fn, (registered ? nil : fn.unavailable)]
280
280
  end
281
281
 
282
282
  def parse_arguments(arguments)
@@ -185,14 +185,14 @@ class LLM::OpenAI
185
185
  end
186
186
 
187
187
  def resolve_tool(tool, function, arguments)
188
- registered = @stream.find_tool(function["name"])
188
+ registered = @stream.__find__(function["name"])
189
189
  fn = (registered || LLM::Function.new(function["name"])).dup.tap do |fn|
190
190
  fn.id = tool["id"]
191
191
  fn.arguments = arguments
192
192
  fn.tracer = @stream.extra[:tracer]
193
193
  fn.model = @stream.extra[:model]
194
194
  end
195
- [fn, (registered ? nil : @stream.tool_not_found(fn))]
195
+ [fn, (registered ? nil : fn.unavailable)]
196
196
  end
197
197
 
198
198
  def parse_arguments(arguments)
data/lib/llm/schema.rb CHANGED
@@ -56,6 +56,8 @@ class LLM::Schema
56
56
  def resolve(schema, type)
57
57
  if LLM::Schema::Leaf === type
58
58
  type
59
+ elsif ::Array === type
60
+ resolve_array(schema, type)
59
61
  elsif Class === type && type.respond_to?(:object)
60
62
  type.object
61
63
  else
@@ -63,6 +65,15 @@ class LLM::Schema
63
65
  schema.public_send(target)
64
66
  end
65
67
  end
68
+
69
+ def resolve_array(schema, values)
70
+ item = if values.size == 1
71
+ resolve(schema, values[0])
72
+ else
73
+ schema.any_of(*values.map { resolve(schema, _1) })
74
+ end
75
+ schema.array(item)
76
+ end
66
77
  end
67
78
 
68
79
  ##
@@ -58,6 +58,11 @@ module LLM::Sequel
58
58
  agent.concurrency(concurrency)
59
59
  end
60
60
 
61
+ def confirm(*tool_names, &block)
62
+ return agent.confirm if tool_names.empty? && !block
63
+ agent.confirm(*tool_names, &block)
64
+ end
65
+
61
66
  def tracer(tracer = nil, &block)
62
67
  return agent.tracer if tracer.nil? && !block
63
68
  agent.tracer(tracer, &block)