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
5 changes: 5 additions & 0 deletions gemfiles/rails_7.1.gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ GEM
concurrent-ruby (~> 1.1)
webrick (~> 1.7)
websocket-driver (~> 0.7)
ffi (1.17.2-arm64-darwin)
ffi (1.17.2-x86_64-linux-gnu)
fiber-annotation (0.2.0)
fiber-local (1.1.0)
Expand Down Expand Up @@ -224,6 +225,8 @@ GEM
net-smtp (0.5.1)
net-protocol
nio4r (2.7.4)
nokogiri (1.18.9-arm64-darwin)
racc (~> 1.4)
nokogiri (1.18.9-x86_64-linux-gnu)
racc (~> 1.4)
os (1.1.4)
Expand Down Expand Up @@ -355,6 +358,7 @@ GEM
simplecov (~> 0.19)
simplecov-html (0.13.2)
simplecov_json_formatter (0.1.4)
sqlite3 (2.7.3-arm64-darwin)
sqlite3 (2.7.3-x86_64-linux-gnu)
stringio (3.1.7)
thor (1.4.0)
Expand All @@ -380,6 +384,7 @@ GEM
zeitwerk (2.7.3)

PLATFORMS
arm64-darwin-24
x86_64-linux

DEPENDENCIES
Expand Down
5 changes: 5 additions & 0 deletions gemfiles/rails_7.2.gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ GEM
concurrent-ruby (~> 1.1)
webrick (~> 1.7)
websocket-driver (~> 0.7)
ffi (1.17.2-arm64-darwin)
ffi (1.17.2-x86_64-linux-gnu)
fiber-annotation (0.2.0)
fiber-local (1.1.0)
Expand Down Expand Up @@ -217,6 +218,8 @@ GEM
net-smtp (0.5.1)
net-protocol
nio4r (2.7.4)
nokogiri (1.18.9-arm64-darwin)
racc (~> 1.4)
nokogiri (1.18.9-x86_64-linux-gnu)
racc (~> 1.4)
os (1.1.4)
Expand Down Expand Up @@ -348,6 +351,7 @@ GEM
simplecov (~> 0.19)
simplecov-html (0.13.2)
simplecov_json_formatter (0.1.4)
sqlite3 (2.7.3-arm64-darwin)
sqlite3 (2.7.3-x86_64-linux-gnu)
stringio (3.1.7)
thor (1.4.0)
Expand All @@ -374,6 +378,7 @@ GEM
zeitwerk (2.7.3)

PLATFORMS
arm64-darwin-24
x86_64-linux

DEPENDENCIES
Expand Down
5 changes: 5 additions & 0 deletions gemfiles/rails_8.0.gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ GEM
concurrent-ruby (~> 1.1)
webrick (~> 1.7)
websocket-driver (~> 0.7)
ffi (1.17.2-arm64-darwin)
ffi (1.17.2-x86_64-linux-gnu)
fiber-annotation (0.2.0)
fiber-local (1.1.0)
Expand Down Expand Up @@ -217,6 +218,8 @@ GEM
net-smtp (0.5.1)
net-protocol
nio4r (2.7.4)
nokogiri (1.18.9-arm64-darwin)
racc (~> 1.4)
nokogiri (1.18.9-x86_64-linux-gnu)
racc (~> 1.4)
os (1.1.4)
Expand Down Expand Up @@ -348,6 +351,7 @@ GEM
simplecov (~> 0.19)
simplecov-html (0.13.2)
simplecov_json_formatter (0.1.4)
sqlite3 (2.7.3-arm64-darwin)
sqlite3 (2.7.3-x86_64-linux-gnu)
stringio (3.1.7)
thor (1.4.0)
Expand All @@ -374,6 +378,7 @@ GEM
zeitwerk (2.7.3)

PLATFORMS
arm64-darwin-24
x86_64-linux

DEPENDENCIES
Expand Down
10 changes: 9 additions & 1 deletion lib/ruby_llm/providers/gemini/tools.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,18 @@ def format_parameters(parameters)
{
type: 'OBJECT',
properties: parameters.transform_values do |param|
{
property = {
type: param_type_for_gemini(param.type),
description: param.description
}.compact

# Add items field for array types
if param.type.to_s.downcase == 'array'
item_type = param.item_type ? param_type_for_gemini(param.item_type) : 'STRING'
property[:items] = { type: item_type }
end

property
end,
required: parameters.select { |_, p| p.required }.keys.map(&:to_s)
}
Expand Down
7 changes: 6 additions & 1 deletion lib/ruby_llm/providers/openai/tools.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,15 @@ def tool_for(tool)
end

def param_schema(param)
{
schema = {
type: param.type,
description: param.description
}.compact

# Add items field for array types
schema[:items] = { type: param.item_type || 'string' } if param.type.to_s.downcase == 'array'

schema
end

def format_tool_calls(tool_calls)
Expand Down
5 changes: 3 additions & 2 deletions lib/ruby_llm/tool.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
module RubyLLM
# Parameter definition for Tool methods.
class Parameter
attr_reader :name, :type, :description, :required
attr_reader :name, :type, :description, :required, :item_type

def initialize(name, type: 'string', desc: nil, required: true)
def initialize(name, type: 'string', desc: nil, required: true, item_type: nil)
@name = name
@type = type
@description = desc
@required = required
@item_type = item_type
end
end

Expand Down
205 changes: 205 additions & 0 deletions spec/ruby_llm/providers/gemini/tools_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
# frozen_string_literal: true

require 'spec_helper'
require 'ruby_llm/tool'

RSpec.describe RubyLLM::Providers::Gemini::Tools do
let(:dummy_class) do
Class.new do
include RubyLLM::Providers::Gemini::Tools
end
end

let(:instance) { dummy_class.new }

describe '#format_parameters' do
context 'with array type parameter' do
it 'includes items field for array parameters with default STRING type' do
parameters = {
fields: RubyLLM::Parameter.new('fields', type: 'array', desc: 'List of fields to return')
}

result = instance.send(:format_parameters, parameters)

expect(result[:type]).to eq('OBJECT')
expect(result[:properties][:fields]).to eq(
type: 'ARRAY',
description: 'List of fields to return',
items: { type: 'STRING' }
)
end

it 'supports arrays of numbers' do
parameters = {
scores: RubyLLM::Parameter.new('scores', type: 'array', desc: 'List of scores', item_type: 'number')
}

result = instance.send(:format_parameters, parameters)

expect(result[:type]).to eq('OBJECT')
expect(result[:properties][:scores]).to eq(
type: 'ARRAY',
description: 'List of scores',
items: { type: 'NUMBER' }
)
end

it 'supports arrays of integers' do
parameters = {
ids: RubyLLM::Parameter.new('ids', type: 'array', desc: 'List of IDs', item_type: 'integer')
}

result = instance.send(:format_parameters, parameters)

expect(result[:type]).to eq('OBJECT')
expect(result[:properties][:ids]).to eq(
type: 'ARRAY',
description: 'List of IDs',
items: { type: 'NUMBER' }
)
end

it 'supports arrays of booleans' do
parameters = {
flags: RubyLLM::Parameter.new('flags', type: 'array', desc: 'List of flags', item_type: 'boolean')
}

result = instance.send(:format_parameters, parameters)

expect(result[:type]).to eq('OBJECT')
expect(result[:properties][:flags]).to eq(
type: 'ARRAY',
description: 'List of flags',
items: { type: 'BOOLEAN' }
)
end

it 'supports arrays of objects' do
parameters = {
users: RubyLLM::Parameter.new('users', type: 'array', desc: 'List of users', item_type: 'object')
}

result = instance.send(:format_parameters, parameters)

expect(result[:type]).to eq('OBJECT')
expect(result[:properties][:users]).to eq(
type: 'ARRAY',
description: 'List of users',
items: { type: 'OBJECT' }
)
end
end

context 'with mixed parameter types' do
it 'correctly formats all parameter types including arrays with items field' do
parameters = {
name: RubyLLM::Parameter.new('name', type: 'string', desc: 'Name field'),
count: RubyLLM::Parameter.new('count', type: 'integer', desc: 'Count field'),
active: RubyLLM::Parameter.new('active', type: 'boolean', desc: 'Active status'),
tags: RubyLLM::Parameter.new('tags', type: 'array', desc: 'List of tags'),
metadata: RubyLLM::Parameter.new('metadata', type: 'object', desc: 'Additional metadata')
}

result = instance.send(:format_parameters, parameters)

expect(result[:type]).to eq('OBJECT')

# String parameter - no items field
expect(result[:properties][:name]).to eq(
type: 'STRING',
description: 'Name field'
)

# Integer parameter - no items field
expect(result[:properties][:count]).to eq(
type: 'NUMBER',
description: 'Count field'
)

# Boolean parameter - no items field
expect(result[:properties][:active]).to eq(
type: 'BOOLEAN',
description: 'Active status'
)

# Array parameter - MUST have items field
expect(result[:properties][:tags]).to eq(
type: 'ARRAY',
description: 'List of tags',
items: { type: 'STRING' }
)

# Object parameter - no items field
expect(result[:properties][:metadata]).to eq(
type: 'OBJECT',
description: 'Additional metadata'
)
end
end

context 'with required and optional parameters' do
it 'correctly identifies required parameters' do
parameters = {
required_field: RubyLLM::Parameter.new('required_field', type: 'string', desc: 'Required', required: true),
optional_field: RubyLLM::Parameter.new('optional_field', type: 'string', desc: 'Optional', required: false),
required_array: RubyLLM::Parameter.new('required_array', type: 'array', desc: 'Required array',
required: true)
}

result = instance.send(:format_parameters, parameters)

expect(result[:required]).to contain_exactly('required_field', 'required_array')

# Also verify array parameter has items field
expect(result[:properties][:required_array]).to include(
type: 'ARRAY',
items: { type: 'STRING' }
)
end
end
end

describe '#format_tools' do
it 'formats tools with array parameters correctly for Gemini API' do
tool_class = Class.new(RubyLLM::Tool) do
def self.name
'DataProcessor'
end

description 'Process data with specific fields'
param :fields, type: 'array', desc: 'Fields to process', required: true
param :format, type: 'string', desc: 'Output format', required: false

def execute(fields:, format: 'json')
"Processed #{fields.length} fields in #{format} format"
end
end

tools = { data_processor: tool_class.new }
result = instance.send(:format_tools, tools)

expect(result).to be_an(Array)
expect(result.first[:functionDeclarations]).to be_an(Array)

function_decl = result.first[:functionDeclarations].first
expect(function_decl[:name]).to eq('data_processor')
expect(function_decl[:description]).to eq('Process data with specific fields')

# Verify array parameter has items field (this is the critical fix)
expect(function_decl[:parameters][:properties][:fields]).to eq(
type: 'ARRAY',
description: 'Fields to process',
items: { type: 'STRING' }
)

# Verify string parameter doesn't have items field
expect(function_decl[:parameters][:properties][:format]).to eq(
type: 'STRING',
description: 'Output format'
)

# Verify only required parameters are marked as required
expect(function_decl[:parameters][:required]).to contain_exactly('fields')
end
end
end
Loading