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
2 changes: 1 addition & 1 deletion lib/ruby_llm/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ def method_missing(method_name, ...)
end

def respond_to_missing?(method_name, include_private = false)
%i[string number integer boolean array object any_of one_of all_of none_of null].include?(method_name) || super
%i[string number integer boolean array tuple object any_of one_of all_of none_of null].include?(method_name) || super
end
end
end
4 changes: 4 additions & 0 deletions lib/ruby_llm/schema/dsl/complex_types.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ def array(name, description: nil, required: true, requires: nil, **options, &blo
add_property(name, array_schema(description: description, **options, &block), required: required, requires: requires)
end

def tuple(name, description: nil, required: true, requires: nil, **options, &block)
add_property(name, tuple_schema(description: description, **options, &block), required: required, requires: requires)
end

def any_of(name, description: nil, required: true, requires: nil, **options, &block)
add_property(name, any_of_schema(description: description, **options, &block), required: required, requires: requires)
end
Expand Down
34 changes: 29 additions & 5 deletions lib/ruby_llm/schema/dsl/schema_builders.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,17 +77,34 @@ def object_schema(description: nil, of: nil, reference: nil, min_properties: nil
schema
end

def array_schema(description: nil, of: nil, min_items: nil, max_items: nil, unevaluated_items: nil, &block)
items = determine_array_items(of, &block)
def array_schema(description: nil, of: nil, min_items: nil, max_items: nil, unevaluated_items: nil, unique: nil, &block)
schema_block = collect_schema_block(&block) if block_given?
items = determine_array_items(of, schema_block)

{
schema = {
type: "array",
description: description,
items: items,
minItems: min_items,
maxItems: max_items,
uniqueItems: unique,
unevaluatedItems: unevaluated_items
}.compact

schema.merge!(schema_block.keywords) if schema_block
schema
end

def tuple_schema(description: nil, &block)
schemas = collect_schemas_from_block(&block)

{
type: "array",
description: description,
prefixItems: schemas,
minItems: schemas.length,
maxItems: schemas.length
}.compact
end

def any_of_schema(description: nil, &block)
Expand Down Expand Up @@ -140,8 +157,9 @@ def raise_unknown_options!(type, options)
raise ArgumentError, "unknown #{type} schema option: #{options.keys.first.inspect}"
end

def determine_array_items(of, &)
return collect_schemas_from_block(&).first if block_given?
def determine_array_items(of, schema_block = nil)
return schema_block.schemas.first if schema_block&.schemas&.any?
return nil if schema_block
return send("#{of}_schema") if primitive_type?(of)
return reference(of) if of.is_a?(Symbol)
return schema_class_to_inline_schema(of) if schema_class?(of)
Expand Down Expand Up @@ -214,6 +232,12 @@ def collect_schema_block(&block)
schema_block.keywords[:unevaluatedItems] = value
end

context.define_singleton_method(:contains) do |min: nil, max: nil, &blk|
schema_block.keywords[:contains] = schema_builder.send(:collect_schemas_from_block, &blk).first
schema_block.keywords[:minContains] = min unless min.nil?
schema_block.keywords[:maxContains] = max unless max.nil?
end

# Allow Schema classes to be accessed in the context
context.define_singleton_method(:const_missing) do |name|
const_get(name) if const_defined?(name)
Expand Down
76 changes: 76 additions & 0 deletions spec/ruby_llm/schema/properties/array_validation_keywords_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# frozen_string_literal: true

require "spec_helper"

RSpec.describe RubyLLM::Schema, "array validation keywords" do
let(:schema_class) { Class.new(described_class) }

it "supports uniqueItems with homogeneous arrays" do
schema_class.array :tags, of: :string, unique: true

expect(schema_class.properties[:tags]).to eq({
type: "array",
items: {type: "string"},
uniqueItems: true
})
end

it "supports tuple schemas with prefixItems" do
schema_class.tuple :coordinates do
number description: "Latitude"
number description: "Longitude"
end

expect(schema_class.properties[:coordinates]).to eq({
type: "array",
prefixItems: [
{type: "number", description: "Latitude"},
{type: "number", description: "Longitude"}
],
minItems: 2,
maxItems: 2
})
end

it "supports tuple schemas with object entries" do
schema_class.tuple :event do
string description: "Event name"
integer description: "Unix timestamp"
object description: "Payload" do
string :id
string :source
end
end

event_schema = schema_class.properties[:event]

expect(event_schema[:prefixItems].length).to eq(3)
expect(event_schema[:prefixItems][2][:properties][:id]).to eq({type: "string"})
expect(event_schema[:minItems]).to eq(3)
expect(event_schema[:maxItems]).to eq(3)
end

it "supports contains with minContains and maxContains" do
schema_class.array :scores do
contains min: 1, max: 3 do
integer minimum: 10
end
end

expect(schema_class.properties[:scores]).to eq({
type: "array",
contains: {type: "integer", minimum: 10},
minContains: 1,
maxContains: 3
})
end

it "keeps existing homogeneous item behavior" do
schema_class.array :items, of: :integer

expect(schema_class.properties[:items]).to eq({
type: "array",
items: {type: "integer"}
})
end
end