Class: LLM::Function

Inherits:
Object
  • Object
show all
Extended by:
Registry
Defined in:
lib/llm/function.rb,
lib/llm/function/fork.rb,
lib/llm/function/task.rb,
lib/llm/function/array.rb,
lib/llm/function/group.rb,
lib/llm/function/ractor.rb,
lib/llm/function/window.rb,
lib/llm/function/fork/job.rb,
lib/llm/function/registry.rb,
lib/llm/function/fork/task.rb,
lib/llm/function/ractor/job.rb,
lib/llm/function/ractor/task.rb

Overview

The LLM::Function class represents a local function that can be called by an LLM. Most users should define tools as subclasses of Tool instead — Function is the lower-level building block that Tool wraps.

Examples:

Tool subclass (preferred for most users)

class ReadFile < LLM::Tool
  name "read-file"
  description "Read a file from disk"
  parameter :path, String, "The filename or path"
  required %i[path]

  def call(path:)
    {contents: File.read(path)}
  end
end

Inline function (block-form DSL)

LLM.function(:run_command) do |fn|
  fn.name "run-command"
  fn.description "Runs a shell command"
  fn.params do |schema|
    schema.object(command: schema.string.required)
  end
  fn.define do |command:|
    {success: Kernel.system(command)}
  end
end

Defined Under Namespace

Modules: Array, Async, Fiber, Fork, Ractor, Registry, Sequential, Thread Classes: Group, Return, Task, Window

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Registry

clear_registry!, find_by_name, lock, register, registry, registry_key, tool_name, unregister

Constructor Details

#initialize(name) {|self| ... } ⇒ Function

Returns a new instance of Function.

Parameters:

  • name (String) —

    The function name

Yield Parameters:



193
194
195
196
197
198
199
# File 'lib/llm/function.rb', line 193

def initialize(name, &b)
  @name = name
  @schema = LLM::Schema.new
  @called = false
  @cancelled = false
  yield(self) if block_given?
end

Instance Attribute Details

#id ⇒ String?

Returns the function ID

Returns:

  • (String, nil)


139
140
141
# File 'lib/llm/function.rb', line 139

def id
  @id
end

#arguments ⇒ Hash, ...

Returns function arguments

Returns:



144
145
146
# File 'lib/llm/function.rb', line 144

def arguments
  @arguments
end

#tracer ⇒ LLM::Tracer?

Returns a tracer, or nil

Returns:



176
177
178
# File 'lib/llm/function.rb', line 176

def tracer
  @tracer
end

#model ⇒ String?

Returns a model name, or nil

Returns:

  • (String, nil)


181
182
183
# File 'lib/llm/function.rb', line 181

def model
  @model
end

#intercept ⇒ Class<LLM::Intercept>?

Returns the intercept class that supervises this function, or nil. The context stamps the intercept onto the functions it binds, so any task built from this function checks it before the tool runs.

Returns:



188
189
190
# File 'lib/llm/function.rb', line 188

def intercept
  @intercept
end

Class Method Details

.interrupt(runner) ⇒ nil

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Tells a runner that its call was interrupted, if it wants telling.

Internal to the strategies that run the hook from their own side, where the runner is a local rather than something #interrupt! can resolve: the child's copy in :fork is not the parent's, and a ractor cannot be handed the function at all. The argument is the runner and not the function, which is what separates this from #interrupt!, and one lookup means on_cancel keeps its precedence over on_interrupt everywhere.

Parameters:

Returns:

  • (nil)


73
74
75
76
77
78
# File 'lib/llm/function.rb', line 73

def self.interrupt(runner)
  return nil unless runner
  hook = %i[on_cancel on_interrupt].find { runner.respond_to?(_1) }
  runner.public_send(hook) if hook
  nil
end

.isolated ⇒ Array<Symbol>

Returns strategies that execute in an isolated environment.

Returns:



57
58
59
# File 'lib/llm/function.rb', line 57

def self.isolated
  %i[fork ractor]
end

Instance Method Details

#runner ⇒ Object

Returns the bound function runner instance.

Resolved once and kept, so that the object an interrupt has to reach is the object that runs the call. A class-backed tool would otherwise build an instance here and discard it, leaving #interrupt! with a class to send the hook to.

An in-process strategy resolves it in #task, on the calling thread, before the task exists for another thread to interrupt. Every other path resolves it at the first call.

The tracer is assigned when the instance is resolved, so a tracer set after that does not reach it.

The instance belongs to this function object rather than to a call. The per-call copy is the one Message#functions makes, and that is what keeps the usual path to one call per instance; a second #task on one function, or a retry, reuses the instance and whatever its ivars hold.

Returns:



497
498
499
500
501
502
503
# File 'lib/llm/function.rb', line 497

def runner
  @_runner ||= begin
    _runner = Class === @runner ? @runner.new : @runner
    _runner.tracer = @tracer if _runner.respond_to?(:tracer=)
    _runner
  end
end

#==(other) ⇒ Boolean Also known as: eql?

Compares functions by tool call ID when both sides have one.

Parameters:

Returns:

  • (Boolean)


158
159
160
161
162
163
# File 'lib/llm/function.rb', line 158

def ==(other)
  return true if equal?(other)
  return false unless self.class === other
  return false unless id && other.id
  id == other.id
end

#hash ⇒ Integer

Returns a hash value compatible with #==.

Returns:

  • (Integer)


169
170
171
# File 'lib/llm/function.rb', line 169

def hash
  id ? id.hash : object_id.hash
end

#name(name = nil)

This method returns an undefined value.

Set (or get) the function name

Parameters:

  • name (String) (defaults to: nil) —

    The function name



205
206
207
208
209
210
211
# File 'lib/llm/function.rb', line 205

def name(name = nil)
  if name
    @name = name.to_s
  else
    @name
  end
end

#description(desc = nil)

This method returns an undefined value.

Set (or get) the function description

Parameters:

  • desc (String) (defaults to: nil) —

    The function description



217
218
219
220
221
222
223
# File 'lib/llm/function.rb', line 217

def description(desc = nil)
  if desc
    @description = desc
  else
    @description
  end
end

#params(p = nil) {|schema| ... } ⇒ LLM::Schema::Leaf?

Note:

A parameter type that is given as a proc is resolved when the parameters are rendered rather than here, so a tool can say parameter :fruit, proc { Enum[Fruit.keys] } and ask the model for what is available at the time of the call.

Set (or get) the function parameters

Parameters:

  • p (Hash, nil) (defaults to: nil) —

    Optional set of params to set

Yield Parameters:

Returns:



238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# File 'lib/llm/function.rb', line 238

def params(p = nil)
  if block_given?
    params = yield(@schema)
    params = LLM::Schema.parse(params) if Hash === params
    if @params
      @params.merge!(params)
    else
      @params = params
    end
  elsif p
    params { p }
  else
    @params || LLM::Schema::Object.new({})
  end
end

#define(klass = nil, &b) Also known as: def

This method returns an undefined value.

Set the function implementation

Parameters:

  • b (Proc, Class) —

    The function implementation



258
259
260
# File 'lib/llm/function.rb', line 258

def define(klass = nil, &b)
  @runner = klass || b
end

#call ⇒ LLM::Function::Return

Call the function



266
267
268
269
270
271
# File 'lib/llm/function.rb', line 266

def call
  llm = @tracer&.llm
  llm ? llm.with_tracer(@tracer) { call_function } : call_function
ensure
  @called = true
end

#task(strategy, options = {}) ⇒ LLM::Function::Task

Returns a function as a LLM::Function::Task.

Examples:

# As a group
ctx.talk(ctx.pending_functions.wait)

# As a task
task = tool.task(:thread)
result = task.value

Parameters:

  • strategy (Symbol) —

    Controls concurrency strategy:

    • :sequential: Call the function sequentially
    • :thread: Use threads
    • :async: Use async tasks (requires async gem)
    • :fork: Use a forked child process (requires xchan.rb gem)
    • :fiber: Use scheduler-backed fibers (requires Fiber.scheduler)
    • :ractor: Use Ruby ractors

Returns:



295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
# File 'lib/llm/function.rb', line 295

def task(strategy, options = {})
  ioptions = @intercept&.options
  options  = options.merge(intercepted: @intercept&.call(function: self, **ioptions))
  options  = options.merge(tracer:, intercepted: options[:intercepted] || resolve(strategy))

  case strategy
  when :sequential
    Sequential::Task.new(self, options)
  when :async
    LLM.require "async" unless defined?(::Async)
    Async::Task.new(self, options)
  when :thread
    Thread::Task.new(self, options)
  when :fiber
    Fiber::Task.new(self, options)
  when :fork
    LLM.require "xchan", "~> 0.24" unless defined?(::Chan::UNIXSocket)
    Fork::Task.new(self, options)
  when :ractor
    unless Class === @runner || LLM::Tool === @runner
      raise LLM::RactorError,
            "Ractor concurrency only supports class-based tools\n" \
            "(function: #{@name}, runner: #{@runner.inspect})"
    end
    Ractor::Task.new(self, options.merge(runner_class: @runner, id:, name:, arguments:, model:))
  else
    raise ArgumentError, "Unknown strategy: #{strategy.inspect}. Expected :sequential, :thread, :fiber, :async, :fork, or :ractor"
  end
end

#cancel(reason: "function call cancelled", **extra) ⇒ LLM::Function::Return

Returns a value that communicates that the function call was cancelled

Examples:

llm = LLM.openai(key: ENV["KEY"])
ctx = LLM::Context.new(llm, tools: [fn1, fn2])
ctx.talk "I want to run the functions"
ctx.talk ctx.pending_functions.map(&:cancel)

Returns:



333
334
335
336
337
# File 'lib/llm/function.rb', line 333

def cancel(reason: "function call cancelled", **extra)
  Return.new(id, name, extra.merge(cancelled: true, reason:))
ensure
  @cancelled = true
end

#interrupt! ⇒ nil Also known as: cancel!

Notifies the function runner that the call was interrupted. This is cooperative and only applies to runners that implement on_cancel or on_interrupt.

Resolved through #runner rather than read off @runner, so that a class-backed tool is told on the object the call will run on. Read off the definition instead, a class answers respond_to? with false for both hooks, and a tool that implements one is never told.

An in-process strategy has already resolved the runner in #task, on the thread that built the task, so this reads a memo rather than creating one. Where there is no tool to tell - one that cannot be built, or a function whose task was never made - this is a no-op rather than a raise, because the call path is where a constructor's failure belongs.

Six routes reach the hook, and they differ in where it runs and in whether an interrupt has to have been delivered first:

  • :thread, :fiber and :async run it on the thread or fiber that ran the call, once the call has ended, and only where the raise was issued into a live job.
  • :fork and :ractor run it inside the child or the ractor, before the interrupt is delivered, and only for a message that arrived.
  • :sequential is the exception, and this method is how it is reached: nothing is raised into a sequential tool, so the hook is its only notification and it runs on the thread that cancelled, through LLM::Function::Sequential::Task#interrupt!.

A caller that reaches this directly - LLM::Context#interrupt! over pending functions, say - takes the last route, with no delivery to speak of and nothing running.

Returns:

  • (nil)


373
374
375
# File 'lib/llm/function.rb', line 373

def interrupt!
  LLM::Function.interrupt(runner_or_nil)
end

#called? ⇒ Boolean

Returns true when a function has been called

Returns:

  • (Boolean)


381
382
383
# File 'lib/llm/function.rb', line 381

def called?
  @called
end

#cancelled? ⇒ Boolean

Returns true when a function has been cancelled

Returns:

  • (Boolean)


388
389
390
# File 'lib/llm/function.rb', line 388

def cancelled?
  @cancelled
end

#skill? ⇒ Boolean

Returns true when this function is backed by a skill tool.

Returns:

  • (Boolean)


395
396
397
# File 'lib/llm/function.rb', line 395

def skill?
  @runner.respond_to?(:skill?) and @runner.skill?
end

#pending? ⇒ Boolean

Returns true when a function has neither been called nor cancelled

Returns:

  • (Boolean)


402
403
404
# File 'lib/llm/function.rb', line 402

def pending?
  !@called && !@cancelled
end

#unavailable ⇒ LLM::Function::Return

Returns an in-band error for an unresolved function call.



409
410
411
412
413
414
415
# File 'lib/llm/function.rb', line 409

def unavailable
  Return.new(id, name, {
    error: true,
    type: LLM::NoSuchToolError.name,
    message: "tool not found"
  })
end

#budget_spent ⇒ LLM::Function::Return

Returns an in-band error that indicates the tool call budget has been spent.



421
422
423
424
425
426
427
# File 'lib/llm/function.rb', line 421

def budget_spent
  cancel(
    reason: "you are making too many tool calls",
    action: ["stop requesting tool calls"],
    advice: ["produce a response from tool calls already made", "temporary error that resets on the next turn"]
  )
end

#mcp? ⇒ Boolean

Returns:

  • (Boolean)


431
432
433
# File 'lib/llm/function.rb', line 431

def mcp?
  Class === @runner and @runner.mcp?
end

#a2a? ⇒ Boolean

Returns:

  • (Boolean)


437
438
439
# File 'lib/llm/function.rb', line 437

def a2a?
  Class === @runner and @runner.a2a?
end

#return(value) ⇒ LLM::Function::Return

Parameters:

  • value (Hash) —

    The return content, eg {error: true, type: ..., message: ...}.

Returns:



452
453
454
# File 'lib/llm/function.rb', line 452

define_method(:return) do |value|
  Return.new(id, name, value)
end

#error(ex:, **extra) ⇒ LLM::Function::Return

Parameters:

  • ex (Exception) —

    An exception

  • extra (Hash) —

    Extra attributes to add to the error

Returns:



462
463
464
465
466
467
468
# File 'lib/llm/function.rb', line 462

def error(ex:, **extra)
  error = extra.merge({
    error: true, type: ex.class.to_s,
    message: ex.message, backtrace: ex.backtrace
  })
  self.return(error)
end

#adapt(provider) ⇒ Hash

Returns:

  • (Hash)


472
473
474
# File 'lib/llm/function.rb', line 472

def adapt(provider)
  provider.adapt_function(self)
end