llm.rb 9.0.0 → 11.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 +182 -4
- data/README.md +194 -42
- 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/a2a/card/capabilities.rb +41 -0
- data/lib/llm/a2a/card/interface.rb +34 -0
- data/lib/llm/a2a/card/provider.rb +27 -0
- data/lib/llm/a2a/card/skill.rb +68 -0
- data/lib/llm/a2a/card.rb +144 -0
- data/lib/llm/a2a/error.rb +49 -0
- data/lib/llm/a2a/notifications.rb +53 -0
- data/lib/llm/a2a/tasks.rb +55 -0
- data/lib/llm/a2a/transport/http.rb +131 -0
- data/lib/llm/a2a.rb +452 -0
- data/lib/llm/active_record/acts_as_agent.rb +20 -9
- data/lib/llm/active_record/acts_as_llm.rb +4 -4
- data/lib/llm/active_record.rb +1 -6
- data/lib/llm/agent.rb +96 -71
- data/lib/llm/buffer.rb +1 -2
- data/lib/llm/context.rb +77 -50
- data/lib/llm/file.rb +7 -0
- data/lib/llm/function/call_task.rb +46 -0
- data/lib/llm/function.rb +28 -2
- data/lib/llm/mcp/transport/http.rb +5 -18
- data/lib/llm/mcp/transport/stdio.rb +7 -0
- data/lib/llm/mcp.rb +20 -17
- data/lib/llm/message.rb +1 -1
- data/lib/llm/object/kernel.rb +1 -1
- data/lib/llm/provider.rb +9 -9
- 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/response.rb +1 -1
- data/lib/llm/schema.rb +11 -0
- data/lib/llm/sequel/agent.rb +19 -9
- data/lib/llm/sequel/plugin.rb +9 -13
- data/lib/llm/stream.rb +11 -36
- data/lib/llm/tool/param.rb +1 -8
- data/lib/llm/tool.rb +57 -27
- data/lib/llm/tracer.rb +1 -1
- data/lib/llm/transport/http.rb +1 -1
- data/lib/llm/transport/stream_decoder.rb +6 -3
- data/lib/llm/transport/utils.rb +35 -0
- data/lib/llm/transport.rb +1 -0
- data/lib/llm/utils.rb +73 -0
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +24 -4
- data/llm.gemspec +16 -1
- metadata +29 -5
- data/lib/llm/bot.rb +0 -3
- data/lib/llm/mcp/transport/http/event_handler.rb +0 -68
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,29 +221,13 @@ 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, :talk)
|
|
202
225
|
end
|
|
203
|
-
alias_method :chat, :talk
|
|
204
226
|
|
|
205
227
|
##
|
|
206
|
-
#
|
|
207
|
-
|
|
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)
|
|
228
|
+
# @see LLM::Context#ask
|
|
229
|
+
def ask(prompt, params = {})
|
|
230
|
+
run_loop(prompt, params, :ask)
|
|
224
231
|
end
|
|
225
232
|
|
|
226
233
|
##
|
|
@@ -347,6 +354,13 @@ module LLM
|
|
|
347
354
|
@ctx.context_window
|
|
348
355
|
end
|
|
349
356
|
|
|
357
|
+
##
|
|
358
|
+
# @see LLM::Context#params
|
|
359
|
+
# @return [Hash]
|
|
360
|
+
def params
|
|
361
|
+
@ctx.params
|
|
362
|
+
end
|
|
363
|
+
|
|
350
364
|
##
|
|
351
365
|
# @see LLM::Context#to_h
|
|
352
366
|
# @return [Hash]
|
|
@@ -357,13 +371,13 @@ module LLM
|
|
|
357
371
|
##
|
|
358
372
|
# @return [String]
|
|
359
373
|
def to_json(...)
|
|
360
|
-
|
|
374
|
+
LLM.json.dump(to_h, ...)
|
|
361
375
|
end
|
|
362
376
|
|
|
363
377
|
##
|
|
364
378
|
# @return [String]
|
|
365
379
|
def inspect
|
|
366
|
-
"#<#{
|
|
380
|
+
"#<#{LLM::Utils.object_id(self)} " \
|
|
367
381
|
"@llm=#{@llm.class}, @mode=#{mode.inspect}, @messages=#{messages.inspect}>"
|
|
368
382
|
end
|
|
369
383
|
|
|
@@ -383,19 +397,33 @@ module LLM
|
|
|
383
397
|
end
|
|
384
398
|
alias_method :restore, :deserialize
|
|
385
399
|
|
|
400
|
+
##
|
|
401
|
+
# This method is called when confirmation is required before a tool can run.
|
|
402
|
+
#
|
|
403
|
+
# @param [LLM::Function] fn
|
|
404
|
+
# The pending function call. It can be cancelled through the
|
|
405
|
+
# {LLM::Function#cancel} method.
|
|
406
|
+
# @param [Symbol, Array<Symbol>] strategy
|
|
407
|
+
# The execution strategy that would be used for the tool call.
|
|
408
|
+
# @return [LLM::Function::Return]
|
|
409
|
+
# Return either `fn.spawn(strategy).wait` to approve execution or
|
|
410
|
+
# `fn.cancel(...)` to cancel the call.
|
|
411
|
+
def on_tool_confirmation(fn, strategy)
|
|
412
|
+
fn.cancel
|
|
413
|
+
end
|
|
414
|
+
|
|
386
415
|
private
|
|
387
416
|
|
|
388
417
|
##
|
|
389
418
|
# @return [LLM::Prompt]
|
|
390
419
|
def apply_instructions(new_prompt)
|
|
391
|
-
|
|
392
|
-
return new_prompt unless instr
|
|
420
|
+
return new_prompt unless @instructions
|
|
393
421
|
if LLM::Prompt === new_prompt
|
|
394
|
-
new_prompt.system(
|
|
422
|
+
new_prompt.system(@instructions) if inject_instructions?(new_prompt)
|
|
395
423
|
new_prompt
|
|
396
424
|
else
|
|
397
425
|
prompt do
|
|
398
|
-
_1.system(
|
|
426
|
+
_1.system(@instructions) if inject_instructions?
|
|
399
427
|
_1.user(new_prompt)
|
|
400
428
|
end
|
|
401
429
|
end
|
|
@@ -416,50 +444,47 @@ module LLM
|
|
|
416
444
|
##
|
|
417
445
|
# @return [Array<LLM::Function::Return>]
|
|
418
446
|
def call_functions
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
"or an array of the mentioned options"
|
|
447
|
+
strategy = concurrency || :call
|
|
448
|
+
return wait(strategy) unless @confirm&.any?
|
|
449
|
+
confirmables = @ctx.functions.select { @confirm.include?(_1.name.to_s) }
|
|
450
|
+
results = confirmables.map do |tool|
|
|
451
|
+
send(:on_tool_confirmation, tool, strategy)
|
|
425
452
|
end
|
|
453
|
+
@ctx.functions? ? [*results, *wait(strategy)] : results
|
|
426
454
|
end
|
|
427
455
|
|
|
428
|
-
|
|
429
|
-
|
|
456
|
+
##
|
|
457
|
+
# Runs the tool loop
|
|
458
|
+
# @api private
|
|
459
|
+
def run_loop(prompt, params, target)
|
|
460
|
+
run = proc do
|
|
461
|
+
talk = @ctx.method(target)
|
|
430
462
|
max = params.key?(:tool_attempts) ? params.delete(:tool_attempts) : 25
|
|
431
463
|
max = Integer(max) if max
|
|
432
464
|
stream = params[:stream] || @ctx.params[:stream]
|
|
433
465
|
stream.extra[:concurrency] = concurrency if LLM::Stream === stream
|
|
434
|
-
res =
|
|
435
|
-
|
|
436
|
-
break unless @ctx.functions?
|
|
466
|
+
res = talk.call(apply_instructions(prompt), params)
|
|
467
|
+
while @ctx.functions?
|
|
437
468
|
if max
|
|
438
469
|
max.times do
|
|
439
470
|
break unless @ctx.functions?
|
|
440
|
-
res =
|
|
471
|
+
res = talk.call(call_functions, params)
|
|
441
472
|
end
|
|
442
|
-
|
|
443
|
-
res = @ctx.public_send(method, @ctx.functions.map { rate_limit(_1) }, params)
|
|
473
|
+
res = talk.call(@ctx.functions.map(&:rate_limit), params) if @ctx.functions?
|
|
444
474
|
else
|
|
445
|
-
res =
|
|
475
|
+
res = talk.call(call_functions, params)
|
|
446
476
|
end
|
|
447
477
|
end
|
|
448
478
|
res
|
|
449
479
|
end
|
|
450
|
-
|
|
480
|
+
return run.call unless @tracer
|
|
481
|
+
@llm.with_tracer(@tracer, &run)
|
|
451
482
|
end
|
|
452
483
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
message: "tool loop rate limit reached"
|
|
458
|
-
})
|
|
459
|
-
end
|
|
460
|
-
|
|
461
|
-
def resolve_option(option)
|
|
462
|
-
Proc === option ? instance_exec(&option) : option
|
|
484
|
+
##
|
|
485
|
+
# @api private
|
|
486
|
+
def resolve_option(...)
|
|
487
|
+
LLM::Utils.resolve_option(...)
|
|
463
488
|
end
|
|
464
489
|
end
|
|
465
490
|
end
|
data/lib/llm/buffer.rb
CHANGED
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?
|
|
@@ -206,41 +201,37 @@ module LLM
|
|
|
206
201
|
@messages.concat [res.choices[-1]]
|
|
207
202
|
res
|
|
208
203
|
end
|
|
209
|
-
alias_method :chat, :talk
|
|
210
204
|
|
|
211
205
|
##
|
|
212
|
-
#
|
|
213
|
-
#
|
|
214
|
-
#
|
|
215
|
-
# @
|
|
216
|
-
# @param
|
|
217
|
-
# @
|
|
218
|
-
#
|
|
219
|
-
# @
|
|
220
|
-
#
|
|
221
|
-
#
|
|
222
|
-
#
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
@messages.concat LLM::Prompt === prompt ? prompt.to_a : [LLM::Message.new(role, prompt)]
|
|
236
|
-
@messages.concat [res.choices[-1]]
|
|
237
|
-
res
|
|
206
|
+
# Ask a question and return the content string directly.
|
|
207
|
+
# Accepts `with:` for file attachments and a block for streaming.
|
|
208
|
+
# This interface is compatible with RubyLLM's `ask` method.
|
|
209
|
+
# @param [String] prompt
|
|
210
|
+
# @param [Hash] options
|
|
211
|
+
# @option options [String, Array<String>, nil] :with
|
|
212
|
+
# File path(s) to attach
|
|
213
|
+
# @option options [#<<, LLM::Stream, nil] :stream
|
|
214
|
+
# A stream target
|
|
215
|
+
# @yield [String] content chunks when streaming
|
|
216
|
+
# @return [LLM::Response]
|
|
217
|
+
def ask(prompt, options = {}, &block)
|
|
218
|
+
options = {with: nil, stream: nil}.merge!(options || {})
|
|
219
|
+
with, stream = options.values_at(:with, :stream)
|
|
220
|
+
prompt = with ? [prompt, [*with].map { local_file(_1) }] : prompt
|
|
221
|
+
target = if block
|
|
222
|
+
blk = block.dup
|
|
223
|
+
blk.singleton_class.alias_method(:<<, :call)
|
|
224
|
+
blk
|
|
225
|
+
else
|
|
226
|
+
stream
|
|
227
|
+
end
|
|
228
|
+
target ? talk(prompt, stream: target) : talk(prompt)
|
|
238
229
|
end
|
|
239
230
|
|
|
240
231
|
##
|
|
241
232
|
# @return [String]
|
|
242
233
|
def inspect
|
|
243
|
-
"#<#{
|
|
234
|
+
"#<#{LLM::Utils.object_id(self)} " \
|
|
244
235
|
"@llm=#{@llm.class}, @mode=#{@mode.inspect}, @params=#{@params.inspect}, " \
|
|
245
236
|
"@messages=#{@messages.inspect}>"
|
|
246
237
|
end
|
|
@@ -448,7 +439,7 @@ module LLM
|
|
|
448
439
|
##
|
|
449
440
|
# @return [String]
|
|
450
441
|
def to_json(...)
|
|
451
|
-
|
|
442
|
+
LLM.json.dump(to_h, ...)
|
|
452
443
|
end
|
|
453
444
|
|
|
454
445
|
##
|
|
@@ -493,6 +484,9 @@ module LLM
|
|
|
493
484
|
|
|
494
485
|
private
|
|
495
486
|
|
|
487
|
+
##
|
|
488
|
+
# Binds runtime metadata onto an active stream.
|
|
489
|
+
# @api private
|
|
496
490
|
def bind!(stream, model, tools)
|
|
497
491
|
return unless LLM::Stream === stream
|
|
498
492
|
@stream = stream
|
|
@@ -502,21 +496,34 @@ module LLM
|
|
|
502
496
|
stream.extra[:tools] = tools
|
|
503
497
|
end
|
|
504
498
|
|
|
499
|
+
##
|
|
500
|
+
# Returns the bound stream queue, if available.
|
|
501
|
+
# @api private
|
|
505
502
|
def queue
|
|
506
|
-
|
|
507
|
-
|
|
503
|
+
[@queue, stream&.queue].compact.first
|
|
504
|
+
rescue NoMethodError
|
|
505
|
+
nil
|
|
508
506
|
end
|
|
509
507
|
|
|
508
|
+
##
|
|
509
|
+
# Loads skill directories and adapts them into tools.
|
|
510
|
+
# @api private
|
|
510
511
|
def load_skills(skills)
|
|
511
512
|
[*skills].map { LLM::Skill.load(_1).to_tool(self) }
|
|
512
513
|
end
|
|
513
514
|
|
|
515
|
+
##
|
|
516
|
+
# Builds in-band guarded returns when the guard blocks tool work.
|
|
517
|
+
# @api private
|
|
514
518
|
def guarded_returns
|
|
515
519
|
warning = guard&.call(self)
|
|
516
520
|
return unless warning
|
|
517
521
|
functions.map { guarded_return_for(_1, warning) }
|
|
518
522
|
end
|
|
519
523
|
|
|
524
|
+
##
|
|
525
|
+
# Rewrites a prompt and params through the configured transformer.
|
|
526
|
+
# @api private
|
|
520
527
|
def transform(prompt, params)
|
|
521
528
|
return [prompt, params] unless transformer
|
|
522
529
|
stream = params[:stream]
|
|
@@ -526,6 +533,32 @@ module LLM
|
|
|
526
533
|
stream.on_transform_finish(self, transformer) if LLM::Stream === stream
|
|
527
534
|
end
|
|
528
535
|
|
|
536
|
+
##
|
|
537
|
+
# Executes a turn through the Responses API.
|
|
538
|
+
# @api private
|
|
539
|
+
def respond(prompt, params)
|
|
540
|
+
params = @params.merge(params)
|
|
541
|
+
prompt, params = transform(prompt, params)
|
|
542
|
+
bind!(params[:stream], params[:model], params[:tools])
|
|
543
|
+
res_id = params[:store] == false ? nil : @messages.find(&:assistant?)&.response&.response_id
|
|
544
|
+
params = params.merge(previous_response_id: res_id, input: @messages.to_a).compact
|
|
545
|
+
[prompt, params, @llm.responses.create(prompt, params)]
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
##
|
|
549
|
+
# Executes a turn through the chat completions API.
|
|
550
|
+
# @api private
|
|
551
|
+
def complete(prompt, params)
|
|
552
|
+
params = params.merge(messages: @messages.to_a)
|
|
553
|
+
params = @params.merge(params)
|
|
554
|
+
prompt, params = transform(prompt, params)
|
|
555
|
+
bind!(params[:stream], params[:model], params[:tools])
|
|
556
|
+
[prompt, params, @llm.complete(prompt, params)]
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
##
|
|
560
|
+
# Builds one guarded tool return for a blocked function call.
|
|
561
|
+
# @api private
|
|
529
562
|
def guarded_return_for(function, warning)
|
|
530
563
|
LLM::Function::Return.new(function.id, function.name, {
|
|
531
564
|
error: true,
|
|
@@ -534,10 +567,4 @@ module LLM
|
|
|
534
567
|
})
|
|
535
568
|
end
|
|
536
569
|
end
|
|
537
|
-
|
|
538
|
-
# Backward-compatible alias
|
|
539
|
-
Bot = Context
|
|
540
|
-
|
|
541
|
-
# Scheduled for removal in v6.0
|
|
542
|
-
deprecate_constant :Bot
|
|
543
570
|
end
|
data/lib/llm/file.rb
CHANGED
|
@@ -43,6 +43,13 @@ class LLM::File
|
|
|
43
43
|
mime_type == "application/pdf"
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
+
##
|
|
47
|
+
# @return [Boolean]
|
|
48
|
+
# Returns true if the file exists on disk
|
|
49
|
+
def exist?
|
|
50
|
+
File.exist?(path)
|
|
51
|
+
end
|
|
52
|
+
|
|
46
53
|
##
|
|
47
54
|
# @return [Integer]
|
|
48
55
|
# Returns the size of the file in bytes
|
|
@@ -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"
|
|
@@ -181,7 +182,7 @@ class LLM::Function
|
|
|
181
182
|
def define(klass = nil, &b)
|
|
182
183
|
@runner = klass || b
|
|
183
184
|
end
|
|
184
|
-
alias_method :
|
|
185
|
+
alias_method :def, :define
|
|
185
186
|
|
|
186
187
|
##
|
|
187
188
|
# Call the function
|
|
@@ -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)
|
|
@@ -7,7 +7,7 @@ module LLM::MCP::Transport
|
|
|
7
7
|
# JSON-RPC messages with HTTP POST requests and buffers response
|
|
8
8
|
# messages for non-blocking reads.
|
|
9
9
|
class HTTP
|
|
10
|
-
|
|
10
|
+
include LLM::Transport::Utils
|
|
11
11
|
|
|
12
12
|
##
|
|
13
13
|
# @param [String] url
|
|
@@ -22,7 +22,7 @@ module LLM::MCP::Transport
|
|
|
22
22
|
def initialize(url:, headers: {}, timeout: nil, transport: nil)
|
|
23
23
|
@uri = URI.parse(url)
|
|
24
24
|
@headers = headers
|
|
25
|
-
@transport = resolve_transport(transport, timeout
|
|
25
|
+
@transport = resolve_transport(uri, transport, timeout)
|
|
26
26
|
@queue = []
|
|
27
27
|
@monitor = Monitor.new
|
|
28
28
|
@running = false
|
|
@@ -101,24 +101,11 @@ module LLM::MCP::Transport
|
|
|
101
101
|
res
|
|
102
102
|
end
|
|
103
103
|
|
|
104
|
-
def resolve_transport(transport, timeout:)
|
|
105
|
-
return default_transport(timeout:) if transport.nil?
|
|
106
|
-
if Class === transport && transport <= LLM::Transport
|
|
107
|
-
return transport.new(host: uri.host, port: uri.port, timeout:, ssl: uri.scheme == "https")
|
|
108
|
-
end
|
|
109
|
-
transport
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
def default_transport(timeout:)
|
|
113
|
-
LLM::Transport::HTTP.new(host: uri.host, port: uri.port, timeout:, ssl: uri.scheme == "https")
|
|
114
|
-
end
|
|
115
|
-
|
|
116
104
|
def read(res)
|
|
117
105
|
if res["content-type"].to_s.include?("text/event-stream")
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
parser.free
|
|
106
|
+
decoder = LLM::Transport::StreamDecoder.new { enqueue(_1) }
|
|
107
|
+
res.read_body { decoder << _1 }
|
|
108
|
+
decoder.free
|
|
122
109
|
else
|
|
123
110
|
body = +""
|
|
124
111
|
res.read_body { body << _1 }
|