ruby_llm-instructor 0.1.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: efe5592fe81d6feb6e5a328c56ae00bca1bb3f8007826da4440d1bc042f12c7e
4
- data.tar.gz: 0d5b70673f1da2ef41686603c1c50bb237cb6d7dd338d921f5ed894c38dcb2a0
3
+ metadata.gz: ad714a8bf1c0fdec80e73420156ddc0d93fa9e48a82b81078918a2ab19e34651
4
+ data.tar.gz: 539d055dbb5dba7937fdd040b740329edcf1f794259963438ef777538a79c7db
5
5
  SHA512:
6
- metadata.gz: 8b0fb08a2c1c5f93639cfa79256029c3433b6ec4459d28082a69373464d08b6d4cc743b9617e7a4fb5eb1856f68f5cdad4efe3bd7d174753b42ca7d7e5aab7b7
7
- data.tar.gz: 0a536fbab7e9f6420e8d9899ec30d78232c4f55af2dd6ed5ce62b154dc7b22607196291c88e39c60ddeed6eefecb46e568e5375e5dcdf0682a39ff0ca3e976b6
6
+ metadata.gz: 36ef7c3fcbf4bedc8eb415e57d0858691e2a8710f558642d04a4fa5c367ef1c6ca41bf85b15dfd82ebf2d45e088ab45069bc8cdf0b5b3182f64b9d1b936de24f
7
+ data.tar.gz: d3bd594ecdecadc5c603f5eb70d8ca79e15a08de79a29765664a7fa36eb57000afb17dcd55a0b0c9a530ad2c7c6861165df28a58e558e825d96863cc179e67f5
data/README.md CHANGED
@@ -189,9 +189,15 @@ person.frozen? # => true
189
189
 
190
190
  ### Struct
191
191
 
192
+ Both `keyword_init: true` and positional structs are supported:
193
+
192
194
  ```ruby
195
+ # keyword_init (recommended)
193
196
  Address = Struct.new(:street, :city, :zip, keyword_init: true)
194
197
 
198
+ # positional — also works
199
+ Point = Struct.new(:x, :y)
200
+
195
201
  address = instructor.chat(
196
202
  model: "gpt-4o",
197
203
  response_model: Address,
@@ -246,6 +252,7 @@ instructor.chat(
246
252
  By default `ruby_llm-instructor` uses `mode: :schema` — structured output via the
247
253
  provider's native JSON schema constraint. Pass `mode: :tools` to use function
248
254
  calling instead, which works with older models that pre-date structured output.
255
+ Passing any other value raises `ArgumentError` immediately.
249
256
 
250
257
  ```ruby
251
258
  # Default — structured output (recommended for modern models)
@@ -258,9 +265,8 @@ instructor.chat(model: "gpt-3.5-turbo", response_model: MyModel, prompt: "...",
258
265
  ## Auto-retry on validation failure
259
266
 
260
267
  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 responseup to
262
- `max_retries` times (default: 3). If all retries are exhausted, a `RuntimeError`
263
- is raised.
268
+ error messages back to the model along with the original taskand asks for a
269
+ corrected response. This repeats up to `max_retries` times (default: 3).
264
270
 
265
271
  ```ruby
266
272
  instructor.chat(
@@ -271,6 +277,18 @@ instructor.chat(
271
277
  )
272
278
  ```
273
279
 
280
+ If all retries are exhausted a `RubyLLM::Instructor::ValidationError` is raised
281
+ (a `StandardError` subclass), carrying the final validation message:
282
+
283
+ ```ruby
284
+ begin
285
+ instructor.chat(model: "gpt-4o", response_model: LeadCapture, prompt: "...")
286
+ rescue RubyLLM::Instructor::ValidationError => e
287
+ # e.message => "ruby_llm-instructor failed validation after 3 attempts. Errors: ..."
288
+ Rails.logger.warn("LLM extraction failed: #{e.message}")
289
+ end
290
+ ```
291
+
274
292
  ## One model, any provider
275
293
 
276
294
  The `model:` string is passed straight through to `ruby_llm`:
@@ -286,13 +304,15 @@ instructor.chat(model: "claude-3-5-sonnet", ...)
286
304
  instructor.chat(model: "llama3", ...)
287
305
  ```
288
306
 
289
- ## What's in v0.1
307
+ ## What's in v0.2
290
308
 
291
309
  - 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`
310
+ - Response models: PORO, ActiveModel, native dry-validation contract, duck-typed dry-v bridge, `Data.define`, `Struct` (keyword and positional), custom `to_json_schema`
293
311
  - Type inference from `ActiveModel::Attributes` (integer, number, boolean)
294
312
  - Required vs. optional fields from presence validators
295
- - Automatic retry-on-validation-failure with corrective prompt
313
+ - Automatic retry-on-validation-failure with corrective prompt (original task preserved on each retry)
314
+ - `RubyLLM::Instructor::ValidationError` raised on exhaustion — rescueable by type
315
+ - `mode:` validation — `ArgumentError` on unknown values
296
316
  - Streaming via `stream:` proc
297
317
  - Function-calling fallback via `mode: :tools`
298
318
 
@@ -54,11 +54,7 @@ module RubyLLM
54
54
  end
55
55
 
56
56
  def dry_contract?
57
- defined?(Dry::Validation::Contract) &&
58
- @klass.is_a?(Class) &&
59
- @klass < Dry::Validation::Contract
60
- rescue TypeError
61
- false
57
+ Utils.dry_contract?(@klass)
62
58
  end
63
59
 
64
60
  def build_dry_contract_schema
@@ -3,7 +3,13 @@
3
3
  module RubyLLM
4
4
  module Instructor
5
5
  class Client
6
+ VALID_MODES = %i[schema tools].freeze
7
+
6
8
  def chat(model:, response_model:, prompt:, max_retries: 3, stream: nil, mode: :schema)
9
+ unless VALID_MODES.include?(mode)
10
+ raise ArgumentError, "Unknown mode #{mode.inspect}. Valid modes are: #{VALID_MODES.join(', ')}"
11
+ end
12
+
7
13
  compiled_schema = Adapters::RubyLlmSchemaAdapter.new(response_model).build_schema
8
14
  current_prompt = prompt
9
15
  retries = 0
@@ -28,10 +34,12 @@ module RubyLLM
28
34
  rescue ValidationError => e
29
35
  if retries < max_retries
30
36
  retries += 1
31
- current_prompt = "Your structural response failed local validation rules: #{e.message}. Please fix the data matching the schema parameters perfectly."
37
+ current_prompt = "Original task: #{prompt}\n\n" \
38
+ "Your previous response failed validation: #{e.message}. " \
39
+ "Please fix the data to match the schema exactly."
32
40
  retry
33
41
  else
34
- raise "ruby_llm-instructor failed validation after #{max_retries} attempts. Errors: #{e.message}"
42
+ raise ValidationError, "ruby_llm-instructor failed validation after #{max_retries} attempts. Errors: #{e.message}"
35
43
  end
36
44
  end
37
45
  end
@@ -58,11 +66,7 @@ module RubyLLM
58
66
  end
59
67
 
60
68
  def dry_contract?(klass)
61
- defined?(Dry::Validation::Contract) &&
62
- klass.is_a?(Class) &&
63
- klass < Dry::Validation::Contract
64
- rescue TypeError
65
- false
69
+ Utils.dry_contract?(klass)
66
70
  end
67
71
 
68
72
  def validate_payload(response_model, parsed_data)
@@ -91,7 +95,13 @@ module RubyLLM
91
95
  end
92
96
 
93
97
  if response_model.respond_to?(:members)
94
- response_model.new(**parsed_data.transform_keys(&:to_sym))
98
+ kwargs = parsed_data.transform_keys(&:to_sym)
99
+ begin
100
+ response_model.new(**kwargs)
101
+ rescue ArgumentError
102
+ # Positional Struct (no keyword_init:true) — fall back to positional args
103
+ response_model.new(*response_model.members.map { |m| kwargs[m] })
104
+ end
95
105
  else
96
106
  instance = response_model.new
97
107
  parsed_data.each do |key, value|
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Instructor
5
+ module Utils
6
+ # Returns true when +klass+ is a Dry::Validation::Contract subclass.
7
+ # Safely returns false when dry-validation is not loaded or klass is not a
8
+ # class (e.g. an instance, a module, or a Data object).
9
+ def dry_contract?(klass)
10
+ !!(defined?(Dry::Validation::Contract) &&
11
+ klass.is_a?(Class) &&
12
+ klass < Dry::Validation::Contract)
13
+ rescue TypeError
14
+ false
15
+ end
16
+ module_function :dry_contract?
17
+ end
18
+ end
19
+ end
20
+
@@ -1,5 +1,5 @@
1
1
  module RubyLLM
2
2
  module Instructor
3
- VERSION = "0.1.0"
3
+ VERSION = "0.2.0"
4
4
  end
5
5
  end
@@ -1,7 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "ruby_llm"
2
4
  require "ruby_llm/schema"
3
5
  require "active_model"
4
6
  require_relative "instructor/version"
7
+ require_relative "instructor/utils"
5
8
  require_relative "instructor/adapters/ruby_llm_schema"
6
9
  require_relative "instructor/client"
7
10
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_llm-instructor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sal Scotto Di Luzio
@@ -30,6 +30,9 @@ dependencies:
30
30
  - - ">="
31
31
  - !ruby/object:Gem::Version
32
32
  version: 0.4.0
33
+ - - "<"
34
+ - !ruby/object:Gem::Version
35
+ version: '2'
33
36
  type: :runtime
34
37
  prerelease: false
35
38
  version_requirements: !ruby/object:Gem::Requirement
@@ -37,6 +40,9 @@ dependencies:
37
40
  - - ">="
38
41
  - !ruby/object:Gem::Version
39
42
  version: 0.4.0
43
+ - - "<"
44
+ - !ruby/object:Gem::Version
45
+ version: '2'
40
46
  - !ruby/object:Gem::Dependency
41
47
  name: rspec
42
48
  requirement: !ruby/object:Gem::Requirement
@@ -132,10 +138,15 @@ files:
132
138
  - lib/ruby_llm/instructor.rb
133
139
  - lib/ruby_llm/instructor/adapters/ruby_llm_schema.rb
134
140
  - lib/ruby_llm/instructor/client.rb
141
+ - lib/ruby_llm/instructor/utils.rb
135
142
  - lib/ruby_llm/instructor/version.rb
143
+ homepage: https://github.com/washu/ruby_llm-instructor
136
144
  licenses:
137
145
  - MIT
138
- metadata: {}
146
+ metadata:
147
+ source_code_uri: https://github.com/washu/ruby_llm-instructor
148
+ changelog_uri: https://github.com/washu/ruby_llm-instructor/blob/main/CHANGELOG.md
149
+ bug_tracker_uri: https://github.com/washu/ruby_llm-instructor/issues
139
150
  rdoc_options: []
140
151
  require_paths:
141
152
  - lib