diff --git a/lib/ruby_llm/schema.rb b/lib/ruby_llm/schema.rb index 0846015..2dc571a 100644 --- a/lib/ruby_llm/schema.rb +++ b/lib/ruby_llm/schema.rb @@ -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 null].include?(method_name) || super + %i[string number integer boolean array object any_of one_of all_of none_of null].include?(method_name) || super end end end diff --git a/lib/ruby_llm/schema/dsl/complex_types.rb b/lib/ruby_llm/schema/dsl/complex_types.rb index e214f95..d5a0345 100644 --- a/lib/ruby_llm/schema/dsl/complex_types.rb +++ b/lib/ruby_llm/schema/dsl/complex_types.rb @@ -20,6 +20,14 @@ def one_of(name, description: nil, required: true, requires: nil, **options, &bl add_property(name, one_of_schema(description: description, **options, &block), required: required, requires: requires) end + def all_of(name, description: nil, required: true, requires: nil, **options, &block) + add_property(name, all_of_schema(description: description, **options, &block), required: required, requires: requires) + end + + def none_of(name, description: nil, required: true, requires: nil, **options, &block) + add_property(name, none_of_schema(description: description, **options, &block), required: required, requires: requires) + end + def optional(name, description: nil, &block) any_of(name, description: description) do instance_eval(&block) diff --git a/lib/ruby_llm/schema/dsl/schema_builders.rb b/lib/ruby_llm/schema/dsl/schema_builders.rb index 7b143ee..f955907 100644 --- a/lib/ruby_llm/schema/dsl/schema_builders.rb +++ b/lib/ruby_llm/schema/dsl/schema_builders.rb @@ -125,6 +125,24 @@ def one_of_schema(description: nil, &block) }.compact end + def all_of_schema(description: nil, &block) + schemas = collect_schemas_from_block(&block) + + { + description: description, + allOf: schemas + }.compact + end + + def none_of_schema(description: nil, &block) + schemas = collect_schemas_from_block(&block) + + { + description: description, + not: schemas.one? ? schemas.first : {anyOf: schemas} + }.compact + end + private def add_const(schema, const) diff --git a/spec/ruby_llm/schema/properties/all_of_spec.rb b/spec/ruby_llm/schema/properties/all_of_spec.rb new file mode 100644 index 0000000..94d6b0a --- /dev/null +++ b/spec/ruby_llm/schema/properties/all_of_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe RubyLLM::Schema, "allOf properties" do + let(:schema_class) { Class.new(described_class) } + + it "supports all_of with mixed schemas" do + schema_class.all_of :payload do + object do + string :name + end + + object do + integer :age + end + end + + all_of_schemas = schema_class.properties[:payload][:allOf] + + expect(all_of_schemas.length).to eq(2) + expect(all_of_schemas[0][:properties][:name]).to eq({type: "string"}) + expect(all_of_schemas[1][:properties][:age]).to eq({type: "integer"}) + end + + it "supports arrays of allOf types" do + schema_class.array :items do + all_of :value do + object do + string :name + end + + object do + integer :age + end + end + end + + all_of_schemas = schema_class.properties[:items][:items][:allOf] + + expect(all_of_schemas.length).to eq(2) + expect(all_of_schemas.map { |schema| schema[:type] }).to eq(%w[object object]) + end +end diff --git a/spec/ruby_llm/schema/properties/none_of_spec.rb b/spec/ruby_llm/schema/properties/none_of_spec.rb new file mode 100644 index 0000000..fb0e27a --- /dev/null +++ b/spec/ruby_llm/schema/properties/none_of_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe RubyLLM::Schema, "not properties" do + let(:schema_class) { Class.new(described_class) } + + it "supports none_of with a single schema" do + schema_class.none_of :status do + string enum: ["forbidden"] + end + + properties = schema_class.properties + + expect(properties[:status]).to eq({ + not: {type: "string", enum: ["forbidden"]} + }) + end + + it "wraps multiple none_of schemas in anyOf" do + schema_class.none_of :status do + string enum: ["forbidden"] + integer + end + + properties = schema_class.properties + + expect(properties[:status]).to eq({ + not: { + anyOf: [ + {type: "string", enum: ["forbidden"]}, + {type: "integer"} + ] + } + }) + end + + it "supports arrays of not schemas" do + schema_class.array :items do + none_of :value do + null + end + end + + item_schema = schema_class.properties[:items][:items] + + expect(item_schema).to eq({not: {type: "null"}}) + end +end