ruby_llm-contract 0.2.0 → 0.2.1

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: dc8b1278c5464978cfc50d87ac90cbde94c2a5920b00996365a2e366bb27f1e6
4
- data.tar.gz: daf51e9b66472464137d371439f503317b84706f167fa74c2118635ae82823b1
3
+ metadata.gz: 2803caa3f400a1c3ca1f2a3c062bf1e79080d25f4dd3d9a652300007eb363d21
4
+ data.tar.gz: 27cfd8b34f7ddeb2e2af72142c2d38994a84cd50a2ecd494c173d9fdaf220329
5
5
  SHA512:
6
- metadata.gz: 7899b4c2df5e7824a5104c24698b728b017e996cced82022c26d81167e8876085fcec93396d13ce67aed7034cfbd0cfbde2e0ebd76376a8dd198d6a561d273d7
7
- data.tar.gz: 7ca10ee16ea71eda609439546b9f02b20e184e9c6a8861d88a172c0e75ca611cf3c2bddf8827b820b31aa9cc5baf88bdaf9b708732b78f3e6321438030186ef8
6
+ metadata.gz: f587bc603e2270a6e96979a62421bb0ac45cf68ae1d0ca53518bca6e394a72fd6ab2b8cb3622e0f98104fa0212aa3fe45fb5814a2aea6560b0ae2ae57a2f3041
7
+ data.tar.gz: b6dd0147f09e8936ac2198f95a26188913095cbb26d3a073a233c7822cb5441b04567ae88a1c68dacd79a792fe84e6479fd771282c1a51bfedb3fa0609ff0ebb
data/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.1 (2026-03-23)
4
+
5
+ Production DX improvements from first real-world integration (persona_tool).
6
+
7
+ ### Features
8
+
9
+ - **`temperature` DSL** — `temperature 0.3` in step definition, overridable via `context: { temperature: 0.7 }`. RubyLLM handles per-model normalization natively.
10
+ - **`around_call` hook** — callback for logging, metrics, observability. Replaces need for custom middleware.
11
+ - **`build_messages` public** — inspect rendered prompt without running the step.
12
+ - **`stub_step` RSpec helper** — `stub_step(MyStep, response: { ... })` reduces test boilerplate. Auto-included via `require "ruby_llm/contract/rspec"`.
13
+ - **`estimate_cost` / `estimate_eval_cost`** — predict spend before API calls.
14
+
15
+ ### Fixes
16
+
17
+ - **Reload lifecycle** — `load_evals!` clears definitions before re-loading. Railtie hooks `config.to_prepare` for development reload. `define_eval` warns on duplicate name (suppressed during reload).
18
+ - **Pipeline eval cost** — uses `Pipeline::Trace#total_cost` (all steps), not just last step.
19
+ - **Adapter isolation** — `compare_models` and `run_all_own_evals` deep-dup context per run.
20
+ - **Offline mode** — cases without adapter return `:skipped` instead of crashing. Skipped cases excluded from score.
21
+ - **`expected_traits`** reachable from `define_eval` DSL via `add_case`.
22
+ - **`verify`** raises when both positional and `expect:` keyword provided.
23
+ - **`best_for`** excludes zero-score models from recommendation.
24
+ - **`print_summary`** replaces `pretty_print` (avoids `Kernel#pretty_print` shadow).
25
+ - **`CaseResult#to_h`** round-trips correctly (`name:` key).
26
+
27
+ ### Docs
28
+
29
+ - All 5 guides updated for v0.2 API
30
+ - Symbol keys documented
31
+ - Retry model priority documented
32
+ - Test adapter format documented
33
+
34
+ ### Stats
35
+
36
+ - 1077 tests, 0 failures
37
+ - 3 architecture review rounds, 32 findings fixed
38
+
3
39
  ## 0.2.0 (2026-03-23)
4
40
 
5
41
  Contracts for LLM quality. Know which model to use, what it costs, and when accuracy drops.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ruby_llm-contract (0.2.0)
4
+ ruby_llm-contract (0.2.1)
5
5
  dry-types (~> 1.7)
6
6
  ruby_llm (~> 1.0)
7
7
  ruby_llm-schema (~> 0.3)
@@ -165,7 +165,7 @@ CHECKSUMS
165
165
  rubocop-ast (1.49.1) sha256=4412f3ee70f6fe4546cc489548e0f6fcf76cafcfa80fa03af67098ffed755035
166
166
  ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
167
167
  ruby_llm (1.14.0) sha256=57c6f7034fc4a44504ea137d70f853b07824f1c1cdbe774ab3ab3522e7098deb
168
- ruby_llm-contract (0.2.0)
168
+ ruby_llm-contract (0.2.1)
169
169
  ruby_llm-schema (0.3.0) sha256=a591edc5ca1b7f0304f0e2261de61ba4b3bea17be09f5cf7558153adfda3dec6
170
170
  unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42
171
171
  unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Contract
5
+ module RSpec
6
+ module Helpers
7
+ # Stub a step to return a canned response without API calls.
8
+ #
9
+ # stub_step(ClassifyTicket, response: { priority: "high" })
10
+ # result = ClassifyTicket.run("test")
11
+ # result.parsed_output # => {priority: "high"}
12
+ #
13
+ # For multiple sequential responses:
14
+ # stub_step(ClassifyTicket, responses: [{ a: 1 }, { a: 2 }])
15
+ #
16
+ def stub_step(step_class, response: nil, responses: nil)
17
+ adapter = if responses
18
+ Adapters::Test.new(responses: responses.map { |r| r.is_a?(String) ? r : r.to_json })
19
+ else
20
+ content = response.is_a?(String) ? response : response.to_json
21
+ Adapters::Test.new(response: content)
22
+ end
23
+ RubyLLM::Contract.configure { |c| c.default_adapter = adapter }
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -4,3 +4,8 @@ require "ruby_llm/contract"
4
4
 
5
5
  require_relative "rspec/satisfy_contract"
6
6
  require_relative "rspec/pass_eval"
7
+ require_relative "rspec/helpers"
8
+
9
+ RSpec.configure do |config|
10
+ config.include RubyLLM::Contract::RSpec::Helpers
11
+ end if defined?(::RSpec)
@@ -69,10 +69,18 @@ module RubyLLM
69
69
  if policy
70
70
  run_with_retry(input, adapter: adapter, default_model: default_model, policy: policy)
71
71
  else
72
- run_once(input, adapter: adapter, model: default_model)
72
+ run_once(input, adapter: adapter, model: default_model, context_temperature: context[:temperature])
73
73
  end
74
74
  end
75
75
 
76
+ def build_messages(input)
77
+ dynamic = prompt.arity >= 1
78
+ ast = Prompt::Builder.build(input: dynamic ? input : nil, &prompt)
79
+ variables = dynamic ? {} : { input: input }
80
+ variables.merge!(input.transform_keys(&:to_sym)) if !dynamic && input.is_a?(Hash)
81
+ Prompt::Renderer.render(ast, variables: variables)
82
+ end
83
+
76
84
  private
77
85
 
78
86
  def warn_unknown_context_keys(context)
@@ -91,13 +99,21 @@ module RubyLLM
91
99
  "{ |c| c.default_adapter = ... } or pass context: { adapter: ... }"
92
100
  end
93
101
 
94
- def run_once(input, adapter:, model:)
95
- Runner.new(
102
+ def run_once(input, adapter:, model:, context_temperature: nil)
103
+ effective_temp = context_temperature || temperature
104
+ runner = Runner.new(
96
105
  input_type: input_type, output_type: output_type,
97
106
  prompt_block: prompt, contract_definition: effective_contract,
98
107
  adapter: adapter, model: model, output_schema: output_schema,
99
- max_output: max_output, max_input: max_input, max_cost: max_cost
100
- ).call(input)
108
+ max_output: max_output, max_input: max_input, max_cost: max_cost,
109
+ temperature: effective_temp
110
+ )
111
+
112
+ if around_call
113
+ around_call.call(self, input) { runner.call(input) }
114
+ else
115
+ runner.call(input)
116
+ end
101
117
  rescue ArgumentError => e
102
118
  Result.new(status: :input_error, raw_output: nil, parsed_output: nil,
103
119
  validation_errors: [e.message])
@@ -118,14 +134,6 @@ module RubyLLM
118
134
  )
119
135
  end
120
136
 
121
- def build_messages(input)
122
- dynamic = prompt.arity >= 1
123
- ast = Prompt::Builder.build(input: dynamic ? input : nil, &prompt)
124
- variables = dynamic ? {} : { input: input }
125
- variables.merge!(input.transform_keys(&:to_sym)) if !dynamic && input.is_a?(Hash)
126
- Prompt::Renderer.render(ast, variables: variables)
127
- end
128
-
129
137
  def json_compatible_type?(type)
130
138
  type == RubyLLM::Contract::Types::Hash || type == Hash ||
131
139
  type == RubyLLM::Contract::Types::Array || type == Array ||
@@ -127,6 +127,34 @@ module RubyLLM
127
127
  end
128
128
  end
129
129
 
130
+ def temperature(value = nil)
131
+ if value
132
+ unless value.is_a?(Numeric) && value >= 0 && value <= 2
133
+ raise ArgumentError, "temperature must be 0.0-2.0, got #{value}"
134
+ end
135
+
136
+ return @temperature = value
137
+ end
138
+
139
+ if defined?(@temperature)
140
+ @temperature
141
+ elsif superclass.respond_to?(:temperature)
142
+ superclass.temperature
143
+ end
144
+ end
145
+
146
+ def around_call(&block)
147
+ if block
148
+ return @around_call = block
149
+ end
150
+
151
+ if defined?(@around_call) && @around_call
152
+ @around_call
153
+ elsif superclass.respond_to?(:around_call)
154
+ superclass.around_call
155
+ end
156
+ end
157
+
130
158
  def retry_policy(models: nil, attempts: nil, retry_on: nil, &block)
131
159
  if block || models || attempts
132
160
  return @retry_policy = RetryPolicy.new(models: models, attempts: attempts, retry_on: retry_on, &block)
@@ -8,7 +8,7 @@ module RubyLLM
8
8
 
9
9
  def initialize(input_type:, output_type:, prompt_block:, contract_definition:,
10
10
  adapter:, model:, output_schema: nil, max_output: nil,
11
- max_input: nil, max_cost: nil)
11
+ max_input: nil, max_cost: nil, temperature: nil)
12
12
  @input_type = input_type
13
13
  @output_type = output_type
14
14
  @prompt_block = prompt_block
@@ -19,6 +19,7 @@ module RubyLLM
19
19
  @max_output = max_output
20
20
  @max_input = max_input
21
21
  @max_cost = max_cost
22
+ @temperature = temperature
22
23
  end
23
24
 
24
25
  def call(input)
@@ -84,6 +85,7 @@ module RubyLLM
84
85
  { model: @model }.tap do |opts|
85
86
  opts[:schema] = @output_schema if @output_schema
86
87
  opts[:max_tokens] = @max_output if @max_output
88
+ opts[:temperature] = @temperature if @temperature
87
89
  end
88
90
  end
89
91
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RubyLLM
4
4
  module Contract
5
- VERSION = "0.2.0"
5
+ VERSION = "0.2.1"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_llm-contract
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justyna
@@ -129,6 +129,7 @@ files:
129
129
  - lib/ruby_llm/contract/railtie.rb
130
130
  - lib/ruby_llm/contract/rake_task.rb
131
131
  - lib/ruby_llm/contract/rspec.rb
132
+ - lib/ruby_llm/contract/rspec/helpers.rb
132
133
  - lib/ruby_llm/contract/rspec/pass_eval.rb
133
134
  - lib/ruby_llm/contract/rspec/satisfy_contract.rb
134
135
  - lib/ruby_llm/contract/step.rb