Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions docs/_core_features/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,58 @@ search_tool = DocumentSearch.new(MyDatabase)
chat.with_tool(search_tool)
```

## Instance-Level Customization

Tools can be customized at the instance level, overriding class-level definitions:

### Customizing Description and Parameters

```ruby
# Different descriptions for different contexts
berlin_weather = Weather.new(description: "Get current weather in Berlin")
paris_weather = Weather.new(description: "Get current weather in Paris")
Comment on lines +260 to +261
Copy link

@SpaYco SpaYco Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the current implementation, this would fail without giving a name, as both would have the same name and get overwritten.

I think the current name approach is great, as this won't break the overwriting functionality, but we need to be clear in the docs.

```

### Using Unique Names for Multiple Instances

When using multiple instances of the same tool class, provide unique names to avoid collisions:

```ruby
berlin_tool = Weather.new(
name: 'berlin_weather',
description: 'Get Berlin weather'
)
paris_tool = Weather.new(
name: 'paris_weather',
description: 'Get Paris weather'
)

chat = RubyLLM.chat.with_tools(berlin_tool, paris_tool)
# Both tools available: chat.tools[:berlin_weather] and chat.tools[:paris_weather]
```

> Without unique names, multiple instances of the same class will override each other since tools are stored by name. Always provide a `name:` when using multiple instances.
{: .warning }

### Available Customization Options

| Parameter | Description |
|-----------|-------------|
| `name:` | Custom tool name (default: derived from class name) |
| `description:` | Tool description for the AI model |
| `parameters:` | Hash of `Parameter` objects defining inputs |
| `provider_params:` | Provider-specific metadata (e.g., `cache_control`) |

### Post-Initialization Customization

All options can also be set after initialization:

```ruby
tool = Weather.new
tool.name = 'custom_weather'
tool.description = 'Custom description'
```

## Using Tools in Chat

Attach tools to a `Chat` instance using `with_tool` or `with_tools`.
Expand Down
45 changes: 42 additions & 3 deletions lib/ruby_llm/tool.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,21 @@ def provider_params
end
end

# Initialize with optional instance-level overrides
# @param name [String, nil] Override the computed tool name (useful for unique identification)
# @param description [String, nil] Override the class-level description
# @param parameters [Hash, nil] Override the class-level parameters
# @param provider_params [Hash, nil] Override the class-level provider_params
def initialize(name: nil, description: nil, parameters: nil, provider_params: nil)
@instance_name = name
@instance_description = description
@instance_parameters = parameters
@instance_provider_params = provider_params
end

def name
return @instance_name if @instance_name

klass_name = self.class.name
normalized = klass_name.to_s.dup.force_encoding('UTF-8').unicode_normalize(:nfkd)
normalized.encode('ASCII', replace: '')
Expand All @@ -74,18 +88,43 @@ def name
end

def description
self.class.description
@instance_description || self.class.description
end

def parameters
self.class.parameters
@instance_parameters || self.class.parameters
end

def provider_params
self.class.provider_params
@instance_provider_params || self.class.provider_params
end

# Instance-level setters for customization after initialization
def name=(value)
@instance_name = value
end

def description=(value)
@instance_description = value
end

def parameters=(value)
@instance_parameters = value
end

def provider_params=(value)
@instance_provider_params = value
end

def params_schema
# If instance parameters are set, generate schema from them (not memoized)
if @instance_parameters
return SchemaDefinition.from_parameters(@instance_parameters)&.json_schema if @instance_parameters.any?

return nil
end
Comment on lines +121 to +125
Copy link

@SpaYco SpaYco Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if @instance_parameters
return SchemaDefinition.from_parameters(@instance_parameters)&.json_schema if @instance_parameters.any?
return nil
end
if @instance_parameters
return SchemaDefinition.from_parameters(@instance_parameters)&.json_schema
end

return nil if parameters.nil? || parameters.empty?

SchemaDefinition.from_parameters would return nil if the value is nil or empty.

we can also just one line it

Suggested change
if @instance_parameters
return SchemaDefinition.from_parameters(@instance_parameters)&.json_schema if @instance_parameters.any?
return nil
end
return SchemaDefinition.from_parameters(@instance_parameters)&.json_schema if @instance_parameters


# Otherwise use class-level schema (memoized)
return @params_schema if defined?(@params_schema)

@params_schema = begin
Expand Down
69 changes: 69 additions & 0 deletions spec/ruby_llm/chat_tools_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -624,4 +624,73 @@ def tool_result_message_for(chat, tool_call)
end
end
end

describe 'instance-level tool customization' do
it 'uses instance-customized tool with chat' do
custom_tool = Weather.new(description: 'Gets weather for Berlin only')

chat = RubyLLM.chat.with_tool(custom_tool)
tool_instance = chat.tools[:weather]

expect(tool_instance.description).to eq('Gets weather for Berlin only')
end

it 'allows multiple customized instances of the same tool class' do
berlin_params = {
district: RubyLLM::Parameter.new(:district, type: 'string', desc: 'Berlin district', required: true)
}

berlin_tool = Weather.new(
description: 'Gets Berlin weather by district'
)
paris_tool = Weather.new(
description: 'Gets Paris weather by arrondissement'
)

chat = RubyLLM.chat.with_tools(berlin_tool, paris_tool)

# Note: Both tools have the same name since they're the same class
# The second one will override the first in the tools hash
expect(chat.tools[:weather].description).to eq('Gets Paris weather by arrondissement')
end

it 'allows multiple instances of the same class with unique names' do
berlin_tool = Weather.new(name: 'berlin_weather', description: 'Berlin weather')
paris_tool = Weather.new(name: 'paris_weather', description: 'Paris weather')

chat = RubyLLM.chat.with_tools(berlin_tool, paris_tool)

expect(chat.tools.keys).to contain_exactly(:berlin_weather, :paris_weather)
expect(chat.tools[:berlin_weather].description).to eq('Berlin weather')
expect(chat.tools[:paris_weather].description).to eq('Paris weather')
end

it 'uses customized provider_params from instance' do
custom_tool = Weather.new(provider_params: { cache_control: { type: 'ephemeral' } })

chat = RubyLLM.chat.with_tool(custom_tool)
tool_instance = chat.tools[:weather]

expect(tool_instance.provider_params).to eq({ cache_control: { type: 'ephemeral' } })
end

it 'preserves instance customization after adding to chat' do
custom_params = {
city: RubyLLM::Parameter.new(:city, type: 'string', desc: 'City name', required: true)
}

custom_tool = Weather.new(
description: 'Custom weather tool',
parameters: custom_params,
provider_params: { timeout: 30 }
)

chat = RubyLLM.chat.with_tool(custom_tool)
tool_instance = chat.tools[:weather]

expect(tool_instance.description).to eq('Custom weather tool')
expect(tool_instance.parameters).to eq(custom_params)
expect(tool_instance.provider_params).to eq({ timeout: 30 })
end
end
end
Loading