diff --git a/lib/ruby_llm/schema/dsl/primitive_types.rb b/lib/ruby_llm/schema/dsl/primitive_types.rb index f7571f3..f30f8b4 100644 --- a/lib/ruby_llm/schema/dsl/primitive_types.rb +++ b/lib/ruby_llm/schema/dsl/primitive_types.rb @@ -4,8 +4,8 @@ module RubyLLM class Schema module DSL module PrimitiveTypes - def string(name, description: nil, required: true, requires: nil, **options) - add_property(name, string_schema(description: description, **options), required: required, requires: requires) + def string(name, description: nil, required: true, requires: nil, **options, &block) + add_property(name, string_schema(description: description, **options, &block), required: required, requires: requires) end def number(name, description: nil, required: true, requires: nil, **options) diff --git a/lib/ruby_llm/schema/dsl/schema_builders.rb b/lib/ruby_llm/schema/dsl/schema_builders.rb index ba7a6a2..787c5cc 100644 --- a/lib/ruby_llm/schema/dsl/schema_builders.rb +++ b/lib/ruby_llm/schema/dsl/schema_builders.rb @@ -15,21 +15,28 @@ module SchemaBuilders }.freeze SchemaBlock = Struct.new(:schemas, :keywords, keyword_init: true) - def string_schema(description: nil, enum: nil, min_length: nil, max_length: nil, pattern: nil, **options) + def string_schema(description: nil, enum: nil, min_length: nil, max_length: nil, pattern: nil, **options, &block) metadata = extract_metadata!(options) format = options.delete(:format) const = options.delete(:const) { NOT_GIVEN } + content_encoding = options.delete(:content_encoding) + content_media_type = options.delete(:content_media_type) raise_unknown_options!("string", options) + schema_block = collect_schema_block(&block) if block_given? - add_const(metadata.merge({ + schema = add_const(metadata.merge({ type: "string", enum: enum, description: description, minLength: min_length, maxLength: max_length, pattern: pattern, - format: format + format: format, + contentEncoding: content_encoding, + contentMediaType: content_media_type }.compact), const) + + merge_schema_block_keywords(schema, schema_block) end def number_schema(description: nil, minimum: nil, maximum: nil, greater_than: nil, less_than: nil, **options) @@ -315,6 +322,10 @@ def collect_schema_block(&block) schema_block.keywords[:maxContains] = max unless max.nil? end + context.define_singleton_method(:content_schema) do |&blk| + schema_block.keywords[:contentSchema] = schema_builder.send(:collect_schemas_from_block, &blk).first + end + context.define_singleton_method(:description) do |value| schema_block.keywords[:description] = value end diff --git a/spec/ruby_llm/schema/properties/content_annotations_spec.rb b/spec/ruby_llm/schema/properties/content_annotations_spec.rb new file mode 100644 index 0000000..005419a --- /dev/null +++ b/spec/ruby_llm/schema/properties/content_annotations_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe RubyLLM::Schema, "content annotations" do + let(:schema_class) { Class.new(described_class) } + + it "supports contentEncoding and contentMediaType keywords" do + schema_class.string :payload, + content_encoding: "base64", + content_media_type: "application/json" + + expect(schema_class.properties[:payload]).to eq({ + type: "string", + contentEncoding: "base64", + contentMediaType: "application/json" + }) + end + + it "supports nested contentSchema blocks" do + schema_class.string :payload, + content_encoding: "base64", + content_media_type: "application/json" do + content_schema do + object do + title "Payload" + description "Decoded JSON payload" + + string :name + end + end + end + + payload_schema = schema_class.properties[:payload] + + expect(payload_schema[:contentSchema]).to include( + type: "object", + title: "Payload", + description: "Decoded JSON payload" + ) + expect(payload_schema[:contentSchema][:properties][:name]).to eq({type: "string"}) + end + + it "supports metadata on the encoded string node" do + schema_class.string :payload, title: "Encoded payload" do + content_schema do + object do + string :name + end + end + end + + payload_schema = schema_class.properties[:payload] + + expect(payload_schema[:title]).to eq("Encoded payload") + expect(payload_schema[:contentSchema][:properties][:name]).to eq({type: "string"}) + end +end