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
23 changes: 21 additions & 2 deletions lib/ruby_llm/schema/json_output.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,34 @@ def to_json_schema

{
name: @name,
description: @description || self.class.description,
schema: schema_hash
description: resolve_runtime_values(@description || self.class.description),
schema: resolve_runtime_values(schema_hash)
}
end

def to_json(*_args)
validate! # Validate schema before generating JSON string
JSON.pretty_generate(to_json_schema)
end

private

def resolve_runtime_values(value)
case value
when Proc
resolve_runtime_values(evaluate_runtime_value(value))
when Hash
value.transform_values { |nested_value| resolve_runtime_values(nested_value) }
when Array
value.map { |nested_value| resolve_runtime_values(nested_value) }
else
value
end
end

def evaluate_runtime_value(value)
value.arity.zero? ? instance_exec(&value) : value.call(self)
end
end
end
end
60 changes: 60 additions & 0 deletions spec/ruby_llm/schema/robustness/runtime_values_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# frozen_string_literal: true

require "spec_helper"

RSpec.describe RubyLLM::Schema, "runtime schema values" do
it "evaluates proc-backed values when rendering JSON schema" do
schema_class = Class.new(described_class) do
def initialize(user_names:)
super()
@user_names = user_names
end

string :name, enum: -> { @user_names }
end

first_schema = schema_class.new(user_names: %w[Alice Bob]).to_json_schema
second_schema = schema_class.new(user_names: %w[Carol]).to_json_schema

expect(first_schema[:schema][:properties][:name][:enum]).to eq(%w[Alice Bob])
expect(second_schema[:schema][:properties][:name][:enum]).to eq(%w[Carol])
expect(schema_class.properties[:name][:enum]).to be_a(Proc)
end

it "evaluates runtime values inside nested schemas" do
schema_class = Class.new(described_class) do
def initialize(user_names:)
super()
@user_names = user_names
end

array :users do
object do
string :name, enum: -> { @user_names }
end
end
end

output = schema_class.new(user_names: %w[Alice Bob]).to_json_schema
name_schema = output[:schema][:properties][:users][:items][:properties][:name]

expect(name_schema[:enum]).to eq(%w[Alice Bob])
end

it "supports procs that receive the schema instance" do
schema_class = Class.new(described_class) do
attr_reader :allowed_roles

def initialize(allowed_roles:)
super()
@allowed_roles = allowed_roles
end

string :role, enum: ->(schema) { schema.allowed_roles.dup }
end

output = schema_class.new(allowed_roles: %w[admin user]).to_json_schema

expect(output[:schema][:properties][:role][:enum]).to eq(%w[admin user])
end
end