Tools

Tools are callable functions that agents can invoke to interact with external systems, fetch data, or perform actions.

Defining a Tool

Create a tool by subclassing Riffer::Tool:

class WeatherTool < Riffer::Tool
  description "Gets the current weather for a city"

  params do
    required :city, String, description: "The city name"
    optional :units, String, default: "celsius", enum: ["celsius", "fahrenheit"]
  end

  def call(context:, city:, units: nil)
    weather = WeatherAPI.fetch(city, units: units || "celsius")
    text("The weather in #{city} is #{weather.temperature} #{units}.")
  end
end

Configuration Methods

description

Sets a description that helps the LLM understand when to use the tool:

class SearchTool < Riffer::Tool
  description "Searches the knowledge base for relevant information"
end

identifier / name

Sets a custom identifier (defaults to snake_case class name):

class SearchTool < Riffer::Tool
  identifier 'kb_search'
end

SearchTool.identifier  # => "kb_search"
SearchTool.name        # => "kb_search" (alias)

params

Defines the tool’s parameters using a DSL:

class CreateOrderTool < Riffer::Tool
  params do
    required :product_id, Integer, description: "The product ID"
    required :quantity, Integer, description: "Number of items"
    optional :notes, String, description: "Order notes"
    optional :priority, String, default: "normal", enum: ["low", "normal", "high"]
  end
end

Parameter DSL

required

Defines a required parameter:

params do
  required :name, String, description: "The user's name"
  required :age, Integer, description: "The user's age"
end

Options:

optional

Defines an optional parameter:

params do
  optional :limit, Integer, default: 10, description: "Max results"
  optional :format, String, enum: ["json", "xml"], description: "Output format"
end

Options:

Supported Types

Ruby Type JSON Schema Type
String string
Integer integer
Float number
Riffer::Boolean boolean
TrueClass / FalseClass boolean
Array array
Hash object

Riffer::Boolean is the preferred way to declare boolean parameters. TrueClass and FalseClass continue to work for backwards compatibility.

Nested Parameters

Tool params support the same nested DSL as structured output β€” nested objects (Hash with block), typed arrays (Array, of:), and arrays of objects (Array with block). See the structured output section in Agents for full syntax.

class CreateOrderTool < Riffer::Tool
  description "Creates an order"

  params do
    required :items, Array, description: "Line items" do
      required :product_id, Integer
      required :quantity, Integer
      optional :notes, String
    end
    required :shipping, Hash, description: "Shipping address" do
      required :street, String
      required :city, String
      optional :zip, String
    end
  end

  def call(context:, items:, shipping:)
    # items is an Array of Hashes with symbolized keys
    # shipping is a Hash with symbolized keys
  end
end

The call Method

Every tool must implement the call method and return a Riffer::Tools::Response:

def call(context:, **kwargs)
  # context - The tool_context passed to agent.generate()
  # kwargs  - Validated parameters
  #
  # Must return a Riffer::Tools::Response
end

Accessing Context

The context argument receives whatever was passed to tool_context:

class UserOrdersTool < Riffer::Tool
  description "Gets the current user's orders"

  def call(context:)
    user_id = context&.dig(:user_id)
    unless user_id
      return error("No user ID provided")
    end

    orders = Order.where(user_id: user_id)
    text(orders.map(&:to_s).join("\n"))
  end
end

# Usage
agent.generate("Show my orders", tool_context: {user_id: 123})

Response Objects

All tools must return a Riffer::Tools::Response object from their call method. Riffer::Tool provides shorthand methods for creating responses.

Success Responses

Use text for string responses and json for structured data:

def call(context:, query:)
  results = Database.search(query)

  if results.empty?
    text("No results found for '#{query}'")
  else
    text(results.map { |r| "- #{r.title}: #{r.summary}" }.join("\n"))
  end
end

text

Converts the result to a string via to_s:

text("Hello, world!")
# => content: "Hello, world!"

text(42)
# => content: "42"

json

Converts the result to JSON via to_json:

json({name: "Alice", age: 30})
# => content: '{"name":"Alice","age":30}'

json([1, 2, 3])
# => content: '[1,2,3]'

Error Responses

Use error(message, type:) for errors:

def call(context:, user_id:)
  user = User.find_by(id: user_id)

  unless user
    return error("User not found", type: :not_found)
  end

  text("User: #{user.name}")
end

The error type is any symbol that describes the error category:

error("Invalid input", type: :validation_error)
error("Service unavailable", type: :service_error)
error("Rate limit exceeded", type: :rate_limit)

If no type is specified, it defaults to :execution_error.

Using Riffer::Tools::Response Directly

The shorthand methods delegate to Riffer::Tools::Response. You can also use the class directly if preferred:

Riffer::Tools::Response.text("Hello")
Riffer::Tools::Response.json({data: [1, 2, 3]})
Riffer::Tools::Response.error("Failed", type: :custom_error)

Response Methods

response = text("result")
response.content        # => "result"
response.success?       # => true
response.error?         # => false
response.error_message  # => nil
response.error_type     # => nil

error_response = error("failed", type: :not_found)
error_response.content        # => "failed"
error_response.success?       # => false
error_response.error?         # => true
error_response.error_message  # => "failed"
error_response.error_type     # => :not_found

Timeout Configuration

Configure timeouts to prevent tools from running indefinitely. The default timeout is 10 seconds.

class SlowExternalApiTool < Riffer::Tool
  description "Calls a slow external API"
  timeout 30  # 30 seconds

  def call(context:, query:)
    result = ExternalAPI.search(query)
    text(result)
  end
end

When a tool times out, the error is reported to the LLM with error type :timeout_error, allowing it to respond appropriately (e.g., suggest retrying or using a different approach).

Validation

Arguments are automatically validated before call is invoked:

Validation errors are captured and sent back to the LLM as tool results with error type :validation_error.

JSON Schema Generation

Riffer automatically generates JSON Schema for each tool:

WeatherTool.parameters_schema
# => {
#   type: "object",
#   properties: {
#     "city" => {type: "string", description: "The city name"},
#     "units" => {type: "string", enum: ["celsius", "fahrenheit"]}
#   },
#   required: ["city"],
#   additionalProperties: false
# }

Registering Tools with Agents

Static Registration

class MyAgent < Riffer::Agent
  model 'openai/gpt-4o'
  uses_tools [WeatherTool, SearchTool]
end

Dynamic Registration

Use a lambda for context-aware tool resolution:

class MyAgent < Riffer::Agent
  model 'openai/gpt-4o'

  uses_tools ->(context) {
    tools = [PublicSearchTool]

    if context&.dig(:user)&.premium?
      tools << PremiumAnalyticsTool
    end

    if context&.dig(:user)&.admin?
      tools << AdminTool
    end

    tools
  }
end

Error Handling

Errors can be returned explicitly using error:

def call(context:, query:)
  results = ExternalAPI.search(query)
  json(results)
rescue RateLimitError => e
  error("API rate limit exceeded, please try again later", type: :rate_limit)
rescue => e
  error("Search failed: #{e.message}")
end

Unhandled exceptions are caught by Riffer and converted to error responses with type :execution_error. However, it’s recommended to handle expected errors explicitly for better error messages.

The LLM receives the error message and can decide how to respond (retry, apologize, ask for different input, etc.).