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 +4 -4
- data/CHANGELOG.md +20 -0
- data/Gemfile.lock +2 -2
- data/lib/ruby_llm/contract/adapters/test.rb +1 -1
- data/lib/ruby_llm/contract/eval/case_result.rb +1 -0
- data/lib/ruby_llm/contract/eval/evaluator/json_includes.rb +2 -0
- data/lib/ruby_llm/contract/pipeline/trace.rb +7 -0
- data/lib/ruby_llm/contract/prompt/renderer.rb +2 -0
- data/lib/ruby_llm/contract/step/base.rb +17 -14
- data/lib/ruby_llm/contract/step/dsl.rb +12 -0
- data/lib/ruby_llm/contract/step/result.rb +15 -2
- data/lib/ruby_llm/contract/step/trace.rb +7 -0
- data/lib/ruby_llm/contract/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '080fd81afd87ad234cf66f7577080a4ac55a59f890e0c8c479479fccec57ad32'
|
|
4
|
+
data.tar.gz: cdabbac3ea1d81e1abd3cb850e927f410d98282bd23111be79463804ea4d84b9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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.
|
|
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
|
|
@@ -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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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
|
|
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
|