Tools

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

When to Use Tools

Use tools when your agent needs to fetch external data, call APIs, query databases, or perform side effects. If the agent only needs to generate text from its training data, you do not need tools.

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 context passed to agent.generate()
  # kwargs  - Validated parameters
  #
  # Must return a Riffer::Tools::Response
end

Accessing Context

The context argument receives whatever was passed as context: to generate:

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", 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