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 @@ -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
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 @@ -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)
Expand Down
27 changes: 26 additions & 1 deletion lib/ruby_llm/schema/dsl/schema_builders.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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?

Expand Down Expand Up @@ -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
Expand Down
73 changes: 73 additions & 0 deletions spec/ruby_llm/schema/properties/boolean_and_raw_schemas_spec.rb
Original file line number Diff line number Diff line change
@@ -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