diff --git a/lib/ruby_llm/schema.rb b/lib/ruby_llm/schema.rb index 6f49d01..414fced 100644 --- a/lib/ruby_llm/schema.rb +++ b/lib/ruby_llm/schema.rb @@ -175,7 +175,7 @@ def method_missing(method_name, ...) end def respond_to_missing?(method_name, include_private = false) - %i[string number integer boolean array tuple 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 raw 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 bc71a14..5e483f5 100644 --- a/lib/ruby_llm/schema/dsl/complex_types.rb +++ b/lib/ruby_llm/schema/dsl/complex_types.rb @@ -32,6 +32,10 @@ def none_of(name, description: nil, required: true, requires: nil, **options, &b add_property(name, none_of_schema(description: description, **options, &block), required: required, requires: requires) end + def raw(name, schema, required: true, requires: nil) + add_property(name, normalize_raw_schema(schema), 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 8399692..14d1f43 100644 --- a/lib/ruby_llm/schema/dsl/schema_builders.rb +++ b/lib/ruby_llm/schema/dsl/schema_builders.rb @@ -203,7 +203,7 @@ def none_of_schema(description: nil, **options, &block) schema = metadata.merge({ description: description, - not: schemas.one? ? schemas.first : {anyOf: schemas} + not: schemas.length == 1 ? schemas.first : {anyOf: schemas} }.compact) merge_schema_block_keywords(schema, schema_block) @@ -240,6 +240,19 @@ def merge_core_keywords(schema, schema_class) schema.merge!(schema_class.schema_core_keywords) end + def normalize_raw_schema(schema) + case schema + when Hash + schema.each_with_object({}) do |(key, value), normalized| + normalized[key.is_a?(Symbol) ? key.to_s : key] = normalize_raw_schema(value) + end + when Array + schema.map { |value| normalize_raw_schema(value) } + else + schema + end + end + def raise_unknown_options!(type, options) return if options.empty? @@ -333,6 +346,18 @@ def collect_schema_block(&block) schema_block.keywords[:contentSchema] = schema_builder.send(:collect_schemas_from_block, &blk).first end + context.define_singleton_method(:any_schema) do + schema_block.schemas << true + end + + context.define_singleton_method(:no_schema) do + schema_block.schemas << false + end + + context.define_singleton_method(:raw) do |schema| + schema_block.schemas << schema_builder.send(:normalize_raw_schema, schema) + end + context.define_singleton_method(:description) do |value| schema_block.keywords[:description] = value end diff --git a/spec/ruby_llm/schema/properties/boolean_and_raw_schemas_spec.rb b/spec/ruby_llm/schema/properties/boolean_and_raw_schemas_spec.rb new file mode 100644 index 0000000..667e49e --- /dev/null +++ b/spec/ruby_llm/schema/properties/boolean_and_raw_schemas_spec.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe RubyLLM::Schema, "boolean and raw schemas" do + let(:schema_class) { Class.new(described_class) } + + it "supports true schemas inside composition" do + schema_class.any_of :value do + any_schema + string + end + + expect(schema_class.properties[:value]).to eq({ + anyOf: [ + true, + {type: "string"} + ] + }) + end + + it "supports false schemas inside composition" do + schema_class.none_of :value do + no_schema + end + + expect(schema_class.properties[:value]).to eq({ + not: false + }) + end + + it "supports raw schema properties" do + schema_class.raw :role, { + "type" => "string", + "const" => "admin" + } + + expect(schema_class.properties[:role]).to eq({ + "type" => "string", + "const" => "admin" + }) + end + + it "normalizes symbol keys inside raw schemas" do + schema_class.raw :payload, { + type: "object", + properties: { + role: {const: "admin"} + } + } + + expect(schema_class.properties[:payload]).to eq({ + "type" => "object", + "properties" => { + "role" => {"const" => "admin"} + } + }) + end + + it "supports anonymous raw schemas inside composition" do + schema_class.any_of :value do + raw type: "string", const: "admin" + integer + end + + expect(schema_class.properties[:value]).to eq({ + anyOf: [ + {"type" => "string", "const" => "admin"}, + {type: "integer"} + ] + }) + end +end