ruby_llm-schema 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +122 -22
- data/lib/ruby_llm/schema/dsl/complex_types.rb +8 -8
- data/lib/ruby_llm/schema/dsl/conditionals.rb +169 -0
- data/lib/ruby_llm/schema/dsl/primitive_types.rb +10 -10
- data/lib/ruby_llm/schema/dsl/schema_builders.rb +28 -25
- data/lib/ruby_llm/schema/dsl/utilities.rb +22 -4
- data/lib/ruby_llm/schema/dsl.rb +2 -0
- data/lib/ruby_llm/schema/errors.rb +2 -10
- data/lib/ruby_llm/schema/json_output.rb +4 -2
- data/lib/ruby_llm/schema/validator.rb +1 -3
- data/lib/ruby_llm/schema/version.rb +2 -2
- data/lib/ruby_llm/schema.rb +1 -0
- data/lib/tasks/release.rake +3 -48
- metadata +16 -47
- data/.rspec +0 -3
- data/RELEASE.md +0 -8
- data/Rakefile +0 -12
- /data/{LICENSE.txt → LICENSE} +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 21c3e0ee2e5a1034eb571749cc2f27af5db9e4abaf07e2df677325f9270db6f1
|
|
4
|
+
data.tar.gz: 57db5c67a751f4e31f1ed2d56bba8bac76ddb989fee7b49add969b1c50be844e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cd93b24110859806ee43a5a794353849e51273797696d84469b998c69d9e93e761ef817ede4d89377e34e227f8b6115d2e9133d58496bfcb2399f7720e4087e3
|
|
7
|
+
data.tar.gz: 1153950f2917a95e00744cb832a2c3ce7b1bd61965314f5652dc4337333d3b9f258d52b01257e62739b997bbd6ede840c9a7adfcd339477dc996bca093bf2a0d
|
data/README.md
CHANGED
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
# RubyLLM::Schema
|
|
2
2
|
|
|
3
3
|
[](https://rubygems.org/gems/ruby_llm-schema)
|
|
4
|
-
[](https://rubygems.org/gems/ruby_llm-schema)
|
|
5
|
+
[](https://codecov.io/gh/crmne/ruby_llm-schema)
|
|
6
|
+
[](https://github.com/rubocop/rubocop)
|
|
6
7
|
|
|
7
|
-
A Ruby DSL for creating JSON schemas with a clean, Rails-inspired API.
|
|
8
|
+
A Ruby DSL for creating JSON schemas with a clean, Rails-inspired API.
|
|
9
|
+
|
|
10
|
+
Originally created by [Daniel Friis](https://github.com/danielfriis).
|
|
8
11
|
|
|
9
12
|
## Use Cases
|
|
10
13
|
|
|
11
|
-
|
|
14
|
+
JSON Schema is useful wherever Ruby code needs to describe structured data in a portable format.
|
|
12
15
|
|
|
13
16
|
Some ideal use cases:
|
|
14
17
|
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
18
|
+
- Defining API request and response shapes
|
|
19
|
+
- Describing configuration files or structured payloads
|
|
20
|
+
- Sharing validation contracts across systems
|
|
21
|
+
- Generating structured output schemas for LLM workflows
|
|
22
|
+
- Defining structured parameters for RubyLLM tools
|
|
19
23
|
|
|
20
24
|
### Simple Example
|
|
21
25
|
|
|
@@ -24,22 +28,22 @@ class PersonSchema < RubyLLM::Schema
|
|
|
24
28
|
string :name, description: "Person's full name"
|
|
25
29
|
number :age, description: "Age in years", minimum: 0, maximum: 120
|
|
26
30
|
boolean :active, required: false
|
|
27
|
-
|
|
31
|
+
|
|
28
32
|
object :address do
|
|
29
33
|
string :street
|
|
30
34
|
string :city
|
|
31
35
|
string :country, required: false
|
|
32
36
|
end
|
|
33
|
-
|
|
37
|
+
|
|
34
38
|
array :tags, of: :string, description: "User tags"
|
|
35
|
-
|
|
39
|
+
|
|
36
40
|
array :contacts do
|
|
37
41
|
object do
|
|
38
42
|
string :email, format: "email"
|
|
39
43
|
string :phone, required: false
|
|
40
44
|
end
|
|
41
45
|
end
|
|
42
|
-
|
|
46
|
+
|
|
43
47
|
any_of :status do
|
|
44
48
|
string enum: ["active", "pending", "inactive"]
|
|
45
49
|
null
|
|
@@ -51,7 +55,7 @@ schema = PersonSchema.new
|
|
|
51
55
|
puts schema.to_json
|
|
52
56
|
```
|
|
53
57
|
|
|
54
|
-
###
|
|
58
|
+
### RubyLLM structured output
|
|
55
59
|
|
|
56
60
|
```ruby
|
|
57
61
|
class PersonSchema < RubyLLM::Schema
|
|
@@ -70,6 +74,43 @@ puts response.content # => {"name" => "Alice", "age" => 30}
|
|
|
70
74
|
puts response.content.class # => Hash
|
|
71
75
|
```
|
|
72
76
|
|
|
77
|
+
### RubyLLM tools
|
|
78
|
+
|
|
79
|
+
RubyLLM tools can use schema classes for structured parameters. This is useful when the same argument shape is shared across tools or elsewhere in your app.
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
class SearchParams < RubyLLM::Schema
|
|
83
|
+
string :query, description: "Search query"
|
|
84
|
+
integer :limit, required: false, description: "Maximum results"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
class SearchDocuments < RubyLLM::Tool
|
|
88
|
+
desc "Searches internal documents"
|
|
89
|
+
params SearchParams
|
|
90
|
+
|
|
91
|
+
def execute(query:, limit: 10)
|
|
92
|
+
DocumentSearch.call(query:, limit:)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
For tool-specific parameters, define the schema inline with `params do ... end`.
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
class Weather < RubyLLM::Tool
|
|
101
|
+
desc "Gets current weather"
|
|
102
|
+
|
|
103
|
+
params do
|
|
104
|
+
string :city, description: "City name"
|
|
105
|
+
string :units, enum: %w[celsius fahrenheit], required: false
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def execute(city:, units: "celsius")
|
|
109
|
+
WeatherAPI.current(city:, units:)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
```
|
|
113
|
+
|
|
73
114
|
## Installation
|
|
74
115
|
|
|
75
116
|
Add this line to your application's Gemfile:
|
|
@@ -101,12 +142,12 @@ class PersonSchema < RubyLLM::Schema
|
|
|
101
142
|
string :name, description: "Person's full name"
|
|
102
143
|
number :age
|
|
103
144
|
boolean :active, required: false
|
|
104
|
-
|
|
145
|
+
|
|
105
146
|
object :address do
|
|
106
147
|
string :street
|
|
107
148
|
string :city
|
|
108
149
|
end
|
|
109
|
-
|
|
150
|
+
|
|
110
151
|
array :tags, of: :string
|
|
111
152
|
end
|
|
112
153
|
|
|
@@ -121,12 +162,12 @@ PersonSchema = RubyLLM::Schema.create do
|
|
|
121
162
|
string :name, description: "Person's full name"
|
|
122
163
|
number :age
|
|
123
164
|
boolean :active, required: false
|
|
124
|
-
|
|
165
|
+
|
|
125
166
|
object :address do
|
|
126
167
|
string :street
|
|
127
168
|
string :city
|
|
128
169
|
end
|
|
129
|
-
|
|
170
|
+
|
|
130
171
|
array :tags, of: :string
|
|
131
172
|
end
|
|
132
173
|
|
|
@@ -144,12 +185,12 @@ person_schema = schema "PersonData", description: "A person object" do
|
|
|
144
185
|
string :name, description: "Person's full name"
|
|
145
186
|
number :age
|
|
146
187
|
boolean :active, required: false
|
|
147
|
-
|
|
188
|
+
|
|
148
189
|
object :address do
|
|
149
190
|
string :street
|
|
150
191
|
string :city
|
|
151
192
|
end
|
|
152
|
-
|
|
193
|
+
|
|
153
194
|
array :tags, of: :string
|
|
154
195
|
end
|
|
155
196
|
|
|
@@ -269,7 +310,7 @@ Union types are a way to specify that a property can be one of several types.
|
|
|
269
310
|
```ruby
|
|
270
311
|
any_of :value do
|
|
271
312
|
string
|
|
272
|
-
number
|
|
313
|
+
number
|
|
273
314
|
null
|
|
274
315
|
end
|
|
275
316
|
|
|
@@ -289,7 +330,7 @@ class MySchema < RubyLLM::Schema
|
|
|
289
330
|
string :latitude
|
|
290
331
|
string :longitude
|
|
291
332
|
end
|
|
292
|
-
|
|
333
|
+
|
|
293
334
|
# Using a reference in an array
|
|
294
335
|
array :coordinates, of: :location
|
|
295
336
|
|
|
@@ -324,7 +365,7 @@ class CompanySchema < RubyLLM::Schema
|
|
|
324
365
|
# Using 'of' parameter
|
|
325
366
|
object :ceo, of: PersonSchema
|
|
326
367
|
array :employees, of: PersonSchema
|
|
327
|
-
|
|
368
|
+
|
|
328
369
|
# Using Schema.new in block
|
|
329
370
|
object :founder do
|
|
330
371
|
PersonSchema.new
|
|
@@ -403,6 +444,65 @@ schema.to_json_schema
|
|
|
403
444
|
# }
|
|
404
445
|
```
|
|
405
446
|
|
|
447
|
+
### Dependencies
|
|
448
|
+
|
|
449
|
+
Use `requires:` inline or `dependent` block to express that the presence of one property requires others. Maps to [`dependentRequired`](https://json-schema.org/understanding-json-schema/reference/conditionals#dependentRequired) (Draft 2019-09) and [`dependentSchemas`](https://json-schema.org/understanding-json-schema/reference/conditionals#dependentSchemas) (Draft 2019-09). Check your provider's documentation for compatibility.
|
|
450
|
+
|
|
451
|
+
```ruby
|
|
452
|
+
class PaymentSchema < RubyLLM::Schema
|
|
453
|
+
string :name
|
|
454
|
+
number :credit_card, required: false, requires: %i[billing_address cvv]
|
|
455
|
+
string :billing_address, required: false
|
|
456
|
+
string :cvv, required: false
|
|
457
|
+
end
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
Use a `dependent` block when you also need validations — this upgrades the output to `dependentSchemas`:
|
|
461
|
+
|
|
462
|
+
```ruby
|
|
463
|
+
dependent :credit_card do
|
|
464
|
+
requires :billing_address
|
|
465
|
+
validates :billing_address, type: :string, min_length: 1
|
|
466
|
+
end
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
### Conditionals
|
|
470
|
+
|
|
471
|
+
Use `given` to add [JSON Schema `if`/`then`/`else`](https://json-schema.org/understanding-json-schema/reference/conditionals#ifthenelse) (Draft 7) rules. Condition values are automatically coerced: strings → `const`, arrays → `enum`, regexps → `pattern`, hashes → raw schema.
|
|
472
|
+
|
|
473
|
+
```ruby
|
|
474
|
+
class OrderSchema < RubyLLM::Schema
|
|
475
|
+
string :status, enum: ["pending", "shipped", "cancelled"]
|
|
476
|
+
string :tracking_number, required: false
|
|
477
|
+
string :cancellation_reason, required: false
|
|
478
|
+
|
|
479
|
+
given status: "shipped" do
|
|
480
|
+
requires :tracking_number
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
given status: "cancelled" do
|
|
484
|
+
requires :cancellation_reason
|
|
485
|
+
validates :cancellation_reason, type: :string, min_length: 1
|
|
486
|
+
end
|
|
487
|
+
end
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
`validates` supports: `type:`, `not_value:`, `min_length:`, `max_length:`, `pattern:` (string or regexp), `enum:`, `const:`, `minimum:`, `maximum:`.
|
|
491
|
+
|
|
492
|
+
Use `otherwise` for an `else` branch:
|
|
493
|
+
|
|
494
|
+
```ruby
|
|
495
|
+
given domestic: true do
|
|
496
|
+
requires :state
|
|
497
|
+
|
|
498
|
+
otherwise do
|
|
499
|
+
requires :country
|
|
500
|
+
end
|
|
501
|
+
end
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
Conditions propagate through nested schemas via `of:`.
|
|
505
|
+
|
|
406
506
|
## JSON Output
|
|
407
507
|
|
|
408
508
|
```ruby
|
|
@@ -4,20 +4,20 @@ module RubyLLM
|
|
|
4
4
|
class Schema
|
|
5
5
|
module DSL
|
|
6
6
|
module ComplexTypes
|
|
7
|
-
def object(name, description: nil, required: true, **options, &block)
|
|
8
|
-
add_property(name, object_schema(description: description, **options, &block), required: required)
|
|
7
|
+
def object(name, description: nil, required: true, requires: nil, **options, &block)
|
|
8
|
+
add_property(name, object_schema(description: description, **options, &block), required: required, requires: requires)
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
def array(name, description: nil, required: true, **options, &block)
|
|
12
|
-
add_property(name, array_schema(description: description, **options, &block), required: required)
|
|
11
|
+
def array(name, description: nil, required: true, requires: nil, **options, &block)
|
|
12
|
+
add_property(name, array_schema(description: description, **options, &block), required: required, requires: requires)
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
def any_of(name, description: nil, required: true, **options, &block)
|
|
16
|
-
add_property(name, any_of_schema(description: description, **options, &block), required: required)
|
|
15
|
+
def any_of(name, description: nil, required: true, requires: nil, **options, &block)
|
|
16
|
+
add_property(name, any_of_schema(description: description, **options, &block), required: required, requires: requires)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
def one_of(name, description: nil, required: true, **options, &block)
|
|
20
|
-
add_property(name, one_of_schema(description: description, **options, &block), required: required)
|
|
19
|
+
def one_of(name, description: nil, required: true, requires: nil, **options, &block)
|
|
20
|
+
add_property(name, one_of_schema(description: description, **options, &block), required: required, requires: requires)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
def optional(name, description: nil, &block)
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
class Schema
|
|
5
|
+
module DSL
|
|
6
|
+
module Conditionals
|
|
7
|
+
def conditions
|
|
8
|
+
@conditions ||= []
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def dependencies
|
|
12
|
+
@dependencies ||= {}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def dependent(property, &block)
|
|
16
|
+
builder = ConditionalBuilder.new
|
|
17
|
+
builder.instance_eval(&block)
|
|
18
|
+
|
|
19
|
+
dependencies[property.to_s] = builder
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def given(**properties, &block)
|
|
23
|
+
raise ArgumentError, "given requires at least one property condition" if properties.empty?
|
|
24
|
+
|
|
25
|
+
if_schema = {
|
|
26
|
+
properties: properties.transform_keys(&:to_s).transform_values { |v| coerce_condition(v) },
|
|
27
|
+
required: properties.keys.map(&:to_s)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
then_builder = ConditionalBuilder.new
|
|
31
|
+
else_builder = ConditionalBuilder.new
|
|
32
|
+
|
|
33
|
+
context = ConditionalContext.new(then_builder, else_builder)
|
|
34
|
+
context.instance_eval(&block)
|
|
35
|
+
|
|
36
|
+
condition = {if: if_schema, then: then_builder.to_schema}
|
|
37
|
+
condition[:else] = else_builder.to_schema unless else_builder.empty?
|
|
38
|
+
|
|
39
|
+
conditions << condition
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def merge_conditions(schema, schema_class)
|
|
45
|
+
if schema_class.respond_to?(:conditions) && schema_class.conditions.any?
|
|
46
|
+
if schema_class.conditions.length == 1
|
|
47
|
+
schema.merge!(schema_class.conditions.first)
|
|
48
|
+
else
|
|
49
|
+
schema[:allOf] = schema_class.conditions
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
if schema_class.respond_to?(:dependencies) && schema_class.dependencies.any?
|
|
54
|
+
dependent_required = {}
|
|
55
|
+
dependent_schemas = {}
|
|
56
|
+
|
|
57
|
+
schema_class.dependencies.each do |property, builder|
|
|
58
|
+
if builder.validations_empty?
|
|
59
|
+
dependent_required[property] = builder.required_fields
|
|
60
|
+
else
|
|
61
|
+
dependent_schemas[property] = builder.to_schema
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
schema[:dependentRequired] = dependent_required if dependent_required.any?
|
|
66
|
+
schema[:dependentSchemas] = dependent_schemas if dependent_schemas.any?
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
schema
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def coerce_condition(value)
|
|
73
|
+
case value
|
|
74
|
+
when Array then {enum: value}
|
|
75
|
+
when Regexp then {pattern: value.source}
|
|
76
|
+
when Hash then value
|
|
77
|
+
else {const: value}
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
class ConditionalContext
|
|
83
|
+
def initialize(then_builder, else_builder)
|
|
84
|
+
@then_builder = then_builder
|
|
85
|
+
@else_builder = else_builder
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def requires(*fields)
|
|
89
|
+
@then_builder.requires(*fields)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def validates(field, **options)
|
|
93
|
+
@then_builder.validates(field, **options)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def otherwise(&block)
|
|
97
|
+
@else_builder.instance_eval(&block)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
class ConditionalBuilder
|
|
102
|
+
def requires(*fields)
|
|
103
|
+
required.concat(fields.map(&:to_s))
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
VALIDATES_KEY_MAP = {
|
|
107
|
+
type: :type,
|
|
108
|
+
const: :const,
|
|
109
|
+
enum: :enum,
|
|
110
|
+
not_value: :not,
|
|
111
|
+
min_length: :minLength,
|
|
112
|
+
max_length: :maxLength,
|
|
113
|
+
pattern: :pattern,
|
|
114
|
+
minimum: :minimum,
|
|
115
|
+
maximum: :maximum
|
|
116
|
+
}.freeze
|
|
117
|
+
|
|
118
|
+
def validates(field, **options)
|
|
119
|
+
constraints = {}
|
|
120
|
+
|
|
121
|
+
options.each do |key, value|
|
|
122
|
+
schema_key = VALIDATES_KEY_MAP[key]
|
|
123
|
+
raise ArgumentError, "unknown validates option: #{key.inspect}" unless schema_key
|
|
124
|
+
|
|
125
|
+
case key
|
|
126
|
+
when :type then constraints[:type] = value.to_s
|
|
127
|
+
when :not_value then constraints[:not] = {const: value}
|
|
128
|
+
when :pattern then constraints[:pattern] = value.is_a?(Regexp) ? value.source : value
|
|
129
|
+
else constraints[schema_key] = value
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
validations[field.to_s] = constraints
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def to_schema
|
|
137
|
+
schema = {}
|
|
138
|
+
|
|
139
|
+
schema[:required] = required if required.any?
|
|
140
|
+
schema[:properties] = validations if validations.any?
|
|
141
|
+
|
|
142
|
+
schema
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def empty?
|
|
146
|
+
required.empty? && validations.empty?
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def required_fields
|
|
150
|
+
required.dup
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def validations_empty?
|
|
154
|
+
validations.empty?
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
private
|
|
158
|
+
|
|
159
|
+
def required
|
|
160
|
+
@required ||= []
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def validations
|
|
164
|
+
@validations ||= {}
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
@@ -4,24 +4,24 @@ module RubyLLM
|
|
|
4
4
|
class Schema
|
|
5
5
|
module DSL
|
|
6
6
|
module PrimitiveTypes
|
|
7
|
-
def string(name, description: nil, required: true, **options)
|
|
8
|
-
add_property(name, string_schema(description: description, **options), required: required)
|
|
7
|
+
def string(name, description: nil, required: true, requires: nil, **options)
|
|
8
|
+
add_property(name, string_schema(description: description, **options), required: required, requires: requires)
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
def number(name, description: nil, required: true, **options)
|
|
12
|
-
add_property(name, number_schema(description: description, **options), required: required)
|
|
11
|
+
def number(name, description: nil, required: true, requires: nil, **options)
|
|
12
|
+
add_property(name, number_schema(description: description, **options), required: required, requires: requires)
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
def integer(name, description: nil, required: true, **options)
|
|
16
|
-
add_property(name, integer_schema(description: description, **options), required: required)
|
|
15
|
+
def integer(name, description: nil, required: true, requires: nil, **options)
|
|
16
|
+
add_property(name, integer_schema(description: description, **options), required: required, requires: requires)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
def boolean(name, description: nil, required: true, **options)
|
|
20
|
-
add_property(name, boolean_schema(description: description, **options), required: required)
|
|
19
|
+
def boolean(name, description: nil, required: true, requires: nil, **options)
|
|
20
|
+
add_property(name, boolean_schema(description: description, **options), required: required, requires: requires)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
def null(name, description: nil, required: true, **options)
|
|
24
|
-
add_property(name, null_schema(description: description, **options), required: required)
|
|
23
|
+
def null(name, description: nil, required: true, requires: nil, **options)
|
|
24
|
+
add_property(name, null_schema(description: description, **options), required: required, requires: requires)
|
|
25
25
|
end
|
|
26
26
|
end
|
|
27
27
|
end
|
|
@@ -64,13 +64,15 @@ module RubyLLM
|
|
|
64
64
|
schema_class_to_inline_schema(result).merge(description ? {description: description} : {})
|
|
65
65
|
# Block didn't return reference or schema, so we build an inline object schema
|
|
66
66
|
else
|
|
67
|
-
{
|
|
67
|
+
schema = {
|
|
68
68
|
type: "object",
|
|
69
69
|
properties: sub_schema.properties,
|
|
70
70
|
required: sub_schema.required_properties,
|
|
71
71
|
additionalProperties: sub_schema.additional_properties,
|
|
72
72
|
description: description
|
|
73
73
|
}.compact
|
|
74
|
+
|
|
75
|
+
merge_conditions(schema, sub_schema)
|
|
74
76
|
end
|
|
75
77
|
end
|
|
76
78
|
end
|
|
@@ -118,21 +120,19 @@ module RubyLLM
|
|
|
118
120
|
|
|
119
121
|
def determine_object_reference(of, description = nil)
|
|
120
122
|
result = case of
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
end
|
|
135
|
-
end
|
|
123
|
+
when Symbol
|
|
124
|
+
reference(of)
|
|
125
|
+
when Class
|
|
126
|
+
raise InvalidObjectTypeError, "Invalid object type: #{of.inspect}. Class must inherit from RubyLLM::Schema." unless schema_class?(of)
|
|
127
|
+
|
|
128
|
+
schema_class_to_inline_schema(of)
|
|
129
|
+
|
|
130
|
+
else
|
|
131
|
+
raise InvalidObjectTypeError, "Invalid object type: #{of.inspect}. Must be a symbol reference, a Schema class, or a Schema instance." unless schema_class?(of)
|
|
132
|
+
|
|
133
|
+
schema_class_to_inline_schema(of)
|
|
134
|
+
|
|
135
|
+
end
|
|
136
136
|
|
|
137
137
|
description ? result.merge(description: description) : result
|
|
138
138
|
end
|
|
@@ -147,7 +147,7 @@ module RubyLLM
|
|
|
147
147
|
schema_builder.methods.grep(/_schema$/).each do |schema_method|
|
|
148
148
|
type_name = schema_method.to_s.sub(/_schema$/, "")
|
|
149
149
|
|
|
150
|
-
context.define_singleton_method(type_name) do |
|
|
150
|
+
context.define_singleton_method(type_name) do |_name = nil, **options, &blk|
|
|
151
151
|
schemas << schema_builder.send(schema_method, **options, &blk)
|
|
152
152
|
end
|
|
153
153
|
end
|
|
@@ -164,10 +164,10 @@ module RubyLLM
|
|
|
164
164
|
def schema_class_to_inline_schema(schema_class_or_instance)
|
|
165
165
|
# Handle both Schema classes and Schema instances
|
|
166
166
|
schema_class = if schema_class_or_instance.is_a?(Class)
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
167
|
+
schema_class_or_instance
|
|
168
|
+
else
|
|
169
|
+
schema_class_or_instance.class
|
|
170
|
+
end
|
|
171
171
|
|
|
172
172
|
# Directly convert schema class to inline object schema
|
|
173
173
|
{
|
|
@@ -178,11 +178,14 @@ module RubyLLM
|
|
|
178
178
|
}.tap do |schema|
|
|
179
179
|
# For instances, prefer instance description over class description
|
|
180
180
|
description = if schema_class_or_instance.is_a?(Class)
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
181
|
+
schema_class.description
|
|
182
|
+
else
|
|
183
|
+
schema_class_or_instance.instance_variable_get(:@description) || schema_class.description
|
|
184
|
+
end
|
|
185
|
+
|
|
185
186
|
schema[:description] = description if description
|
|
187
|
+
|
|
188
|
+
merge_conditions(schema, schema_class)
|
|
186
189
|
end
|
|
187
190
|
end
|
|
188
191
|
end
|
|
@@ -9,12 +9,16 @@ module RubyLLM
|
|
|
9
9
|
sub_schema = Class.new(Schema)
|
|
10
10
|
sub_schema.class_eval(&)
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
schema = {
|
|
13
13
|
type: "object",
|
|
14
14
|
properties: sub_schema.properties,
|
|
15
15
|
required: sub_schema.required_properties,
|
|
16
16
|
additionalProperties: sub_schema.additional_properties
|
|
17
17
|
}
|
|
18
|
+
|
|
19
|
+
merge_conditions(schema, sub_schema)
|
|
20
|
+
|
|
21
|
+
definitions[name] = schema
|
|
18
22
|
end
|
|
19
23
|
|
|
20
24
|
def reference(schema_name)
|
|
@@ -27,9 +31,23 @@ module RubyLLM
|
|
|
27
31
|
|
|
28
32
|
private
|
|
29
33
|
|
|
30
|
-
def add_property(name, definition, required:)
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
def add_property(name, definition, required:, requires: nil)
|
|
35
|
+
property_name = name.to_sym
|
|
36
|
+
|
|
37
|
+
properties[property_name] = definition
|
|
38
|
+
if required
|
|
39
|
+
required_properties << property_name unless required_properties.include?(property_name)
|
|
40
|
+
else
|
|
41
|
+
required_properties.delete(property_name)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
if requires
|
|
45
|
+
builder = ConditionalBuilder.new
|
|
46
|
+
builder.requires(*Array(requires))
|
|
47
|
+
dependencies[name.to_s] = builder
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
nil
|
|
33
51
|
end
|
|
34
52
|
|
|
35
53
|
def primitive_type?(type)
|
data/lib/ruby_llm/schema/dsl.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "dsl/schema_builders"
|
|
4
4
|
require_relative "dsl/primitive_types"
|
|
5
5
|
require_relative "dsl/complex_types"
|
|
6
|
+
require_relative "dsl/conditionals"
|
|
6
7
|
require_relative "dsl/utilities"
|
|
7
8
|
|
|
8
9
|
module RubyLLM
|
|
@@ -11,6 +12,7 @@ module RubyLLM
|
|
|
11
12
|
include SchemaBuilders
|
|
12
13
|
include PrimitiveTypes
|
|
13
14
|
include ComplexTypes
|
|
15
|
+
include Conditionals
|
|
14
16
|
include Utilities
|
|
15
17
|
end
|
|
16
18
|
end
|
|
@@ -13,18 +13,10 @@ module RubyLLM
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
# Raised when an invalid array type is specified
|
|
16
|
-
class InvalidArrayTypeError < Error
|
|
17
|
-
def initialize(message)
|
|
18
|
-
super
|
|
19
|
-
end
|
|
20
|
-
end
|
|
16
|
+
class InvalidArrayTypeError < Error; end
|
|
21
17
|
|
|
22
18
|
# Raised when an invalid object type is specified
|
|
23
|
-
class InvalidObjectTypeError < Error
|
|
24
|
-
def initialize(message)
|
|
25
|
-
super
|
|
26
|
-
end
|
|
27
|
-
end
|
|
19
|
+
class InvalidObjectTypeError < Error; end
|
|
28
20
|
|
|
29
21
|
# Raised when schema definition is invalid
|
|
30
22
|
class InvalidSchemaError < Error; end
|
|
@@ -4,7 +4,7 @@ module RubyLLM
|
|
|
4
4
|
class Schema
|
|
5
5
|
module JsonOutput
|
|
6
6
|
def to_json_schema
|
|
7
|
-
validate!
|
|
7
|
+
validate! # Validate schema before generating JSON
|
|
8
8
|
|
|
9
9
|
schema_hash = {
|
|
10
10
|
type: "object",
|
|
@@ -18,6 +18,8 @@ module RubyLLM
|
|
|
18
18
|
# Only include $defs if there are definitions
|
|
19
19
|
schema_hash["$defs"] = self.class.definitions unless self.class.definitions.empty?
|
|
20
20
|
|
|
21
|
+
self.class.send(:merge_conditions, schema_hash, self.class)
|
|
22
|
+
|
|
21
23
|
{
|
|
22
24
|
name: @name,
|
|
23
25
|
description: @description || self.class.description,
|
|
@@ -26,7 +28,7 @@ module RubyLLM
|
|
|
26
28
|
end
|
|
27
29
|
|
|
28
30
|
def to_json(*_args)
|
|
29
|
-
validate!
|
|
31
|
+
validate! # Validate schema before generating JSON string
|
|
30
32
|
JSON.pretty_generate(to_json_schema)
|
|
31
33
|
end
|
|
32
34
|
end
|
|
@@ -45,9 +45,7 @@ module RubyLLM
|
|
|
45
45
|
return if marks[node] == BLACK
|
|
46
46
|
|
|
47
47
|
# If node has a temporary mark, we found a cycle
|
|
48
|
-
if marks[node] == GRAY
|
|
49
|
-
raise ValidationError, "Circular reference detected involving '#{node}'"
|
|
50
|
-
end
|
|
48
|
+
raise ValidationError, "Circular reference detected involving '#{node}'" if marks[node] == GRAY
|
|
51
49
|
|
|
52
50
|
# Mark node with temporary mark
|
|
53
51
|
marks[node] = GRAY
|
data/lib/ruby_llm/schema.rb
CHANGED
data/lib/tasks/release.rake
CHANGED
|
@@ -1,53 +1,8 @@
|
|
|
1
|
-
#
|
|
2
|
-
# Update VERSION in lib/ruby_llm/schema/version.rb
|
|
3
|
-
|
|
4
|
-
# 2. Update Gemfile.lock
|
|
5
|
-
# bundle install
|
|
6
|
-
|
|
7
|
-
# 3. Commit everything
|
|
8
|
-
# git add .
|
|
9
|
-
# git commit -m "Bump version to X.Y.Z"
|
|
10
|
-
|
|
11
|
-
# 4. Push to main
|
|
12
|
-
# git push origin main
|
|
13
|
-
|
|
14
|
-
# 5. Trigger the release
|
|
15
|
-
# bundle exec rake release:prepare
|
|
16
|
-
|
|
17
|
-
# 6. Delete the release branch locally and remotely
|
|
18
|
-
# git branch -d release/X.Y.Z
|
|
19
|
-
# git push origin --delete release/X.Y.Z
|
|
1
|
+
# frozen_string_literal: true
|
|
20
2
|
|
|
21
3
|
namespace :release do
|
|
22
|
-
desc
|
|
4
|
+
desc 'Prepare for release'
|
|
23
5
|
task :prepare do
|
|
24
|
-
|
|
25
|
-
abort "Not on main branch. Releases must be run from main branch" unless `git rev-parse --abbrev-ref HEAD`.strip == "main"
|
|
26
|
-
|
|
27
|
-
require_relative "../ruby_llm/schema/version"
|
|
28
|
-
version = RubyLlm::Schema::VERSION or abort "Could not determine version"
|
|
29
|
-
|
|
30
|
-
branch = "release/#{version}"
|
|
31
|
-
|
|
32
|
-
if system("git rev-parse --quiet --verify refs/tags/v#{version} > /dev/null 2>&1")
|
|
33
|
-
abort "Tag v#{version} already exists. Bump the version first."
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
if system("git show-ref --verify --quiet refs/heads/#{branch}")
|
|
37
|
-
abort "Local branch #{branch} already exists. Remove it or choose a new version."
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
sh "git fetch origin"
|
|
41
|
-
|
|
42
|
-
if system("git ls-remote --exit-code --heads origin #{branch} > /dev/null 2>&1")
|
|
43
|
-
abort "Release branch #{branch} already exists on origin. Remove it or choose a new version."
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
sh "git checkout -b #{branch}"
|
|
47
|
-
sh "git push -u origin #{branch}"
|
|
48
|
-
|
|
49
|
-
puts "Release branch #{branch} pushed. GitHub Actions will run tests and publish if they pass."
|
|
50
|
-
ensure
|
|
51
|
-
system "git checkout main"
|
|
6
|
+
sh 'overcommit --run'
|
|
52
7
|
end
|
|
53
8
|
end
|
metadata
CHANGED
|
@@ -1,58 +1,28 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruby_llm-schema
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Daniel Friis
|
|
8
|
-
|
|
9
|
-
bindir: exe
|
|
8
|
+
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
12
|
-
dependencies:
|
|
13
|
-
-
|
|
14
|
-
|
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
|
16
|
-
requirements:
|
|
17
|
-
- - "~>"
|
|
18
|
-
- !ruby/object:Gem::Version
|
|
19
|
-
version: '3.0'
|
|
20
|
-
type: :development
|
|
21
|
-
prerelease: false
|
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
-
requirements:
|
|
24
|
-
- - "~>"
|
|
25
|
-
- !ruby/object:Gem::Version
|
|
26
|
-
version: '3.0'
|
|
27
|
-
- !ruby/object:Gem::Dependency
|
|
28
|
-
name: standard
|
|
29
|
-
requirement: !ruby/object:Gem::Requirement
|
|
30
|
-
requirements:
|
|
31
|
-
- - ">="
|
|
32
|
-
- !ruby/object:Gem::Version
|
|
33
|
-
version: '0'
|
|
34
|
-
type: :development
|
|
35
|
-
prerelease: false
|
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
-
requirements:
|
|
38
|
-
- - ">="
|
|
39
|
-
- !ruby/object:Gem::Version
|
|
40
|
-
version: '0'
|
|
41
|
-
description:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: A compact Ruby DSL for building standards-oriented JSON Schema documents
|
|
13
|
+
from Ruby.
|
|
42
14
|
email:
|
|
43
15
|
- d@friis.me
|
|
44
16
|
executables: []
|
|
45
17
|
extensions: []
|
|
46
18
|
extra_rdoc_files: []
|
|
47
19
|
files:
|
|
48
|
-
-
|
|
49
|
-
- LICENSE.txt
|
|
20
|
+
- LICENSE
|
|
50
21
|
- README.md
|
|
51
|
-
- RELEASE.md
|
|
52
|
-
- Rakefile
|
|
53
22
|
- lib/ruby_llm/schema.rb
|
|
54
23
|
- lib/ruby_llm/schema/dsl.rb
|
|
55
24
|
- lib/ruby_llm/schema/dsl/complex_types.rb
|
|
25
|
+
- lib/ruby_llm/schema/dsl/conditionals.rb
|
|
56
26
|
- lib/ruby_llm/schema/dsl/primitive_types.rb
|
|
57
27
|
- lib/ruby_llm/schema/dsl/schema_builders.rb
|
|
58
28
|
- lib/ruby_llm/schema/dsl/utilities.rb
|
|
@@ -62,15 +32,15 @@ files:
|
|
|
62
32
|
- lib/ruby_llm/schema/validator.rb
|
|
63
33
|
- lib/ruby_llm/schema/version.rb
|
|
64
34
|
- lib/tasks/release.rake
|
|
65
|
-
homepage: https://github.com/
|
|
35
|
+
homepage: https://github.com/crmne/ruby_llm-schema#readme
|
|
66
36
|
licenses:
|
|
67
37
|
- MIT
|
|
68
38
|
metadata:
|
|
69
|
-
homepage_uri: https://github.com/
|
|
70
|
-
source_code_uri: https://github.com/
|
|
71
|
-
changelog_uri: https://github.com/
|
|
39
|
+
homepage_uri: https://github.com/crmne/ruby_llm-schema#readme
|
|
40
|
+
source_code_uri: https://github.com/crmne/ruby_llm-schema
|
|
41
|
+
changelog_uri: https://github.com/crmne/ruby_llm-schema/releases
|
|
42
|
+
bug_tracker_uri: https://github.com/crmne/ruby_llm-schema/issues
|
|
72
43
|
rubygems_mfa_required: 'true'
|
|
73
|
-
post_install_message:
|
|
74
44
|
rdoc_options: []
|
|
75
45
|
require_paths:
|
|
76
46
|
- lib
|
|
@@ -78,15 +48,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
78
48
|
requirements:
|
|
79
49
|
- - ">="
|
|
80
50
|
- !ruby/object:Gem::Version
|
|
81
|
-
version: 3.1.
|
|
51
|
+
version: 3.1.3
|
|
82
52
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
83
53
|
requirements:
|
|
84
54
|
- - ">="
|
|
85
55
|
- !ruby/object:Gem::Version
|
|
86
56
|
version: '0'
|
|
87
57
|
requirements: []
|
|
88
|
-
rubygems_version:
|
|
89
|
-
signing_key:
|
|
58
|
+
rubygems_version: 4.0.10
|
|
90
59
|
specification_version: 4
|
|
91
|
-
summary: A simple
|
|
60
|
+
summary: A simple Ruby DSL for creating JSON schemas.
|
|
92
61
|
test_files: []
|
data/.rspec
DELETED
data/RELEASE.md
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
# Release process
|
|
2
|
-
|
|
3
|
-
1. Bump the version in `lib/ruby_llm/schema/version.rb`
|
|
4
|
-
2. Run `bundle install` to update the gemspec
|
|
5
|
-
3. Commit the changes with a message like "Bump version to X.Y.Z"
|
|
6
|
-
4. Run `bundle exec rake release:prepare` to create a release branch and push it to GitHub
|
|
7
|
-
5. Github Actions will run the tests and publish the gem if they pass
|
|
8
|
-
6. Delete the release branch: `git branch -d release/<version> && git push origin --delete release/<version>`
|
data/Rakefile
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "bundler/gem_tasks"
|
|
4
|
-
require "rspec/core/rake_task"
|
|
5
|
-
require "standard/rake"
|
|
6
|
-
|
|
7
|
-
# Load custom tasks
|
|
8
|
-
Dir.glob("lib/tasks/**/*.rake").each { |r| load r }
|
|
9
|
-
|
|
10
|
-
RSpec::Core::RakeTask.new(:spec)
|
|
11
|
-
|
|
12
|
-
task default: %i[standard spec]
|
/data/{LICENSE.txt → LICENSE}
RENAMED
|
File without changes
|