squishling 0.1.0 → 0.3.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 +4 -4
- data/CHANGELOG.md +123 -0
- data/README.md +117 -35
- data/docs/configuration.md +113 -32
- data/docs/failures.md +111 -14
- data/docs/harnesses.md +198 -0
- data/docs/measuring-tokens.md +148 -0
- data/docs/naming.md +43 -0
- data/docs/routing.md +46 -31
- data/docs/schemas.md +32 -2
- data/lib/squishling/appendices.rb +3 -3
- data/lib/squishling/class_methods.rb +81 -31
- data/lib/squishling/collisions.rb +54 -0
- data/lib/squishling/configuration.rb +58 -9
- data/lib/squishling/definition.rb +78 -39
- data/lib/squishling/errors.rb +29 -5
- data/lib/squishling/harness.rb +153 -0
- data/lib/squishling/invoker.rb +223 -74
- data/lib/squishling/judge.rb +142 -0
- data/lib/squishling/llm_client.rb +150 -0
- data/lib/squishling/model_path.rb +129 -0
- data/lib/squishling/output_check.rb +109 -0
- data/lib/squishling/params.rb +39 -2
- data/lib/squishling/router.rb +13 -7
- data/lib/squishling/schema.rb +45 -3
- data/lib/squishling/source.rb +5 -5
- data/lib/squishling/squawk.rb +52 -0
- data/lib/squishling/version.rb +1 -1
- data/lib/squishling.rb +25 -5
- metadata +13 -3
|
@@ -9,38 +9,47 @@ module Squishling
|
|
|
9
9
|
|
|
10
10
|
attr_reader :klass, :name, :call_context
|
|
11
11
|
|
|
12
|
-
def initialize(klass:, name:,
|
|
13
|
-
provider: nil, params: nil, predicate: nil, fallback: nil,
|
|
12
|
+
def initialize(klass:, name:, purpose: nil, append_to_purpose: nil, output_schema: nil, model: nil,
|
|
13
|
+
provider: nil, params: nil, harness: nil, predicate: nil, fallback: nil, validator: nil, squawk: nil,
|
|
14
|
+
call_context: {})
|
|
14
15
|
@klass = klass
|
|
15
16
|
@name = name
|
|
16
|
-
@
|
|
17
|
-
@
|
|
17
|
+
@purpose = purpose
|
|
18
|
+
@append_to_purpose = append_to_purpose
|
|
18
19
|
@output_schema = output_schema
|
|
19
20
|
@model = model
|
|
20
21
|
@provider = provider
|
|
21
22
|
@params = params
|
|
23
|
+
@harness = harness
|
|
22
24
|
@predicate = predicate
|
|
23
25
|
@fallback = fallback
|
|
26
|
+
@validator = validator
|
|
27
|
+
@squawk = squawk
|
|
24
28
|
@call_context = call_context
|
|
25
29
|
end
|
|
26
30
|
|
|
27
|
-
# This definition with one call's overrides on top. The output schema
|
|
28
|
-
# the call must still return the method's result type.
|
|
29
|
-
def for_call(
|
|
30
|
-
|
|
31
|
+
# This definition with one call's overrides on top. The output schema, predicate, fallback, and validator
|
|
32
|
+
# can't be overridden: the call must still return the method's result type.
|
|
33
|
+
def for_call(purpose: nil, append_to_purpose: nil, context: nil, model: nil, escalation: nil,
|
|
34
|
+
provider: nil, params: nil, harness: nil)
|
|
35
|
+
call_path = ModelPath.declare(model, escalation, "#{label} squish!")
|
|
36
|
+
raise ConfigurationError, "#{label}: squish! provider: needs a model: or escalation:" if provider && !call_path
|
|
31
37
|
unless context.nil? || (context.is_a?(Hash) && context.each_key.all?(NAME_KEY))
|
|
32
38
|
raise ConfigurationError, "#{label}: squish! context: must be a Hash with String or Symbol keys"
|
|
33
39
|
end
|
|
34
40
|
|
|
35
|
-
appended = Appendices.normalize(
|
|
41
|
+
appended = Appendices.normalize(append_to_purpose, "#{label} squish!") unless append_to_purpose.nil?
|
|
36
42
|
call_params = params && Params.normalize(params, "#{label} squish! params")
|
|
43
|
+
call_harness = Harness.normalize(harness, "#{label} squish!") unless harness.nil?
|
|
37
44
|
self.class.new(
|
|
38
45
|
klass:, name:, output_schema: @output_schema, predicate: @predicate, fallback: @fallback,
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
46
|
+
validator: @validator, squawk: @squawk,
|
|
47
|
+
purpose: purpose || @purpose,
|
|
48
|
+
append_to_purpose: [*@append_to_purpose, *appended],
|
|
49
|
+
model: call_path || @model, provider: call_path ? provider : @provider,
|
|
42
50
|
# merge, not Params.resolve: a nil at the method level must still unset the class's key.
|
|
43
51
|
params: call_params ? (@params || {}).merge(call_params) : @params,
|
|
52
|
+
harness: call_harness || @harness,
|
|
44
53
|
call_context: @call_context.merge((context || {}).transform_keys(&:to_sym))
|
|
45
54
|
)
|
|
46
55
|
end
|
|
@@ -49,17 +58,17 @@ module Squishling
|
|
|
49
58
|
"#{klass}##{name}"
|
|
50
59
|
end
|
|
51
60
|
|
|
52
|
-
# The system prompt: the
|
|
53
|
-
def
|
|
54
|
-
value = @
|
|
61
|
+
# The system prompt: the purpose, then each append_to_purpose section.
|
|
62
|
+
def purpose(receiver)
|
|
63
|
+
value = @purpose || klass.purpose
|
|
55
64
|
value = receiver.instance_exec(&value) if value.is_a?(Proc)
|
|
56
65
|
return value if value.nil? || value.empty?
|
|
57
66
|
|
|
58
|
-
[value, *Appendices.render(
|
|
67
|
+
[value, *Appendices.render(append_to_purpose, receiver, label)].join("\n\n")
|
|
59
68
|
end
|
|
60
69
|
|
|
61
|
-
def
|
|
62
|
-
Appendices.resolve(klass.
|
|
70
|
+
def append_to_purpose
|
|
71
|
+
Appendices.resolve(klass.squishling_append_to_purpose + (@append_to_purpose || []))
|
|
63
72
|
end
|
|
64
73
|
|
|
65
74
|
def schema
|
|
@@ -67,20 +76,20 @@ module Squishling
|
|
|
67
76
|
raw && Schema.for(raw)
|
|
68
77
|
end
|
|
69
78
|
|
|
70
|
-
|
|
71
|
-
|
|
79
|
+
# One ModelPath::Step per attempt, from the first level (method, class, or config) that declares a model
|
|
80
|
+
# or escalation, or a single attempt on RubyLLM's default model. A provider travels with the model declared
|
|
81
|
+
# at the same level, so a per-method Anthropic model never inherits a class-level OpenAI provider.
|
|
82
|
+
def escalation_path
|
|
83
|
+
config = Squishling.config
|
|
84
|
+
entries, provider = [[@model, @provider], [klass.squishling_model_path, klass.squishling_provider],
|
|
85
|
+
[config.default_model_path, config.default_provider]].find(&:first)
|
|
86
|
+
entries ||= [{ model: nil, attempts: 1, forward_rejected: true }]
|
|
87
|
+
ModelPath.steps(entries, provider:, params:)
|
|
72
88
|
end
|
|
73
89
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
# A provider travels with the model declared at the same level (method, class, or config),
|
|
79
|
-
# so a per-method Anthropic model never inherits a class-level OpenAI provider.
|
|
80
|
-
def model_and_provider
|
|
81
|
-
config = Squishling.config
|
|
82
|
-
[[@model, @provider], [klass.squishling_model, klass.squishling_provider],
|
|
83
|
-
[config.default_model, config.default_provider]].find(&:first) || [nil, nil]
|
|
90
|
+
# How the escalation is used (see Harness): the method's, the class's, the configured default, or :escalation.
|
|
91
|
+
def harness
|
|
92
|
+
@harness || klass.squishling_harness || Squishling.config.default_harness || Harness::DEFAULT
|
|
84
93
|
end
|
|
85
94
|
|
|
86
95
|
# Generation params: config defaults, overridden key by key by the class, then by the method.
|
|
@@ -88,6 +97,17 @@ module Squishling
|
|
|
88
97
|
Params.resolve(Squishling.config.default_params, klass.squishling_params, @params)
|
|
89
98
|
end
|
|
90
99
|
|
|
100
|
+
# Extra output checks run on schema-valid LLM results (see ClassMethods#squish_validate).
|
|
101
|
+
def validator
|
|
102
|
+
@validator || klass.squishling_validator
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# The observability hook: the method's, else the class's, else the configured one. `false` at a level
|
|
106
|
+
# silences the levels above it.
|
|
107
|
+
def squawk
|
|
108
|
+
[@squawk, klass.squishling_squawk, Squishling.config.squawk].find { |hook| !hook.nil? } || nil
|
|
109
|
+
end
|
|
110
|
+
|
|
91
111
|
def context_names
|
|
92
112
|
klass.squishling_context_names
|
|
93
113
|
end
|
|
@@ -111,43 +131,62 @@ module Squishling
|
|
|
111
131
|
coerce(receiver.instance_exec(e, **inputs, &handler))
|
|
112
132
|
end
|
|
113
133
|
|
|
114
|
-
# Deterministic return values:
|
|
115
|
-
#
|
|
134
|
+
# Deterministic and fallback return values: with an output_schema, every value is validated and
|
|
135
|
+
# typed like an LLM result. Without one, the value passes through untouched.
|
|
116
136
|
def coerce(value)
|
|
117
|
-
|
|
137
|
+
schema ? build_result(value, squished: false) : value
|
|
118
138
|
end
|
|
119
139
|
|
|
140
|
+
# A result of this schema's own class passes through; any other result is re-validated via to_h.
|
|
120
141
|
def build_result(attrs, squished:)
|
|
121
142
|
raise ConfigurationError, "#{label} has no output_schema" unless schema
|
|
122
|
-
return attrs if attrs.is_a?(
|
|
143
|
+
return attrs if schema.result_class && attrs.is_a?(schema.result_class)
|
|
123
144
|
|
|
124
|
-
|
|
125
|
-
errors =
|
|
145
|
+
attrs = attrs.to_h if attrs.is_a?(Result::Instance)
|
|
146
|
+
data, errors = jsonify_return(attrs)
|
|
147
|
+
errors = schema.validate(data) if errors.empty?
|
|
126
148
|
raise InvalidOutputError.new(errors:, raw: attrs, source: "Deterministic") if errors.any?
|
|
127
149
|
|
|
128
150
|
schema.build(data, squished:)
|
|
129
151
|
end
|
|
130
152
|
|
|
131
|
-
# Map positional and keyword arguments onto the original method's parameter names.
|
|
153
|
+
# Map positional and keyword arguments onto the original method's parameter names. A positional with no
|
|
154
|
+
# name (a destructuring parameter, or one beyond the declared parameters) is sent as arg0, arg1, ...
|
|
132
155
|
def bind_arguments(args, kwargs)
|
|
133
156
|
positional = args.dup
|
|
134
157
|
bound = {}
|
|
158
|
+
unnamed = 0
|
|
135
159
|
|
|
136
160
|
parameters.each do |type, param|
|
|
137
161
|
case type
|
|
138
162
|
when :req, :opt
|
|
139
|
-
|
|
163
|
+
next if positional.empty?
|
|
164
|
+
|
|
165
|
+
value = positional.shift
|
|
166
|
+
if param
|
|
167
|
+
bound[param] = value
|
|
168
|
+
else
|
|
169
|
+
bound[:"arg#{unnamed}"] = value
|
|
170
|
+
unnamed += 1
|
|
171
|
+
end
|
|
140
172
|
when :rest
|
|
141
173
|
bound[param.nil? || param == :* ? :args : param] = positional.shift(positional.size)
|
|
142
174
|
end
|
|
143
175
|
end
|
|
144
|
-
positional.each_with_index { |value, index| bound[:"arg#{index}"] = value }
|
|
176
|
+
positional.each_with_index { |value, index| bound[:"arg#{unnamed + index}"] = value }
|
|
145
177
|
|
|
146
178
|
bound.merge(kwargs)
|
|
147
179
|
end
|
|
148
180
|
|
|
149
181
|
private
|
|
150
182
|
|
|
183
|
+
# Values JSON can't represent (NaN, Infinity, cycles) are invalid output, not a raw JSON error.
|
|
184
|
+
def jsonify_return(attrs)
|
|
185
|
+
[Schema.jsonify(attrs), []]
|
|
186
|
+
rescue JSON::GeneratorError, JSON::NestingError => e
|
|
187
|
+
[nil, [e.message]]
|
|
188
|
+
end
|
|
189
|
+
|
|
151
190
|
# The implementation's parameters, beneath the prepended wrappers.
|
|
152
191
|
def parameters
|
|
153
192
|
Wrapper.implementation(klass.instance_method(name))&.parameters || []
|
data/lib/squishling/errors.rb
CHANGED
|
@@ -3,20 +3,44 @@
|
|
|
3
3
|
module Squishling
|
|
4
4
|
class Error < StandardError; end
|
|
5
5
|
|
|
6
|
-
# A programming or setup mistake (missing
|
|
6
|
+
# A programming or setup mistake (missing purpose/schema, non-strict schema, unknown model,
|
|
7
7
|
# missing API key). Never retried and never passed to squish_fallback.
|
|
8
8
|
class ConfigurationError < Error; end
|
|
9
9
|
|
|
10
|
-
# Output that didn't match the schema: from the LLM after
|
|
10
|
+
# Output that didn't match the schema (or a squish_validate check): from the LLM after every attempt in the
|
|
11
|
+
# escalation, or from the deterministic path.
|
|
11
12
|
class InvalidOutputError < Error
|
|
12
|
-
|
|
13
|
+
# models: the model ids tried, one per attempt (nil entries mean RubyLLM's default model).
|
|
14
|
+
attr_reader :errors, :raw, :attempts, :models
|
|
13
15
|
|
|
14
|
-
def initialize(errors:, raw: nil, source: "LLM", attempts: nil)
|
|
16
|
+
def initialize(errors:, raw: nil, source: "LLM", attempts: nil, models: nil)
|
|
15
17
|
@errors = errors
|
|
16
18
|
@raw = raw
|
|
17
19
|
@attempts = attempts
|
|
20
|
+
@models = models
|
|
18
21
|
tries = attempts ? " after #{attempts} attempt#{'s' unless attempts == 1}" : ""
|
|
19
|
-
super("#{source} output
|
|
22
|
+
super("#{source} output was invalid#{tries}: #{errors.join('; ')}")
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# A sampling harness's samples (squishsum or ensemble) were valid but didn't agree, and either no judge was declared
|
|
27
|
+
# (verdict nil) or the judge rejected both (verdict :neither, with its reason). candidates holds the two
|
|
28
|
+
# typed results, so a squish_fallback can still return one of them, and raw both as Hashes. models names the
|
|
29
|
+
# model behind each role (sample a, sample b, then the judge if one ran), not one entry per attempt; attempts
|
|
30
|
+
# is nil.
|
|
31
|
+
class DisagreementError < InvalidOutputError
|
|
32
|
+
attr_reader :candidates, :verdict, :reason
|
|
33
|
+
|
|
34
|
+
# detail: is text Squishling wrote itself (a judgment model's choice and probability) and is added to the
|
|
35
|
+
# message. A chat judge's reason is model-written and can quote the input, so it stays out of the message
|
|
36
|
+
# (and the logs); read it from #reason.
|
|
37
|
+
def initialize(candidates:, verdict: nil, reason: nil, detail: nil, models: nil)
|
|
38
|
+
@candidates = candidates
|
|
39
|
+
@verdict = verdict
|
|
40
|
+
@reason = reason
|
|
41
|
+
error = verdict ? "the judge rejected both samples" : "the two samples disagreed"
|
|
42
|
+
error += " (#{detail})" if detail
|
|
43
|
+
super(errors: [error], raw: candidates.map(&:to_h), models:)
|
|
20
44
|
end
|
|
21
45
|
end
|
|
22
46
|
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Squishling
|
|
4
|
+
# How a squished call uses its escalation. Declared as a type, or a Hash with the type and its options:
|
|
5
|
+
# harness: :squishsum
|
|
6
|
+
# harness: { type: :judged_squishsum, judge: "claude-opus-5-5", compare: ->(a, b, **) { a.team == b.team } }
|
|
7
|
+
# :escalation (the default) tries each attempt in order until an output passes. :squishsum asks the first
|
|
8
|
+
# escalation step twice, concurrently, and accepts the output only when both samples agree. :ensemble asks
|
|
9
|
+
# the first and second escalation steps once each instead (a cross-model check). :judged_squishsum and
|
|
10
|
+
# :judged_ensemble hand disagreeing samples to a judge that picks one or rejects both.
|
|
11
|
+
class Harness
|
|
12
|
+
TYPES = %i[escalation squishsum judged_squishsum ensemble judged_ensemble].freeze
|
|
13
|
+
ENSEMBLE_TYPES = %i[ensemble judged_ensemble].freeze
|
|
14
|
+
JUDGED_TYPES = %i[judged_squishsum judged_ensemble].freeze
|
|
15
|
+
KEYS = %i[type judge judge_instructions compare].freeze
|
|
16
|
+
|
|
17
|
+
# A judge is one step: a model name or a Hash with the escalation step keys (except order:), plus
|
|
18
|
+
# type: (:chat, the default, or :judgment for a System One decision model through RubyLLM.judge) and,
|
|
19
|
+
# for :judgment, min_confidence: (the probability the chosen candidate needs).
|
|
20
|
+
JUDGE_TYPES = %i[chat judgment].freeze
|
|
21
|
+
JUDGE_KEYS = %i[type min_confidence].freeze
|
|
22
|
+
# Request keys RubyLLM's judgment protocols own (model and input are already reserved params).
|
|
23
|
+
JUDGMENT_RESERVED_PARAMS = %i[state questions].freeze
|
|
24
|
+
DEFAULT_MIN_CONFIDENCE = 0.8
|
|
25
|
+
|
|
26
|
+
DEFAULT_JUDGE_INSTRUCTIONS = <<~TEXT.strip
|
|
27
|
+
You are an impartial judge. Two independent attempts at the same operation returned different outputs,
|
|
28
|
+
candidate "a" and candidate "b". Both already match the required output format, so judge only their
|
|
29
|
+
content. You are given the operation's purpose and its input. Decide which candidate correctly and
|
|
30
|
+
faithfully carries out the purpose for this input. If both do (for example, they differ only in
|
|
31
|
+
wording), choose either one, preferring "a". Choose "neither" only when both are wrong or you can't tell
|
|
32
|
+
whether either is correct. Never combine them or invent a third answer.
|
|
33
|
+
TEXT
|
|
34
|
+
|
|
35
|
+
attr_reader :type, :judge, :judge_instructions, :compare
|
|
36
|
+
|
|
37
|
+
class << self
|
|
38
|
+
def normalize(value, label)
|
|
39
|
+
case value
|
|
40
|
+
when Symbol, String then new(type: type_for(value, label))
|
|
41
|
+
when Hash then from_hash(value, label)
|
|
42
|
+
else
|
|
43
|
+
raise ConfigurationError, "#{label}: harness must be one of #{TYPES.join(', ')} or a Hash with type:, " \
|
|
44
|
+
"got #{value.inspect}"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def from_hash(hash, label)
|
|
51
|
+
hash = hash.transform_keys(&:to_sym)
|
|
52
|
+
unknown = hash.keys - KEYS
|
|
53
|
+
raise ConfigurationError, "#{label}: unknown harness option(s) #{unknown.join(', ')}" if unknown.any?
|
|
54
|
+
raise ConfigurationError, "#{label}: a harness Hash needs type:" unless hash.key?(:type)
|
|
55
|
+
|
|
56
|
+
type = type_for(hash[:type], label)
|
|
57
|
+
check_options!(type, hash, label)
|
|
58
|
+
new(type:, judge: hash[:judge].nil? ? nil : normalize_judge(hash[:judge], label),
|
|
59
|
+
judge_instructions: hash[:judge_instructions], compare: hash[:compare])
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def type_for(value, label)
|
|
63
|
+
type = value.to_s.to_sym if value.is_a?(Symbol) || value.is_a?(String)
|
|
64
|
+
return type if TYPES.include?(type)
|
|
65
|
+
|
|
66
|
+
raise ConfigurationError, "#{label}: unknown harness #{value.inspect} (use #{TYPES.join(', ')})"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def check_options!(type, hash, label)
|
|
70
|
+
judge_options = %i[judge judge_instructions].select { |key| hash.key?(key) }
|
|
71
|
+
if judge_options.any? && !JUDGED_TYPES.include?(type)
|
|
72
|
+
options = judge_options.map { |key| "#{key}:" }.join(", ")
|
|
73
|
+
raise ConfigurationError,
|
|
74
|
+
"#{label}: #{options} can only be used with the #{JUDGED_TYPES.join(' and ')} harnesses"
|
|
75
|
+
end
|
|
76
|
+
if hash.key?(:compare) && type == :escalation
|
|
77
|
+
raise ConfigurationError, "#{label}: compare: only applies to the sampling harnesses (squishsum and ensemble)"
|
|
78
|
+
end
|
|
79
|
+
unless hash[:compare].nil? || hash[:compare].is_a?(Proc)
|
|
80
|
+
raise ConfigurationError, "#{label}: compare: must be a Proc, got #{hash[:compare].class}"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
instructions = hash[:judge_instructions]
|
|
84
|
+
return if instructions.nil? || instructions.is_a?(Proc)
|
|
85
|
+
return if instructions.is_a?(String) && !instructions.strip.empty?
|
|
86
|
+
|
|
87
|
+
raise ConfigurationError, "#{label}: judge_instructions: must be a non-blank String or a Proc"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# The judge as a normalized escalation step (see ModelPath), plus its type and min_confidence.
|
|
91
|
+
def normalize_judge(judge, label)
|
|
92
|
+
judge_label = "#{label} judge"
|
|
93
|
+
return ModelPath.normalize_step(judge, judge_label).merge(type: :chat) unless judge.is_a?(Hash)
|
|
94
|
+
|
|
95
|
+
judge = judge.transform_keys(&:to_sym)
|
|
96
|
+
raise ConfigurationError, "#{judge_label}: a judge is one step, so it takes no order:" if judge.key?(:order)
|
|
97
|
+
|
|
98
|
+
type = judge.fetch(:type, :chat)
|
|
99
|
+
type = type.to_sym if type.is_a?(String)
|
|
100
|
+
unless JUDGE_TYPES.include?(type)
|
|
101
|
+
raise ConfigurationError,
|
|
102
|
+
"#{judge_label}: type: must be one of #{JUDGE_TYPES.join(', ')}, got #{type.inspect}"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
if type == :chat && judge.key?(:min_confidence)
|
|
106
|
+
raise ConfigurationError, "#{judge_label}: min_confidence: only applies to type: :judgment judges"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
step = ModelPath.normalize_step(judge.except(*JUDGE_KEYS), judge_label).merge(type:)
|
|
110
|
+
return step unless type == :judgment
|
|
111
|
+
|
|
112
|
+
reserved = (step[:params] || {}).keys & JUDGMENT_RESERVED_PARAMS
|
|
113
|
+
if reserved.any?
|
|
114
|
+
raise ConfigurationError, "#{judge_label}: #{reserved.join(', ')} can't be set through params " \
|
|
115
|
+
"(controlled by Squishling/RubyLLM)"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
step.merge(min_confidence: min_confidence(judge, judge_label))
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def min_confidence(judge, label)
|
|
122
|
+
value = judge.fetch(:min_confidence, DEFAULT_MIN_CONFIDENCE)
|
|
123
|
+
return value.to_f if value.is_a?(Numeric) && value.between?(0, 1)
|
|
124
|
+
|
|
125
|
+
raise ConfigurationError, "#{label}: min_confidence: must be a number from 0 to 1, got #{value.inspect}"
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def initialize(type:, judge: nil, judge_instructions: nil, compare: nil)
|
|
130
|
+
@type = type
|
|
131
|
+
@judge = judge
|
|
132
|
+
@judge_instructions = judge_instructions
|
|
133
|
+
@compare = compare
|
|
134
|
+
freeze
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
DEFAULT = new(type: :escalation)
|
|
138
|
+
|
|
139
|
+
# Whether the call asks two samples (squishsum and ensemble harnesses) rather than walking the escalation.
|
|
140
|
+
def squishsum?
|
|
141
|
+
type != :escalation
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Whether the two samples come from the first two escalation steps instead of both from the first.
|
|
145
|
+
def ensemble?
|
|
146
|
+
ENSEMBLE_TYPES.include?(type)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def judged?
|
|
150
|
+
JUDGED_TYPES.include?(type)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|