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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +76 -4
- data/README.md +80 -12
- data/data/anthropic.json +278 -258
- data/data/bedrock.json +1288 -1561
- data/data/deepseek.json +38 -38
- data/data/google.json +656 -579
- data/data/openai.json +860 -818
- data/data/xai.json +243 -552
- data/data/zai.json +168 -168
- data/lib/llm/active_record/acts_as_agent.rb +5 -0
- data/lib/llm/active_record.rb +1 -6
- data/lib/llm/agent.rb +90 -71
- data/lib/llm/context.rb +49 -48
- data/lib/llm/function/call_task.rb +46 -0
- data/lib/llm/function.rb +27 -1
- data/lib/llm/provider.rb +7 -0
- data/lib/llm/providers/anthropic/stream_parser.rb +2 -2
- data/lib/llm/providers/bedrock/stream_parser.rb +2 -2
- data/lib/llm/providers/google/stream_parser.rb +2 -2
- data/lib/llm/providers/openai/responses/stream_parser.rb +2 -2
- data/lib/llm/providers/openai/stream_parser.rb +2 -2
- data/lib/llm/schema.rb +11 -0
- data/lib/llm/sequel/agent.rb +5 -0
- data/lib/llm/sequel/plugin.rb +1 -6
- data/lib/llm/stream.rb +11 -36
- data/lib/llm/tool/param.rb +1 -8
- data/lib/llm/utils.rb +29 -0
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +1 -0
- metadata +4 -3
- data/lib/llm/bot.rb +0 -3
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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(
|
|
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
|
-
|
|
392
|
-
return new_prompt unless instr
|
|
415
|
+
return new_prompt unless @instructions
|
|
393
416
|
if LLM::Prompt === new_prompt
|
|
394
|
-
new_prompt.system(
|
|
417
|
+
new_prompt.system(@instructions) if inject_instructions?(new_prompt)
|
|
395
418
|
new_prompt
|
|
396
419
|
else
|
|
397
420
|
prompt do
|
|
398
|
-
_1.system(
|
|
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
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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
|
-
|
|
429
|
-
|
|
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.
|
|
435
|
-
|
|
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.
|
|
465
|
+
res = @ctx.talk(call_functions, params)
|
|
441
466
|
end
|
|
442
|
-
|
|
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.
|
|
469
|
+
res = @ctx.talk(call_functions, params)
|
|
446
470
|
end
|
|
447
471
|
end
|
|
448
472
|
res
|
|
449
473
|
end
|
|
450
|
-
|
|
451
|
-
|
|
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
|
-
|
|
462
|
-
|
|
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
|
|
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.
|
|
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 :
|
|
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.
|
|
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 :
|
|
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.
|
|
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 :
|
|
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.
|
|
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 :
|
|
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.
|
|
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 :
|
|
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
|
##
|
data/lib/llm/sequel/agent.rb
CHANGED
|
@@ -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)
|