From e06cf0c56f3342529f409f2d6a734608f3ebc545 Mon Sep 17 00:00:00 2001 From: Carmine Paolino Date: Sat, 6 Jun 2026 12:33:17 +0200 Subject: [PATCH] Support runtime schema values --- lib/ruby_llm/schema/json_output.rb | 23 ++++++- .../schema/robustness/runtime_values_spec.rb | 60 +++++++++++++++++++ 2 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 spec/ruby_llm/schema/robustness/runtime_values_spec.rb diff --git a/lib/ruby_llm/schema/json_output.rb b/lib/ruby_llm/schema/json_output.rb index 94c73f2..b5b3bba 100644 --- a/lib/ruby_llm/schema/json_output.rb +++ b/lib/ruby_llm/schema/json_output.rb @@ -23,8 +23,8 @@ 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 @@ -32,6 +32,25 @@ 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 diff --git a/spec/ruby_llm/schema/robustness/runtime_values_spec.rb b/spec/ruby_llm/schema/robustness/runtime_values_spec.rb new file mode 100644 index 0000000..805dc11 --- /dev/null +++ b/spec/ruby_llm/schema/robustness/runtime_values_spec.rb @@ -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