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 +7 -0
- data/README.md +308 -0
- data/Rakefile +6 -0
- data/lib/ruby_llm/instructor/adapters/ruby_llm_schema.rb +79 -0
- data/lib/ruby_llm/instructor/client.rb +105 -0
- data/lib/ruby_llm/instructor/version.rb +5 -0
- data/lib/ruby_llm/instructor.rb +20 -0
- metadata +157 -0
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
|
+
[](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,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,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: []
|