Skip to content

Commit 9241875

Browse files
committed
Fix handling of multiple tool calls in single LLM response
Previously, the library only processed the first tool call when an LLM returned multiple tool_use blocks in a single response. This limitation prevented parallel function execution, a key feature of modern LLMs. Changes: - Update Anthropic provider to extract ALL tool_use blocks from responses - Modify tool parser to handle arrays of content blocks - Ensure all tools execute before continuing the conversation - Add comprehensive tests for multi-tool scenarios across all providers This enables use cases like: - Executing multiple independent operations in parallel - Rolling dice multiple times in one request - Fetching data from multiple sources simultaneously The implementation maintains backward compatibility while extending support for advanced parallel tool calling capabilities. Fixes the limitation where only the first tool call was processed when multiple were requested.
1 parent a35aa11 commit 9241875

10 files changed

+1293
-15
lines changed

lib/ruby_llm/providers/anthropic/chat.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,21 +55,21 @@ def parse_completion_response(response)
5555
content_blocks = data['content'] || []
5656

5757
text_content = extract_text_content(content_blocks)
58-
tool_use = Tools.find_tool_use(content_blocks)
58+
tool_use_blocks = content_blocks.select { |c| c['type'] == 'tool_use' }
5959

60-
build_message(data, text_content, tool_use)
60+
build_message(data, text_content, tool_use_blocks)
6161
end
6262

6363
def extract_text_content(blocks)
6464
text_blocks = blocks.select { |c| c['type'] == 'text' }
6565
text_blocks.map { |c| c['text'] }.join
6666
end
6767

68-
def build_message(data, content, tool_use)
68+
def build_message(data, content, tool_use_blocks)
6969
Message.new(
7070
role: :assistant,
7171
content: content,
72-
tool_calls: Tools.parse_tool_calls(tool_use),
72+
tool_calls: Tools.parse_tool_calls(tool_use_blocks),
7373
input_tokens: data.dig('usage', 'input_tokens'),
7474
output_tokens: data.dig('usage', 'output_tokens'),
7575
model_id: data['model']

lib/ruby_llm/providers/anthropic/tools.rb

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@ def find_tool_use(blocks)
1212
end
1313

1414
def format_tool_call(msg)
15-
tool_call = msg.tool_calls.values.first
16-
1715
content = []
16+
1817
content << Media.format_text(msg.content) unless msg.content.nil? || msg.content.empty?
19-
content << format_tool_use_block(tool_call)
18+
19+
msg.tool_calls.each_value do |tool_call|
20+
content << format_tool_use_block(tool_call)
21+
end
2022

2123
{
2224
role: 'assistant',
@@ -68,16 +70,24 @@ def extract_tool_calls(data)
6870
end
6971
end
7072

71-
def parse_tool_calls(content_block)
72-
return nil unless content_block && content_block['type'] == 'tool_use'
73+
def parse_tool_calls(content_blocks)
74+
return nil if content_blocks.nil?
7375

74-
{
75-
content_block['id'] => ToolCall.new(
76-
id: content_block['id'],
77-
name: content_block['name'],
78-
arguments: content_block['input']
76+
# Handle single content block (backward compatibility)
77+
content_blocks = [content_blocks] unless content_blocks.is_a?(Array)
78+
79+
tool_calls = {}
80+
content_blocks.each do |block|
81+
next unless block && block['type'] == 'tool_use'
82+
83+
tool_calls[block['id']] = ToolCall.new(
84+
id: block['id'],
85+
name: block['name'],
86+
arguments: block['input']
7987
)
80-
}
88+
end
89+
90+
tool_calls.empty? ? nil : tool_calls
8191
end
8292

8393
def clean_parameters(parameters)

spec/fixtures/vcr_cassettes/chat_function_calling_anthropic_claude-3-5-haiku-20241022_can_handle_multiple_tool_calls_in_a_single_response.yml

Lines changed: 170 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

spec/fixtures/vcr_cassettes/chat_function_calling_bedrock_anthropic_claude-3-5-haiku-20241022-v1_0_can_handle_multiple_tool_calls_in_a_single_response.yml

Lines changed: 116 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)