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
9 changes: 9 additions & 0 deletions docs/_core_features/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,15 @@ end
> ```
{: .note }

> If a model attempts to call a tool that doesn't exist (sometimes called "tool hallucination"), RubyLLM handles this gracefully by:
>
> 1. Returning an error message to the model indicating which tool it tried to call
> 2. Listing the actually available tools
> 3. Allowing the conversation to continue so the model can correct itself
>
> This prevents crashes and gives the model a chance to use the correct tool or respond appropriately.
{: .note }

## Declaring Parameters

RubyLLM ships with two complementary approaches:
Expand Down
7 changes: 7 additions & 0 deletions lib/ruby_llm/chat.rb
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,13 @@ def handle_tool_calls(response, &) # rubocop:disable Metrics/PerceivedComplexity

def execute_tool(tool_call)
tool = tools[tool_call.name.to_sym]
if tool.nil?
return {
error: "Model tried to call unavailable tool `#{tool_call.name}`. " \
"Available tools: #{tools.keys.to_json}."
}
end

args = tool_call.arguments
tool.call(args)
end
Expand Down
34 changes: 34 additions & 0 deletions spec/ruby_llm/chat_tools_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,40 @@ def tool_result_message_for(chat, tool_call)
expect(response.content).to include('15')
expect(response.content).to include('10')
end

it "#{provider}/#{model} deals with non-existent tool calls" do
hallucinated_tool_call = RubyLLM::ToolCall.new(
id: 'call_1',
name: 'list_tools',
arguments: {}
)

tool_results_received = []

chat = RubyLLM.chat(model: model, provider: provider)
.with_tool(Weather)
.on_tool_result { |result| tool_results_received << result }

final_answer = 'The `list_tools` tool is not supported, but I see you have the `weather` tool.'
allow(chat.instance_variable_get(:@provider)).to receive(:complete).and_return(
RubyLLM::Message.new(
role: :assistant,
content: '',
tool_calls: { hallucinated_tool_call.id => hallucinated_tool_call }
),
RubyLLM::Message.new(
role: :assistant,
content: final_answer
)
)

response = chat.ask('What tools do you support?')
expect(response.content).to eq(final_answer)
expect(tool_results_received).to eq([
{ error: 'Model tried to call unavailable tool `list_tools`. ' \
'Available tools: ["weather"].' }
])
end
end

describe 'thought signatures' do
Expand Down