Skip to content

Commit 4537bb0

Browse files
committed
Lots of work
1 parent 16ec048 commit 4537bb0

File tree

7 files changed

+455
-20
lines changed

7 files changed

+455
-20
lines changed

example_inline_schemas.rb

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
4+
require_relative "lib/ruby_llm/schema"
5+
6+
# Define reusable schema classes
7+
class PersonSchema < RubyLLM::Schema
8+
string :name, description: "Person's full name"
9+
integer :age, description: "Person's age"
10+
end
11+
12+
class AddressSchema < RubyLLM::Schema
13+
string :street, description: "Street address"
14+
string :city, description: "City name"
15+
string :zipcode, description: "Postal code"
16+
end
17+
18+
# Define a CompanySchema using inline schema insertion
19+
class CompanySchema < RubyLLM::Schema
20+
string :name, description: "Company name"
21+
22+
# Arrays with inline schema insertion
23+
array :employees, of: PersonSchema, description: "Company employees"
24+
25+
# Objects with inline schema insertion
26+
object :founder, of: PersonSchema, description: "Company founder"
27+
object :headquarters, of: AddressSchema, description: "Main office"
28+
29+
# Mixed usage - Schema.new in blocks still works
30+
object :ceo do
31+
PersonSchema.new
32+
end
33+
34+
# Users can still use explicit definitions when they want shared references
35+
define :department do
36+
string :name
37+
integer :employee_count
38+
end
39+
40+
array :departments, of: :department, description: "Company departments"
41+
end
42+
43+
# Generate and display the JSON schema
44+
company = CompanySchema.new("CompanyExample")
45+
json_output = company.to_json_schema
46+
47+
puts "=== Inline Schema Example ==="
48+
puts "Notice how PersonSchema and AddressSchema are embedded directly where used,"
49+
puts "while :department uses a shared definition.\n\n"
50+
51+
puts JSON.pretty_generate(json_output)

lib/ruby_llm/schema.rb

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,21 @@ def valid?
6262
end
6363
end
6464

65-
def initialize(name = nil, description: nil)
65+
def self.new(*args, **kwargs, &block)
66+
# Only return the class itself when called within a schema block context for embedding
67+
# This is determined by checking if we're being called with no arguments in a specific context
68+
if args.empty? && kwargs.empty? && block.nil? && caller.any? { |line| line.include?("class_eval") }
69+
self
70+
else
71+
instance = allocate
72+
instance.send(:initialize, *args, **kwargs)
73+
instance
74+
end
75+
end
76+
77+
def initialize(name = nil, description: nil, **kwargs)
6678
@name = name || self.class.name || "Schema"
67-
@description = description
79+
@description = description || kwargs[:description]
6880
end
6981

7082
def validate!

lib/ruby_llm/schema/dsl/schema_builders.rb

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,19 @@ def null_schema(description: nil)
4444
{type: "null", description: description}.compact
4545
end
4646

47-
def object_schema(description: nil, reference: nil, &block)
48-
if reference
49-
reference(reference)
47+
def object_schema(description: nil, of: nil, &block)
48+
if of
49+
determine_object_reference(of, description)
5050
else
5151
sub_schema = Class.new(Schema)
5252
result = sub_schema.class_eval(&block)
5353

5454
# If the block returned a reference and no properties were added, use the reference
5555
if result.is_a?(Hash) && result["$ref"] && sub_schema.properties.empty?
5656
result.merge(description ? {description: description} : {})
57+
# If the block returned a Schema class instance, convert it to reference
58+
elsif schema_class?(result) && sub_schema.properties.empty?
59+
schema_class_to_inline_schema(result).merge(description ? {description: description} : {})
5760
else
5861
{
5962
type: "object",

lib/ruby_llm/schema/dsl/utilities.rb

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,26 @@ def determine_array_items(of, &)
3636
return collect_schemas_from_block(&).first if block_given?
3737
return send("#{of}_schema") if primitive_type?(of)
3838
return reference(of) if of.is_a?(Symbol)
39+
return schema_class_to_inline_schema(of) if schema_class?(of)
3940

40-
raise InvalidArrayTypeError, of
41+
raise InvalidArrayTypeError, "Invalid array type: #{of.inspect}. Must be a primitive type (:string, :number, etc.), a symbol reference, or a Schema class."
42+
end
43+
44+
def determine_object_reference(of, description = nil)
45+
result = case of
46+
when Symbol
47+
reference(of)
48+
when Class
49+
if schema_class?(of)
50+
schema_class_to_inline_schema(of)
51+
else
52+
raise InvalidObjectTypeError, "Invalid object type: #{of.inspect}. Class must inherit from RubyLLM::Schema."
53+
end
54+
else
55+
raise InvalidObjectTypeError, "Invalid object type: #{of.inspect}. Must be a symbol reference or a Schema class."
56+
end
57+
58+
description ? result.merge(description: description) : result
4159
end
4260

4361
def collect_schemas_from_block(&block)
@@ -55,13 +73,34 @@ def collect_schemas_from_block(&block)
5573
end
5674
end
5775

76+
# Allow Schema classes to be accessed in the context
77+
context.define_singleton_method(:const_missing) do |name|
78+
const_get(name) if const_defined?(name)
79+
end
80+
5881
context.instance_eval(&block)
5982
schemas
6083
end
6184

6285
def primitive_type?(type)
6386
type.is_a?(Symbol) && PRIMITIVE_TYPES.include?(type)
6487
end
88+
89+
def schema_class?(type)
90+
type.is_a?(Class) && type < Schema
91+
end
92+
93+
def schema_class_to_inline_schema(schema_class)
94+
# Directly convert schema class to inline object schema
95+
{
96+
type: "object",
97+
properties: schema_class.properties,
98+
required: schema_class.required_properties,
99+
additionalProperties: schema_class.additional_properties
100+
}.tap do |schema|
101+
schema[:description] = schema_class.description if schema_class.description
102+
end
103+
end
65104
end
66105
end
67106
end

lib/ruby_llm/schema/errors.rb

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,15 @@ def initialize(type)
1414

1515
# Raised when an invalid array type is specified
1616
class InvalidArrayTypeError < Error
17-
def initialize(type)
18-
super("Invalid array type: #{type}")
17+
def initialize(message)
18+
super
19+
end
20+
end
21+
22+
# Raised when an invalid object type is specified
23+
class InvalidObjectTypeError < Error
24+
def initialize(message)
25+
super
1926
end
2027
end
2128

lib/ruby_llm/schema/json_output.rb

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,21 @@ module JsonOutput
66
def to_json_schema
77
validate! # Validate schema before generating JSON
88

9+
schema_hash = {
10+
type: "object",
11+
properties: self.class.properties,
12+
required: self.class.required_properties,
13+
additionalProperties: self.class.additional_properties,
14+
strict: self.class.strict
15+
}
16+
17+
# Only include $defs if there are definitions
18+
schema_hash["$defs"] = self.class.definitions unless self.class.definitions.empty?
19+
920
{
1021
name: @name,
1122
description: @description || self.class.description,
12-
schema: {
13-
:type => "object",
14-
:properties => self.class.properties,
15-
:required => self.class.required_properties,
16-
:additionalProperties => self.class.additional_properties,
17-
:strict => self.class.strict,
18-
"$defs" => self.class.definitions
19-
}
23+
schema: schema_hash
2024
}
2125
end
2226

0 commit comments

Comments
 (0)