Skip to content

Support nested parameters for tools #112

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
Closed
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
17 changes: 13 additions & 4 deletions lib/ruby_llm/providers/anthropic/tools.rb
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,22 @@ def parse_tool_calls(content_block)

def clean_parameters(parameters)
parameters.transform_values do |param|
{
type: param.type,
description: param.description
}.compact
clean_parameter(param)
Copy link

@jayelkaake jayelkaake Apr 19, 2025

Choose a reason for hiding this comment

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

We should just let the parameter class return the schema. This way it is more forward compatible when LLMs add schema support (like minItems, etc)

Here's an implementation of RubyLLM::Parameter that is reverse compatible and can simply get .schema on the parameter:

# In `lib/ruby_llm/providers/*/tools.rb`:
##
# @param parameters [Hash{String => Parameters}]
# @return [Hash]
def clean_parameters(parameters)
  parameters.transform_values(&:schema)
end
# In `lib/ruby_llm/tool.rb`:
# ...
  ##
  # @!attribute name [r]
  #   @return [Symbol]
  # @!attribute required [r]
  #   @return [Boolean]
  # @!attribute schema [r]
  #   @return [Hash{String => Object}]
  class Parameter
    attr_reader :name, :required, :schema

    ##
    # If providing schema directly MAKE SURE TO USE STRING KEYS.
    # Also note that under_scored keys are NOT automatically transformed to camelCase.
    # @param name [Symbol]
    # @param schema [Hash{String => Object}, NilClass] 
    # @option type [String] (default: `'string'`)
    # @options required [Boolean] (default: `true`)
    # @options desc [String]
    def initialize(name, schema = nil, required: true, type: 'string', desc: nil)
      @name = name
      @schema = schema.to_h

      @schema['type'] ||= type
      @schema['description'] ||= desc if desc.present?
      @required = required
    end

    ##
    # @return [String]
    def type
      @schema['type']
    end

    ##
    # @return [String, NilClass]
    def description
      @schema['description']
    end

    alias required? required
  end
# ...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Love it! Did you want to submit a separate PR or should I try to get it into this one?

Choose a reason for hiding this comment

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

Uhh I'm realizing maybe I should just do a separate PR since I should test the code thoroughly.

I realized my solution is now using string and symbol keys so just want to ensure we're not breaking anything 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Feel free to borrow from this one as much as you need.

end
end

def clean_parameter(param)
{
type: param.type,
description: param.description,
items: param.items && {
type: 'object',
properties: clean_parameters(param.items)
},
properties: param.properties && clean_parameters(param.properties)
}.compact
end

def required_parameters(parameters)
parameters.select { |_, param| param.required }.keys
end
Expand Down
28 changes: 22 additions & 6 deletions lib/ruby_llm/providers/gemini/tools.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,16 +62,32 @@ def function_declaration_for(tool)
def format_parameters(parameters)
{
type: 'OBJECT',
properties: parameters.transform_values do |param|
{
type: param_type_for_gemini(param.type),
description: param.description
}.compact
end,
properties: parameters.transform_values { |param| format_parameter(param) },
required: parameters.select { |_, p| p.required }.keys.map(&:to_s)
}
end

def format_parameter(param)
{
type: param_type_for_gemini(param.type),
description: param.description,
items: param.items && {
type: 'OBJECT',
properties: format_nested_parameters(param.items)
},
properties: param.properties && format_nested_parameters(param.properties)
}.compact
end

def format_nested_parameters(parameters)
parameters.transform_values do |param|
{
type: param_type_for_gemini(param.type),
description: param.description
}.compact
end
end

# Convert RubyLLM param types to Gemini API types
def param_type_for_gemini(type)
case type.to_s.downcase
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 @@ -25,7 +25,12 @@ def tool_for(tool) # rubocop:disable Metrics/MethodLength
def param_schema(param)
{
type: param.type,
description: param.description
description: param.description,
items: param.items && {
type: 'object',
properties: param.items.transform_values { |p| param_schema(p) }
},
properties: param.properties&.transform_values { |p| param_schema(p) }
}.compact
end

Expand Down
12 changes: 7 additions & 5 deletions lib/ruby_llm/tool.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ module RubyLLM
# Parameter definition for Tool methods. Specifies type constraints,
# descriptions, and whether parameters are required.
class Parameter
attr_reader :name, :type, :description, :required
attr_reader :name, :type, :description, :required, :items, :properties

def initialize(name, type: 'string', desc: nil, required: true)
def initialize(name, **options)
@name = name
@type = type
@description = desc
@required = required
@type = options.fetch(:type, 'string')
@description = options.fetch(:desc, nil)
@required = options.fetch(:required, true)
@items = options[:items]&.transform_values { |v| Parameter.new(v[:name], **v) }
@properties = options[:properties]&.transform_values { |v| Parameter.new(v[:name], **v) }
end
end

Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading