ruby_llm-contract 0.2.1 → 0.2.3

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: 2803caa3f400a1c3ca1f2a3c062bf1e79080d25f4dd3d9a652300007eb363d21
4
- data.tar.gz: 27cfd8b34f7ddeb2e2af72142c2d38994a84cd50a2ecd494c173d9fdaf220329
3
+ metadata.gz: '080fd81afd87ad234cf66f7577080a4ac55a59f890e0c8c479479fccec57ad32'
4
+ data.tar.gz: cdabbac3ea1d81e1abd3cb850e927f410d98282bd23111be79463804ea4d84b9
5
5
  SHA512:
6
- metadata.gz: f587bc603e2270a6e96979a62421bb0ac45cf68ae1d0ca53518bca6e394a72fd6ab2b8cb3622e0f98104fa0212aa3fe45fb5814a2aea6560b0ae2ae57a2f3041
7
- data.tar.gz: b6dd0147f09e8936ac2198f95a26188913095cbb26d3a073a233c7822cb5441b04567ae88a1c68dacd79a792fe84e6479fd771282c1a51bfedb3fa0609ff0ebb
6
+ metadata.gz: 294b36f7264a2ba8b04334f3fd1c6b4433466a04c6be4aaccf23a92df3c7e92d04061ace018aa5243e28a9ef4fe64abc7f6de5ec11143c32bf5466bf591b9130
7
+ data.tar.gz: d7447319e3389264571209bc84d7dc84a441ffb76d1f64506d9cac2dc1953d26ba8cf3e1eb4169adf64576f1be7ae182bdf0c6e8e6b876220b102cea1e653fa6
data/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.3 (2026-03-23)
4
+
5
+ Production hardening from senior Rails review panel.
6
+
7
+ - **`around_call` propagates exceptions** — no longer silently swallows DB errors, timeouts, etc. User who wants swallowing can rescue in their block.
8
+ - **Nil section content skipped** — `section "X", nil` no longer renders `"null"` to the LLM. Section is omitted entirely.
9
+ - **Range support in `expected:`** — `expected: { score: 1..5 }` works in `add_case`. Previously only Regexp was supported.
10
+ - **`Trace#dig`** — `trace.dig(:usage, :input_tokens)` works on both Step and Pipeline traces.
11
+
12
+ ## 0.2.2 (2026-03-23)
13
+
14
+ Fixes from first real-world integration (persona_tool).
15
+
16
+ - **`around_call` fires per-run** — not per-attempt. With retry_policy, callback fires once with final result. Signature: `around_call { |step, input, result| ... }`
17
+ - **`Result#trace` always `Trace` object** — never bare Hash. `result.trace.model` works on success AND failure.
18
+ - **`around_call` exception safe** — warns and returns result instead of crashing.
19
+ - **`model` DSL** — `model "gpt-4o-mini"` per-step. Priority: context > step DSL > global config.
20
+ - **Test adapter `raw_output` always String** — Hash/Array normalized to `.to_json`.
21
+ - **`Trace#dig`** — `trace.dig(:usage, :input_tokens)` works.
22
+
3
23
  ## 0.2.1 (2026-03-23)
4
24
 
5
25
  Production DX improvements from first real-world integration (persona_tool).
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ruby_llm-contract (0.2.1)
4
+ ruby_llm-contract (0.2.3)
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.1)
168
+ ruby_llm-contract (0.2.3)
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
@@ -20,7 +20,7 @@ module RubyLLM
20
20
 
21
21
  def normalize_response(response)
22
22
  case response
23
- when Hash, Array then response
23
+ when Hash, Array then response.to_json
24
24
  when nil then ""
25
25
  else response.to_s
26
26
  end
@@ -67,6 +67,7 @@ module RubyLLM
67
67
  def match?(expected_value, actual)
68
68
  case expected_value
69
69
  when ::Regexp then actual.to_s.match?(expected_value)
70
+ when ::Range then expected_value.include?(actual)
70
71
  else expected_value == actual
71
72
  end
72
73
  end
@@ -27,6 +27,8 @@ module RubyLLM
27
27
  "missing key: #{key}"
28
28
  elsif expected_value.is_a?(::Regexp)
29
29
  mismatch_message(key, expected_value, actual) unless actual.to_s.match?(expected_value)
30
+ elsif expected_value.is_a?(::Range)
31
+ mismatch_message(key, expected_value, actual) unless expected_value.include?(actual)
30
32
  elsif actual != expected_value
31
33
  mismatch_message(key, expected_value, actual)
32
34
  end
@@ -25,6 +25,13 @@ module RubyLLM
25
25
  public_send(key)
26
26
  end
27
27
 
28
+ def dig(key, *rest)
29
+ value = self[key]
30
+ return value if rest.empty? || value.nil?
31
+
32
+ value.dig(*rest)
33
+ end
34
+
28
35
  def to_h
29
36
  { trace_id: @trace_id, total_latency_ms: @total_latency_ms,
30
37
  total_usage: @total_usage, step_traces: @step_traces,
@@ -38,6 +38,8 @@ module RubyLLM
38
38
  end
39
39
 
40
40
  def render_section_node(node, variables, messages)
41
+ return if node.content.nil?
42
+
41
43
  section_content = node.content.is_a?(Hash) || node.content.is_a?(Array) ? node.content.to_json : node.content
42
44
  return unless content_present?(section_content)
43
45
 
@@ -63,14 +63,16 @@ module RubyLLM
63
63
  def run(input, context: {})
64
64
  warn_unknown_context_keys(context)
65
65
  adapter = resolve_adapter(context)
66
- default_model = context[:model] || RubyLLM::Contract.configuration.default_model
66
+ default_model = context[:model] || model || RubyLLM::Contract.configuration.default_model
67
67
  policy = retry_policy
68
68
 
69
- if policy
70
- run_with_retry(input, adapter: adapter, default_model: default_model, policy: policy)
71
- else
72
- run_once(input, adapter: adapter, model: default_model, context_temperature: context[:temperature])
73
- end
69
+ result = if policy
70
+ run_with_retry(input, adapter: adapter, default_model: default_model, policy: policy)
71
+ else
72
+ run_once(input, adapter: adapter, model: default_model, context_temperature: context[:temperature])
73
+ end
74
+
75
+ invoke_around_call(input, result)
74
76
  end
75
77
 
76
78
  def build_messages(input)
@@ -101,24 +103,25 @@ module RubyLLM
101
103
 
102
104
  def run_once(input, adapter:, model:, context_temperature: nil)
103
105
  effective_temp = context_temperature || temperature
104
- runner = Runner.new(
106
+ Runner.new(
105
107
  input_type: input_type, output_type: output_type,
106
108
  prompt_block: prompt, contract_definition: effective_contract,
107
109
  adapter: adapter, model: model, output_schema: output_schema,
108
110
  max_output: max_output, max_input: max_input, max_cost: max_cost,
109
111
  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
112
+ ).call(input)
117
113
  rescue ArgumentError => e
118
114
  Result.new(status: :input_error, raw_output: nil, parsed_output: nil,
119
115
  validation_errors: [e.message])
120
116
  end
121
117
 
118
+ def invoke_around_call(input, result)
119
+ return result unless around_call
120
+
121
+ around_call.call(self, input, result)
122
+ result
123
+ end
124
+
122
125
  def effective_contract
123
126
  base = contract
124
127
  extra = class_validates
@@ -127,6 +127,18 @@ module RubyLLM
127
127
  end
128
128
  end
129
129
 
130
+ def model(name = nil)
131
+ if name
132
+ return @model = name
133
+ end
134
+
135
+ if defined?(@model)
136
+ @model
137
+ elsif superclass.respond_to?(:model)
138
+ superclass.model
139
+ end
140
+ end
141
+
130
142
  def temperature(value = nil)
131
143
  if value
132
144
  unless value.is_a?(Numeric) && value >= 0 && value <= 2
@@ -6,12 +6,12 @@ module RubyLLM
6
6
  class Result
7
7
  attr_reader :status, :raw_output, :parsed_output, :validation_errors, :trace
8
8
 
9
- def initialize(status:, raw_output:, parsed_output:, validation_errors: [], trace: {})
9
+ def initialize(status:, raw_output:, parsed_output:, validation_errors: [], trace: nil)
10
10
  @status = status
11
11
  @raw_output = raw_output
12
12
  @parsed_output = parsed_output
13
13
  @validation_errors = validation_errors.freeze
14
- @trace = trace.freeze
14
+ @trace = normalize_trace(trace)
15
15
  freeze
16
16
  end
17
17
 
@@ -23,6 +23,19 @@ module RubyLLM
23
23
  @status != :ok
24
24
  end
25
25
 
26
+ private
27
+
28
+ def normalize_trace(trace)
29
+ case trace
30
+ when Trace then trace
31
+ when Hash then Trace.new(**trace)
32
+ when nil then Trace.new
33
+ else trace
34
+ end.freeze
35
+ end
36
+
37
+ public
38
+
26
39
  def to_s
27
40
  if ok?
28
41
  "#{@status} (#{@trace})"
@@ -26,6 +26,13 @@ module RubyLLM
26
26
  public_send(key)
27
27
  end
28
28
 
29
+ def dig(key, *rest)
30
+ value = self[key]
31
+ return value if rest.empty? || value.nil?
32
+
33
+ value.dig(*rest)
34
+ end
35
+
29
36
  def key?(key)
30
37
  KNOWN_KEYS.include?(key.to_sym) && !public_send(key).nil?
31
38
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RubyLLM
4
4
  module Contract
5
- VERSION = "0.2.1"
5
+ VERSION = "0.2.3"
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.1
4
+ version: 0.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justyna