ruby_llm-instructor 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: efe5592fe81d6feb6e5a328c56ae00bca1bb3f8007826da4440d1bc042f12c7e
4
+ data.tar.gz: 0d5b70673f1da2ef41686603c1c50bb237cb6d7dd338d921f5ed894c38dcb2a0
5
+ SHA512:
6
+ metadata.gz: 8b0fb08a2c1c5f93639cfa79256029c3433b6ec4459d28082a69373464d08b6d4cc743b9617e7a4fb5eb1856f68f5cdad4efe3bd7d174753b42ca7d7e5aab7b7
7
+ data.tar.gz: 0a536fbab7e9f6420e8d9899ec30d78232c4f55af2dd6ed5ce62b154dc7b22607196291c88e39c60ddeed6eefecb46e568e5375e5dcdf0682a39ff0ca3e976b6
data/README.md ADDED
@@ -0,0 +1,308 @@
1
+ # ruby_llm-instructor
2
+
3
+ [![CI](https://github.com/washu/ruby_llm-instructor/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/washu/ruby_llm-instructor/actions/workflows/ci.yml)
4
+
5
+ Structured, validated outputs from LLMs for Ruby. Define a Ruby class, hand it to
6
+ `RubyLLM::Instructor::Client`, and get back a fully-hydrated, validated instance —
7
+ with automatic retries on validation failure.
8
+
9
+ Part of the [RubyLLM ecosystem](https://rubyllm.com/ecosystem/). Built on top of
10
+ [`ruby_llm`](https://github.com/crmne/ruby_llm), so the same code works against
11
+ OpenAI, Anthropic, Gemini, and every other provider `ruby_llm` supports.
12
+
13
+ ## Installation
14
+
15
+ ```ruby
16
+ gem "ruby_llm-instructor"
17
+ ```
18
+
19
+ ```bash
20
+ bundle install
21
+ ```
22
+
23
+ ## Quick start
24
+
25
+ Configure `RubyLLM` with your API key(s), then pass any Ruby class as `response_model`:
26
+
27
+ ```ruby
28
+ require "ruby_llm"
29
+ require "ruby_llm/instructor"
30
+
31
+ RubyLLM.configure do |config|
32
+ config.openai_api_key = ENV["OPENAI_API_KEY"]
33
+ end
34
+
35
+ class UserProfile
36
+ attr_accessor :name, :email
37
+ end
38
+
39
+ instructor = RubyLLM::Instructor::Client.new
40
+
41
+ user = instructor.chat(
42
+ model: "gpt-4o",
43
+ response_model: UserProfile,
44
+ prompt: "Extract information: My name is Sal, reached at sal@example.com"
45
+ )
46
+
47
+ user.name # => "Sal"
48
+ user.email # => "sal@example.com"
49
+ user.class # => UserProfile
50
+ ```
51
+
52
+ ## Supported response model types
53
+
54
+ `ruby_llm-instructor` uses duck-typing — no base class or mixin required. The JSON
55
+ schema sent to the LLM is inferred automatically from your class's shape.
56
+
57
+ ### Plain Ruby class (PORO)
58
+
59
+ Schema inferred from `attr_accessor` setters. No validation — any response is accepted.
60
+
61
+ ```ruby
62
+ class UserProfile
63
+ attr_accessor :name, :email
64
+ end
65
+ ```
66
+
67
+ ### ActiveModel
68
+
69
+ Add validations; `ruby_llm-instructor` calls `valid?` automatically and feeds
70
+ error messages back to the LLM on retry.
71
+
72
+ ```ruby
73
+ require "active_model"
74
+
75
+ class LeadCapture
76
+ include ActiveModel::Model
77
+ include ActiveModel::Attributes
78
+
79
+ attribute :company, :string
80
+ attribute :phone, :string
81
+ attribute :revenue, :integer
82
+
83
+ validates :company, presence: true
84
+ validates :phone, format: { with: /\A\+?\d{10,15}\z/, message: "must be a valid phone number" }
85
+ end
86
+
87
+ instructor = RubyLLM::Instructor::Client.new
88
+
89
+ lead = instructor.chat(
90
+ model: "claude-3-5-sonnet",
91
+ response_model: LeadCapture,
92
+ prompt: "Inbound transcript: We are Stripe, call us at +15550192831. ARR is $4B."
93
+ )
94
+
95
+ lead.company # => "Stripe"
96
+ lead.phone # => "+15550192831"
97
+ lead.revenue # => 4000000000
98
+ ```
99
+
100
+ Using `ActiveModel::Attributes` also improves the JSON schema sent to the LLM —
101
+ field types (`integer`, `number`, `boolean`) are inferred from your attribute
102
+ declarations rather than defaulting to `string`.
103
+
104
+ ### dry-validation (native contract)
105
+
106
+ Pass a `Dry::Validation::Contract` subclass directly. The JSON schema is built
107
+ automatically from the contract's params block, and validation runs through the
108
+ contract itself — no bridge required.
109
+
110
+ ```ruby
111
+ require "dry-validation"
112
+
113
+ class PersonContract < Dry::Validation::Contract
114
+ params do
115
+ required(:name).filled(:string)
116
+ required(:email).filled(:string)
117
+ end
118
+
119
+ rule(:email) { key.failure("must include @") unless value.include?("@") }
120
+ end
121
+
122
+ instructor = RubyLLM::Instructor::Client.new
123
+
124
+ person = instructor.chat(
125
+ model: "gpt-4o",
126
+ response_model: PersonContract,
127
+ prompt: "Sal Scotto, sal@example.com"
128
+ )
129
+
130
+ person.name # => "Sal Scotto"
131
+ person.email # => "sal@example.com"
132
+ person.frozen? # => true (returned as a Data object)
133
+ ```
134
+
135
+ The returned instance is a `Data.define` value object with one member per contract
136
+ field — immutable and frozen.
137
+
138
+ #### Duck-typed bridge (alternative)
139
+
140
+ If you prefer to keep your domain class, bridge dry-validation's result to
141
+ `valid?` / `errors.full_messages` and it works the same way:
142
+
143
+ ```ruby
144
+ class PersonDry
145
+ attr_accessor :name, :email
146
+
147
+ CONTRACT = Class.new(Dry::Validation::Contract) do
148
+ params do
149
+ required(:name).filled(:string)
150
+ required(:email).filled(:string)
151
+ end
152
+ rule(:email) { key.failure("must include @") unless value.include?("@") }
153
+ end
154
+
155
+ def valid?
156
+ @result = CONTRACT.new.call(name: @name, email: @email)
157
+ @result.success?
158
+ end
159
+
160
+ def errors
161
+ DryErrors.new(@result)
162
+ end
163
+
164
+ DryErrors = Struct.new(:result) do
165
+ def full_messages
166
+ return [] unless result
167
+ result.errors.to_h.flat_map { |field, msgs| msgs.map { |m| "#{field} #{m}" } }
168
+ end
169
+ end
170
+ end
171
+ ```
172
+
173
+ ### Ruby `Data.define` (immutable value object)
174
+
175
+ Members are inferred automatically. The returned instance is frozen.
176
+
177
+ ```ruby
178
+ Person = Data.define(:name, :email)
179
+
180
+ person = instructor.chat(
181
+ model: "gpt-4o",
182
+ response_model: Person,
183
+ prompt: "Sal Scotto, sal@example.com"
184
+ )
185
+
186
+ person.name # => "Sal Scotto"
187
+ person.frozen? # => true
188
+ ```
189
+
190
+ ### Struct
191
+
192
+ ```ruby
193
+ Address = Struct.new(:street, :city, :zip, keyword_init: true)
194
+
195
+ address = instructor.chat(
196
+ model: "gpt-4o",
197
+ response_model: Address,
198
+ prompt: "Ship to: 123 Main St, Springfield, 62701"
199
+ )
200
+
201
+ address.city # => "Springfield"
202
+ ```
203
+
204
+ ### Custom schema
205
+
206
+ If your class defines `to_json_schema` (class or instance method), the adapter uses
207
+ it directly instead of introspecting setters — giving you full control over the schema
208
+ sent to the LLM while keeping the normal hydration and validation flow.
209
+
210
+ ```ruby
211
+ class Article
212
+ attr_accessor :title, :status
213
+
214
+ def self.to_json_schema
215
+ {
216
+ name: "article",
217
+ schema: {
218
+ type: "object",
219
+ properties: {
220
+ title: { type: "string", description: "Article headline" },
221
+ status: { type: "string", enum: %w[draft published archived] }
222
+ },
223
+ required: %w[title status]
224
+ }
225
+ }
226
+ end
227
+ end
228
+ ```
229
+
230
+ ## Streaming
231
+
232
+ Pass a `stream:` proc to receive chunks as they arrive. The final hydrated object
233
+ is still returned once the response completes.
234
+
235
+ ```ruby
236
+ instructor.chat(
237
+ model: "gpt-4o",
238
+ response_model: UserProfile,
239
+ prompt: "...",
240
+ stream: ->(chunk) { print chunk.content }
241
+ )
242
+ ```
243
+
244
+ ## Extraction mode: schema vs tools
245
+
246
+ By default `ruby_llm-instructor` uses `mode: :schema` — structured output via the
247
+ provider's native JSON schema constraint. Pass `mode: :tools` to use function
248
+ calling instead, which works with older models that pre-date structured output.
249
+
250
+ ```ruby
251
+ # Default — structured output (recommended for modern models)
252
+ instructor.chat(model: "gpt-4o", response_model: MyModel, prompt: "...", mode: :schema)
253
+
254
+ # Function-calling fallback — works with older models
255
+ instructor.chat(model: "gpt-3.5-turbo", response_model: MyModel, prompt: "...", mode: :tools)
256
+ ```
257
+
258
+ ## Auto-retry on validation failure
259
+
260
+ When the LLM returns data that fails `valid?`, `ruby_llm-instructor` feeds the
261
+ error messages back to the model and asks for a corrected response — up to
262
+ `max_retries` times (default: 3). If all retries are exhausted, a `RuntimeError`
263
+ is raised.
264
+
265
+ ```ruby
266
+ instructor.chat(
267
+ model: "gpt-4o",
268
+ response_model: LeadCapture,
269
+ prompt: "...",
270
+ max_retries: 5
271
+ )
272
+ ```
273
+
274
+ ## One model, any provider
275
+
276
+ The `model:` string is passed straight through to `ruby_llm`:
277
+
278
+ ```ruby
279
+ # OpenAI
280
+ instructor.chat(model: "gpt-4o", ...)
281
+
282
+ # Anthropic
283
+ instructor.chat(model: "claude-3-5-sonnet", ...)
284
+
285
+ # Ollama (local)
286
+ instructor.chat(model: "llama3", ...)
287
+ ```
288
+
289
+ ## What's in v0.1
290
+
291
+ - All `ruby_llm`-supported providers (OpenAI, Anthropic, Gemini, Ollama, …)
292
+ - Response models: PORO, ActiveModel, native dry-validation contract, duck-typed dry-v bridge, `Data.define`, `Struct`, custom `to_json_schema`
293
+ - Type inference from `ActiveModel::Attributes` (integer, number, boolean)
294
+ - Required vs. optional fields from presence validators
295
+ - Automatic retry-on-validation-failure with corrective prompt
296
+ - Streaming via `stream:` proc
297
+ - Function-calling fallback via `mode: :tools`
298
+
299
+ ## Development
300
+
301
+ ```bash
302
+ bin/setup
303
+ bundle exec rspec
304
+ ```
305
+
306
+ ## License
307
+
308
+ MIT
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
@@ -0,0 +1,79 @@
1
+ module RubyLLM
2
+ module Instructor
3
+ module Adapters
4
+ class RubyLlmSchemaAdapter
5
+ def initialize(model_klass)
6
+ @klass = model_klass
7
+ end
8
+
9
+ def build_schema
10
+ return build_dry_contract_schema if dry_contract?
11
+ return @klass.to_json_schema if @klass.respond_to?(:to_json_schema)
12
+ return @klass.new.to_json_schema if @klass.method_defined?(:to_json_schema)
13
+
14
+ attrs = attribute_definitions
15
+
16
+ RubyLLM::Schema.create do
17
+ attrs.each do |name, type, required|
18
+ opts = { required: required, description: "Extracted value for #{name}" }
19
+ case type
20
+ when :integer then integer name, **opts
21
+ when :number then number name, **opts
22
+ when :boolean then boolean name, **opts
23
+ else string name, **opts
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def attribute_definitions
32
+ if @klass.respond_to?(:members)
33
+ @klass.members.map { |m| [m.to_sym, :string, true] }
34
+ elsif @klass.respond_to?(:attribute_types)
35
+ required = presence_validated_fields
36
+ @klass.attribute_types.filter_map do |name, type|
37
+ next if name == "id"
38
+ [name.to_sym, map_active_model_type(type), required.include?(name)]
39
+ end
40
+ else
41
+ @klass.instance_methods(false)
42
+ .select { |m| m.to_s.end_with?("=") }
43
+ .map { |m| [m.to_s.chomp("=").to_sym, :string, true] }
44
+ end
45
+ end
46
+
47
+ def map_active_model_type(type)
48
+ case type.type
49
+ when :integer then :integer
50
+ when :float, :decimal then :number
51
+ when :boolean then :boolean
52
+ else :string
53
+ end
54
+ end
55
+
56
+ def dry_contract?
57
+ defined?(Dry::Validation::Contract) &&
58
+ @klass.is_a?(Class) &&
59
+ @klass < Dry::Validation::Contract
60
+ rescue TypeError
61
+ false
62
+ end
63
+
64
+ def build_dry_contract_schema
65
+ raw = @klass.schema.json_schema
66
+ { name: "response", schema: raw.reject { |k, _| k.to_s == "$schema" } }
67
+ end
68
+
69
+ def presence_validated_fields
70
+ return [] unless @klass.respond_to?(:_validators)
71
+
72
+ @klass._validators.select { |_, validators|
73
+ validators.any? { |v| v.is_a?(ActiveModel::Validations::PresenceValidator) }
74
+ }.keys.map(&:to_s)
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Instructor
5
+ class Client
6
+ def chat(model:, response_model:, prompt:, max_retries: 3, stream: nil, mode: :schema)
7
+ compiled_schema = Adapters::RubyLlmSchemaAdapter.new(response_model).build_schema
8
+ current_prompt = prompt
9
+ retries = 0
10
+
11
+ begin
12
+ session = RubyLLM.chat(model: model)
13
+ response = mode == :tools ? via_tools(session, compiled_schema, current_prompt, stream)
14
+ : via_schema(session, compiled_schema, current_prompt, stream)
15
+ parsed_data = response.content
16
+
17
+ unless parsed_data.is_a?(Hash)
18
+ raise ValidationError,
19
+ "Expected a structured JSON object matching the schema, " \
20
+ "got #{parsed_data.class} (#{parsed_data.inspect[0, 200]})"
21
+ end
22
+
23
+ errors = validate_payload(response_model, parsed_data)
24
+ raise ValidationError, errors.join(", ") if errors.any?
25
+
26
+ build_instance(response_model, parsed_data)
27
+
28
+ rescue ValidationError => e
29
+ if retries < max_retries
30
+ retries += 1
31
+ current_prompt = "Your structural response failed local validation rules: #{e.message}. Please fix the data matching the schema parameters perfectly."
32
+ retry
33
+ else
34
+ raise "ruby_llm-instructor failed validation after #{max_retries} attempts. Errors: #{e.message}"
35
+ end
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def via_schema(session, schema, prompt, stream)
42
+ session.with_schema(schema).ask(prompt, &stream)
43
+ end
44
+
45
+ def via_tools(session, schema, prompt, stream)
46
+ tool = extraction_tool_for(schema)
47
+ session.with_tool(tool, choice: :required, calls: :one).ask(prompt, &stream)
48
+ end
49
+
50
+ def extraction_tool_for(schema)
51
+ tool_params = schema.is_a?(Hash) ? (schema[:schema] || schema["schema"] || schema) : schema
52
+ Class.new(RubyLLM::Tool) do
53
+ description "Extract and return the structured data from the text"
54
+ singleton_class.define_method(:name) { "RubyLLMInstructorExtract" }
55
+ params tool_params
56
+ def execute(**args) = halt(args)
57
+ end
58
+ end
59
+
60
+ def dry_contract?(klass)
61
+ defined?(Dry::Validation::Contract) &&
62
+ klass.is_a?(Class) &&
63
+ klass < Dry::Validation::Contract
64
+ rescue TypeError
65
+ false
66
+ end
67
+
68
+ def validate_payload(response_model, parsed_data)
69
+ if dry_contract?(response_model)
70
+ result = response_model.new.call(parsed_data.transform_keys(&:to_sym))
71
+ return [] if result.success?
72
+ return result.errors.to_h.flat_map { |field, msgs| msgs.map { |m| "#{field} #{m}" } }
73
+ end
74
+
75
+ return [] unless response_model.respond_to?(:new)
76
+
77
+ instance = build_instance(response_model, parsed_data)
78
+ return [] unless instance.respond_to?(:valid?) && !instance.valid?
79
+
80
+ if instance.respond_to?(:errors) && instance.errors.respond_to?(:full_messages)
81
+ instance.errors.full_messages
82
+ else
83
+ ["Validation failed"]
84
+ end
85
+ end
86
+
87
+ def build_instance(response_model, parsed_data)
88
+ if dry_contract?(response_model)
89
+ fields = response_model.schema.key_map.map(&:name).map(&:to_sym)
90
+ return Data.define(*fields).new(**parsed_data.transform_keys(&:to_sym).slice(*fields))
91
+ end
92
+
93
+ if response_model.respond_to?(:members)
94
+ response_model.new(**parsed_data.transform_keys(&:to_sym))
95
+ else
96
+ instance = response_model.new
97
+ parsed_data.each do |key, value|
98
+ instance.send("#{key}=", value) if instance.respond_to?("#{key}=")
99
+ end
100
+ instance
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,5 @@
1
+ module RubyLLM
2
+ module Instructor
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,20 @@
1
+ require "ruby_llm"
2
+ require "ruby_llm/schema"
3
+ require "active_model"
4
+ require_relative "instructor/version"
5
+ require_relative "instructor/adapters/ruby_llm_schema"
6
+ require_relative "instructor/client"
7
+
8
+ begin
9
+ require "dry-validation"
10
+ require "dry/schema"
11
+ Dry::Schema.load_extensions(:json_schema)
12
+ rescue LoadError
13
+ nil
14
+ end
15
+
16
+ module RubyLLM
17
+ module Instructor
18
+ class ValidationError < StandardError; end
19
+ end
20
+ end
metadata ADDED
@@ -0,0 +1,157 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby_llm-instructor
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Sal Scotto Di Luzio
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: ruby_llm
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: 1.15.0
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: 1.15.0
26
+ - !ruby/object:Gem::Dependency
27
+ name: ruby_llm-schema
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 0.4.0
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: 0.4.0
40
+ - !ruby/object:Gem::Dependency
41
+ name: rspec
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: bundler
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '2.0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '2.0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: rake
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '13.0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '13.0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: activemodel
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '7.0'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '7.0'
96
+ - !ruby/object:Gem::Dependency
97
+ name: dry-validation
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '1.0'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '1.0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: simplecov
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '0.22'
117
+ type: :development
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '0.22'
124
+ description: Validates and coerces unstructured LLM responses directly into rich,
125
+ schema-validated Ruby objects with automatic self-correction loops.
126
+ executables: []
127
+ extensions: []
128
+ extra_rdoc_files: []
129
+ files:
130
+ - README.md
131
+ - Rakefile
132
+ - lib/ruby_llm/instructor.rb
133
+ - lib/ruby_llm/instructor/adapters/ruby_llm_schema.rb
134
+ - lib/ruby_llm/instructor/client.rb
135
+ - lib/ruby_llm/instructor/version.rb
136
+ licenses:
137
+ - MIT
138
+ metadata: {}
139
+ rdoc_options: []
140
+ require_paths:
141
+ - lib
142
+ required_ruby_version: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ required_rubygems_version: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - ">="
150
+ - !ruby/object:Gem::Version
151
+ version: '0'
152
+ requirements: []
153
+ rubygems_version: 3.6.9
154
+ specification_version: 4
155
+ summary: Structured outputs for LLMs in Ruby, powered by RubyLLM and Can be used with
156
+ ActiveModel, DryValidations or PORO objects that define a valid? method.
157
+ test_files: []