forked from danielfriis/ruby_llm-schema
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathschema_builders.rb
More file actions
191 lines (164 loc) · 6.47 KB
/
schema_builders.rb
File metadata and controls
191 lines (164 loc) · 6.47 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
# frozen_string_literal: true
module RubyLLM
class Schema
module DSL
module SchemaBuilders
def string_schema(description: nil, enum: nil, min_length: nil, max_length: nil, pattern: nil, format: nil)
{
type: "string",
enum: enum,
description: description,
minLength: min_length,
maxLength: max_length,
pattern: pattern,
format: format
}.compact
end
def number_schema(description: nil, minimum: nil, maximum: nil, multiple_of: nil)
{
type: "number",
description: description,
minimum: minimum,
maximum: maximum,
multipleOf: multiple_of
}.compact
end
def integer_schema(description: nil, minimum: nil, maximum: nil, multiple_of: nil)
{
type: "integer",
description: description,
minimum: minimum,
maximum: maximum,
multipleOf: multiple_of
}.compact
end
def boolean_schema(description: nil)
{type: "boolean", description: description}.compact
end
def null_schema(description: nil)
{type: "null", description: description}.compact
end
def object_schema(description: nil, of: nil, reference: nil, &block)
if reference
warn "[DEPRECATION] The `reference` option will be deprecated. Please use `of` instead."
of = reference
end
if of
determine_object_reference(of, description)
else
sub_schema = Class.new(Schema)
result = sub_schema.class_eval(&block)
# If the block returned a reference and no properties were added, use the reference
if result.is_a?(Hash) && result["$ref"] && sub_schema.properties.empty?
result.merge(description ? {description: description} : {})
# If the block returned a Schema class or instance, convert it to inline schema
elsif schema_class?(result) && sub_schema.properties.empty?
schema_class_to_inline_schema(result).merge(description ? {description: description} : {})
# Block didn't return reference or schema, so we build an inline object schema
else
{
type: "object",
properties: sub_schema.properties,
required: sub_schema.required_properties,
additionalProperties: sub_schema.additional_properties,
description: description
}.compact
end
end
end
def array_schema(description: nil, of: nil, min_items: nil, max_items: nil, &block)
items = determine_array_items(of, &block)
{
type: "array",
description: description,
items: items,
minItems: min_items,
maxItems: max_items
}.compact
end
def any_of_schema(description: nil, &block)
schemas = collect_schemas_from_block(&block)
{
description: description,
anyOf: schemas
}.compact
end
def one_of_schema(description: nil, &block)
schemas = collect_schemas_from_block(&block)
{
description: description,
oneOf: schemas
}.compact
end
private
def determine_array_items(of, &)
return collect_schemas_from_block(&).first if block_given?
return send("#{of}_schema") if primitive_type?(of)
return reference(of) if of.is_a?(Symbol)
return schema_class_to_inline_schema(of) if schema_class?(of)
raise InvalidArrayTypeError, "Invalid array type: #{of.inspect}. Must be a primitive type (:string, :number, etc.), a symbol reference, a Schema class, or a Schema instance."
end
def determine_object_reference(of, description = nil)
result = case of
when Symbol
reference(of)
when Class
if schema_class?(of)
schema_class_to_inline_schema(of)
else
raise InvalidObjectTypeError, "Invalid object type: #{of.inspect}. Class must inherit from RubyLLM::Schema."
end
else
if schema_class?(of)
schema_class_to_inline_schema(of)
else
raise InvalidObjectTypeError, "Invalid object type: #{of.inspect}. Must be a symbol reference, a Schema class, or a Schema instance."
end
end
description ? result.merge(description: description) : result
end
def collect_schemas_from_block(&block)
schemas = []
schema_builder = self
context = Object.new
# Dynamically create methods for all schema builders
schema_builder.methods.grep(/_schema$/).each do |schema_method|
type_name = schema_method.to_s.sub(/_schema$/, "")
context.define_singleton_method(type_name) do |name = nil, **options, &blk|
schemas << schema_builder.send(schema_method, **options, &blk)
end
end
# Allow Schema classes to be accessed in the context
context.define_singleton_method(:const_missing) do |name|
const_get(name) if const_defined?(name)
end
context.instance_eval(&block)
schemas
end
def schema_class_to_inline_schema(schema_class_or_instance)
# Handle both Schema classes and Schema instances
schema_class = if schema_class_or_instance.is_a?(Class)
schema_class_or_instance
else
schema_class_or_instance.class
end
# Directly convert schema class to inline object schema
{
type: "object",
properties: schema_class.properties,
required: schema_class.required_properties,
additionalProperties: schema_class.additional_properties
}.tap do |schema|
# For instances, prefer instance description over class description
description = if schema_class_or_instance.is_a?(Class)
schema_class.description
else
schema_class_or_instance.instance_variable_get(:@description) || schema_class.description
end
schema[:description] = description if description
end
end
end
end
end
end