squishling 0.4.0 → 0.5.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 +75 -0
- data/README.md +25 -9
- data/docs/adapters.md +189 -0
- data/docs/configuration.md +53 -18
- data/docs/failures.md +123 -18
- data/docs/harnesses.md +24 -14
- data/docs/measuring-tokens.md +8 -6
- data/docs/routing.md +5 -0
- data/docs/schemas.md +11 -2
- data/docs/testing.md +183 -0
- data/docs/verifying.md +84 -0
- data/lib/squishling/adapter/error_body.rb +47 -0
- data/lib/squishling/adapter.rb +275 -0
- data/lib/squishling/adapters/openai_compatible.rb +304 -0
- data/lib/squishling/adapters/openai_options.rb +72 -0
- data/lib/squishling/adapters/ruby_llm.rb +193 -0
- data/lib/squishling/budget.rb +71 -0
- data/lib/squishling/class_methods.rb +43 -7
- data/lib/squishling/class_registry.rb +63 -0
- data/lib/squishling/configuration.rb +57 -6
- data/lib/squishling/definition.rb +90 -16
- data/lib/squishling/errors.rb +92 -7
- data/lib/squishling/harness/judged_squishsum.rb +2 -2
- data/lib/squishling/harness/option_types.rb +2 -1
- data/lib/squishling/harness/registry.rb +8 -36
- data/lib/squishling/invoker.rb +21 -9
- data/lib/squishling/judge.rb +16 -3
- data/lib/squishling/llm_client.rb +66 -132
- data/lib/squishling/model_path.rb +60 -13
- data/lib/squishling/named_registry.rb +64 -0
- data/lib/squishling/observer.rb +11 -2
- data/lib/squishling/output_check.rb +13 -9
- data/lib/squishling/params.rb +6 -23
- data/lib/squishling/railtie.rb +10 -0
- data/lib/squishling/result.rb +37 -16
- data/lib/squishling/rotation.rb +5 -0
- data/lib/squishling/rspec.rb +18 -0
- data/lib/squishling/run/messages.rb +40 -3
- data/lib/squishling/run/rejections.rb +43 -0
- data/lib/squishling/run.rb +66 -42
- data/lib/squishling/schema.rb +10 -9
- data/lib/squishling/squawk.rb +33 -3
- data/lib/squishling/tasks/squishling.rake +18 -0
- data/lib/squishling/testing/adapter.rb +194 -0
- data/lib/squishling/testing/checks.rb +39 -0
- data/lib/squishling/testing/direct.rb +41 -0
- data/lib/squishling/testing/errors.rb +18 -0
- data/lib/squishling/testing/fixtures.rb +206 -0
- data/lib/squishling/testing/helpers.rb +71 -0
- data/lib/squishling/testing/minitest.rb +32 -0
- data/lib/squishling/testing/observer.rb +31 -0
- data/lib/squishling/testing/reply.rb +60 -0
- data/lib/squishling/testing/request.rb +75 -0
- data/lib/squishling/testing/rspec.rb +30 -0
- data/lib/squishling/testing/script.rb +189 -0
- data/lib/squishling/testing/state.rb +73 -0
- data/lib/squishling/testing/stubs.rb +112 -0
- data/lib/squishling/testing.rb +126 -0
- data/lib/squishling/verification.rb +39 -0
- data/lib/squishling/verifier.rb +179 -0
- data/lib/squishling/version.rb +1 -1
- data/lib/squishling.rb +57 -4
- metadata +40 -21
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "squishling"
|
|
4
|
+
|
|
5
|
+
require_relative "testing/errors"
|
|
6
|
+
require_relative "testing/reply"
|
|
7
|
+
require_relative "testing/script"
|
|
8
|
+
require_relative "testing/request"
|
|
9
|
+
require_relative "testing/observer"
|
|
10
|
+
require_relative "testing/adapter"
|
|
11
|
+
require_relative "testing/state"
|
|
12
|
+
require_relative "testing/checks"
|
|
13
|
+
require_relative "testing/stubs"
|
|
14
|
+
require_relative "testing/direct"
|
|
15
|
+
require_relative "testing/fixtures"
|
|
16
|
+
require_relative "testing/helpers"
|
|
17
|
+
require_relative "testing/minitest"
|
|
18
|
+
require_relative "testing/rspec"
|
|
19
|
+
|
|
20
|
+
module Squishling
|
|
21
|
+
# Test support, opt-in: `require "squishling/testing"` (it is not loaded with `require "squishling"`, and needs no
|
|
22
|
+
# test framework). A scripted adapter answers in place of a provider and records what was sent to it, plus stubs
|
|
23
|
+
# for the callers of squished methods, schema-driven fixtures, and assertions. See docs/testing.md and the
|
|
24
|
+
# Squishling::Testing::Minitest and ::RSpec mixins.
|
|
25
|
+
module Testing
|
|
26
|
+
class << self
|
|
27
|
+
# Makes the scripted Adapter the one every squished call goes through, and returns it. The block scripts it
|
|
28
|
+
# (see Script); #script adds to it later.
|
|
29
|
+
def install(adapter = Adapter.new)
|
|
30
|
+
yield adapter.script if block_given?
|
|
31
|
+
State.set(:observer, Observer.new(chained_observer(Squishling.config.observer)))
|
|
32
|
+
State.set(:adapter, adapter)
|
|
33
|
+
adapter
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Any request that reaches the provider seam raises ProviderCallError, naming the squished method. Call it in a
|
|
37
|
+
# suite-wide before hook: a test that forgot to script its calls then fails at the call, not far from it.
|
|
38
|
+
def forbid_provider_calls!
|
|
39
|
+
State.set(:adapter, ForbiddenAdapter.new)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Runs the block with provider calls forbidden and returns the calls (the squished methods' labels) that tried
|
|
43
|
+
# to reach the provider, even if the block rescued the error. The adapter is put back afterwards.
|
|
44
|
+
def no_llm_call
|
|
45
|
+
config = Squishling.config
|
|
46
|
+
previous = config.adapter
|
|
47
|
+
guard = ForbiddenAdapter.new
|
|
48
|
+
config.adapter = guard
|
|
49
|
+
begin
|
|
50
|
+
yield
|
|
51
|
+
rescue ProviderCallError
|
|
52
|
+
nil
|
|
53
|
+
ensure
|
|
54
|
+
config.adapter = previous
|
|
55
|
+
end
|
|
56
|
+
guard.attempts
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# See Stubs.result: Klass.call returns a real, schema-validated result of the method's type.
|
|
60
|
+
def stub_result(klass, attrs = nil, method: :call, squished: true, **more, &)
|
|
61
|
+
Stubs.result(klass, Schema.jsonify((attrs || {}).merge(more)), method:, squished:, &)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# See Stubs.error: Klass.call raises the real error class with realistic attributes.
|
|
65
|
+
def stub_error(klass, kind, **, &)
|
|
66
|
+
Stubs.error(klass, kind, **, &)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# The messages squish_validate and the schema reject this reply with. See Direct.
|
|
70
|
+
def validate(klass, reply, **inputs)
|
|
71
|
+
Direct.validate(klass, reply, **inputs)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def fallback_for(klass, error, **inputs)
|
|
75
|
+
Direct.fallback_for(klass, error, **inputs)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def preview(klass, **inputs)
|
|
79
|
+
Direct.preview(klass, **inputs)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def valid_reply_for(source, **overrides)
|
|
83
|
+
Fixtures.valid_reply_for(source, **overrides)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def invalid_reply_for(source, **)
|
|
87
|
+
Fixtures.invalid_reply_for(source, **)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# A recorder to assign as config.squawk (or give to with_squawk): every attempt's output, error and metadata.
|
|
91
|
+
def capture_squawk
|
|
92
|
+
SquawkCapture.new
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Runs the block with the capture as config.squawk, and puts the previous hook back. A squawk set on a class or
|
|
96
|
+
# a method still takes precedence over the configured one.
|
|
97
|
+
def with_squawk(capture = capture_squawk)
|
|
98
|
+
config = Squishling.config
|
|
99
|
+
previous = config.squawk
|
|
100
|
+
config.squawk = capture
|
|
101
|
+
yield capture
|
|
102
|
+
capture
|
|
103
|
+
ensure
|
|
104
|
+
config.squawk = previous
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Puts back what the helpers changed (the adapter and observer) and clears the stubs. The mixins do this
|
|
108
|
+
# after every test; call it yourself with a custom setup.
|
|
109
|
+
def reset!
|
|
110
|
+
Stubs.clear!
|
|
111
|
+
State.restore!
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
private
|
|
115
|
+
|
|
116
|
+
# An observer the app configured itself (an object, or :coolhand) is kept in the chain; the default Coolhand
|
|
117
|
+
# auto-detection, and our own observer from an earlier install, are not.
|
|
118
|
+
def chained_observer(observer)
|
|
119
|
+
return observer.previous if observer.is_a?(Observer)
|
|
120
|
+
return Squishling::Observer.resolve(observer) if observer.is_a?(Symbol)
|
|
121
|
+
|
|
122
|
+
observer if observer.respond_to?(:around)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Squishling
|
|
4
|
+
# What Squishling.verify found: the problems in the squished classes' setup, none when it is sound.
|
|
5
|
+
module Verification
|
|
6
|
+
# One thing wrong. label names where ("Triage#call", or "adapter" for a setup that isn't one class's).
|
|
7
|
+
Problem = Data.define(:label, :message) do
|
|
8
|
+
# The existing messages already start with the label; say it once.
|
|
9
|
+
def to_s
|
|
10
|
+
message.start_with?(label) ? message : "#{label}: #{message}"
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class Report
|
|
15
|
+
attr_reader :problems
|
|
16
|
+
|
|
17
|
+
def initialize(problems)
|
|
18
|
+
@problems = problems.dup.freeze
|
|
19
|
+
freeze
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def ok?
|
|
23
|
+
problems.empty?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def size
|
|
27
|
+
problems.size
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# A lint-style listing, one problem per line.
|
|
31
|
+
def to_s
|
|
32
|
+
return "Squishling: no problems found" if ok?
|
|
33
|
+
|
|
34
|
+
lines = problems.map { |problem| " #{problem}" }
|
|
35
|
+
"Squishling found #{size} #{size == 1 ? 'problem' : 'problems'}:\n#{lines.join("\n")}"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Squishling
|
|
4
|
+
# Checks squished classes without making a request: what can only otherwise fail on a method's first call (its
|
|
5
|
+
# output schema and typed result, its callables' keywords) and the settings the layers resolve to. Collects every
|
|
6
|
+
# problem rather than stopping at the first. See Squishling.verify.
|
|
7
|
+
class Verifier
|
|
8
|
+
# Checks one squished method's Definition. Each check returns messages, or raises what the call would.
|
|
9
|
+
class MethodCheck
|
|
10
|
+
CHECKS = %i[schema declarations harness callables].freeze
|
|
11
|
+
|
|
12
|
+
def initialize(definition)
|
|
13
|
+
@definition = definition
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def problems
|
|
17
|
+
CHECKS.flat_map { |check| run(check) }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def run(check)
|
|
23
|
+
Array(send(check)).map { |message| problem(message) }
|
|
24
|
+
rescue ConfigurationError, ArgumentError => e
|
|
25
|
+
[problem(e.message)]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def problem(message)
|
|
29
|
+
Verification::Problem.new(label: @definition.label, message:)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# The output schema, its typed result (the Data class a call returns), and the JSON Schema itself.
|
|
33
|
+
def schema
|
|
34
|
+
schema = @definition.schema
|
|
35
|
+
messages = []
|
|
36
|
+
messages << "has no output_schema" unless schema
|
|
37
|
+
messages << "has no purpose" unless @definition.purpose_declared?
|
|
38
|
+
return messages unless schema
|
|
39
|
+
|
|
40
|
+
schema.result_class
|
|
41
|
+
messages + JSONSchemer.schema(schema.json_schema).validate_schema.map { |error| error["error"] }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# The escalation, generation params, and squawk this method resolves to across method, class, and config.
|
|
45
|
+
def declarations
|
|
46
|
+
steps = @definition.declared_path.first.reject { |step| step[:model].nil? }
|
|
47
|
+
ModelPath.from_escalation(steps, "#{@definition.label} escalation") if steps.any?
|
|
48
|
+
Params.normalize(@definition.params, "#{@definition.label} params")
|
|
49
|
+
Squawk.validate(@definition.squawk, @definition.label)
|
|
50
|
+
[]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def harness
|
|
54
|
+
harness = @definition.harness
|
|
55
|
+
messages = []
|
|
56
|
+
unless Harness::Registry.fetch(harness.type).equal?(harness.klass)
|
|
57
|
+
messages << "harness #{harness.type.inspect} was unregistered or replaced after it was declared"
|
|
58
|
+
end
|
|
59
|
+
messages << "the judge needs an adapter that supports judgments" if unsupported_judgment?(harness)
|
|
60
|
+
messages
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def unsupported_judgment?(harness)
|
|
64
|
+
judge = harness.options[:judge]
|
|
65
|
+
judge.is_a?(Hash) && judge[:type] == :judgment && !Squishling.config.resolved_adapter.judgments?
|
|
66
|
+
rescue ConfigurationError
|
|
67
|
+
false # no usable adapter: reported once, as the adapter's own problem
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Callables the method's inputs are passed to as keywords: include_when: of each step, and squish_when.
|
|
71
|
+
def callables
|
|
72
|
+
names, open = @definition.input_names
|
|
73
|
+
gates = @definition.declared_path.first.filter_map do |step|
|
|
74
|
+
[step[:include_when], "escalation step #{step[:model].inspect} include_when:"] if step[:include_when]
|
|
75
|
+
end
|
|
76
|
+
gates << [@definition.predicate, "squish_when"] if @definition.predicate
|
|
77
|
+
gates.flat_map { |callable, what| KeywordCheck.messages(callable, names, open).map { "#{what} #{_1}" } }
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Whether a Proc called as instance_exec(**inputs, &proc) accepts the inputs.
|
|
82
|
+
module KeywordCheck
|
|
83
|
+
module_function
|
|
84
|
+
|
|
85
|
+
# Messages for how the proc can't be called with the inputs. open: any keyword may be an input (the method
|
|
86
|
+
# takes **), so none can be known.
|
|
87
|
+
def messages(callable, inputs, open)
|
|
88
|
+
return ["must be a Proc, got #{callable.inspect}"] unless callable.is_a?(Proc)
|
|
89
|
+
return [] if open
|
|
90
|
+
|
|
91
|
+
parameters = callable.parameters
|
|
92
|
+
[missing_keywords(parameters, inputs), unaccepted_keywords(callable, parameters, inputs),
|
|
93
|
+
missing_positionals(callable, parameters, inputs)].compact
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def missing_keywords(parameters, inputs)
|
|
97
|
+
missing = parameters.filter_map { |type, name| name if type == :keyreq } - inputs
|
|
98
|
+
return if missing.empty?
|
|
99
|
+
|
|
100
|
+
"requires #{keywords(missing)}, which #{missing.one? ? 'is not an input' : 'are not inputs'} of the method"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# A lambda with no positional parameters, or a proc that names keywords, raises on a keyword it doesn't take.
|
|
104
|
+
# Other procs and positional lambdas receive the keywords as a Hash, or ignore them.
|
|
105
|
+
def unaccepted_keywords(callable, parameters, inputs)
|
|
106
|
+
types = parameters.map(&:first)
|
|
107
|
+
return if types.include?(:keyrest) || !strict?(callable, types)
|
|
108
|
+
|
|
109
|
+
unaccepted = inputs - parameters.filter_map { |type, name| name if Squawk::KEYWORD_TYPES.include?(type) }
|
|
110
|
+
return if unaccepted.empty?
|
|
111
|
+
|
|
112
|
+
"doesn't accept #{keywords(unaccepted)}, which the method takes as input when a caller passes it (add **)"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# A lambda's required positional parameters get the keywords as one Hash, or nothing when there are none or
|
|
116
|
+
# the lambda names keywords itself.
|
|
117
|
+
def missing_positionals(callable, parameters, inputs)
|
|
118
|
+
return unless callable.lambda?
|
|
119
|
+
|
|
120
|
+
types = parameters.map(&:first)
|
|
121
|
+
required = types.count(:req)
|
|
122
|
+
given = types.intersect?(%i[key keyreq keyrest]) || inputs.empty? ? 0 : 1
|
|
123
|
+
return unless required > given
|
|
124
|
+
|
|
125
|
+
"takes #{required} positional argument(s), but is called with keywords only"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def strict?(callable, types)
|
|
129
|
+
keywords = types.intersect?(Squawk::KEYWORD_TYPES)
|
|
130
|
+
callable.lambda? ? keywords || !types.intersect?(%i[req opt rest]) : keywords
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def keywords(names)
|
|
134
|
+
names.map { |name| "#{name}:" }.join(", ")
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def initialize(classes)
|
|
139
|
+
classes.each { |klass| check_squished!(klass) }
|
|
140
|
+
@classes = classes
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def call
|
|
144
|
+
Verification::Report.new(adapter_problems + @classes.flat_map { |klass| class_problems(klass) })
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
private
|
|
148
|
+
|
|
149
|
+
def check_squished!(klass)
|
|
150
|
+
return if klass.is_a?(Class) && klass.include?(Squishling)
|
|
151
|
+
|
|
152
|
+
raise ConfigurationError, "#{klass.inspect} doesn't include Squishling"
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# The configured adapter, or RubyLLM's, must be usable.
|
|
156
|
+
def adapter_problems
|
|
157
|
+
Squishling.config.resolved_adapter
|
|
158
|
+
[]
|
|
159
|
+
rescue ConfigurationError => e
|
|
160
|
+
[Verification::Problem.new(label: "adapter", message: e.message)]
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def class_problems(klass)
|
|
164
|
+
klass.squished_methods.keys.flat_map do |name|
|
|
165
|
+
definition = klass.squishling_definition(name)
|
|
166
|
+
next [] if unused_default?(definition)
|
|
167
|
+
|
|
168
|
+
MethodCheck.new(definition).problems
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Every class has the default method (call) squished, whether or not it uses it.
|
|
173
|
+
def unused_default?(definition)
|
|
174
|
+
definition.name == ClassMethods::DEFAULT_METHOD && !definition.implemented? && definition.schema.nil?
|
|
175
|
+
rescue ConfigurationError, ArgumentError
|
|
176
|
+
false
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
data/lib/squishling/version.rb
CHANGED
data/lib/squishling.rb
CHANGED
|
@@ -2,13 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "json_schemer"
|
|
5
|
-
require "ruby_llm"
|
|
6
5
|
require "schematist"
|
|
7
6
|
|
|
8
7
|
require_relative "squishling/version"
|
|
9
8
|
require_relative "squishling/errors"
|
|
10
9
|
require_relative "squishling/configuration"
|
|
11
10
|
require_relative "squishling/params"
|
|
11
|
+
require_relative "squishling/named_registry"
|
|
12
|
+
require_relative "squishling/adapter/error_body"
|
|
13
|
+
require_relative "squishling/adapter"
|
|
14
|
+
require_relative "squishling/adapters/ruby_llm"
|
|
15
|
+
require_relative "squishling/adapters/openai_options"
|
|
16
|
+
require_relative "squishling/adapters/openai_compatible"
|
|
12
17
|
require_relative "squishling/squawk"
|
|
13
18
|
require_relative "squishling/observer"
|
|
14
19
|
require_relative "squishling/observers/coolhand"
|
|
@@ -26,9 +31,11 @@ require_relative "squishling/schema"
|
|
|
26
31
|
require_relative "squishling/source"
|
|
27
32
|
require_relative "squishling/appendices"
|
|
28
33
|
require_relative "squishling/definition"
|
|
34
|
+
require_relative "squishling/budget"
|
|
29
35
|
require_relative "squishling/llm_client"
|
|
30
36
|
require_relative "squishling/output_check"
|
|
31
37
|
require_relative "squishling/run/messages"
|
|
38
|
+
require_relative "squishling/run/rejections"
|
|
32
39
|
require_relative "squishling/run"
|
|
33
40
|
require_relative "squishling/invoker"
|
|
34
41
|
require_relative "squishling/judge"
|
|
@@ -36,6 +43,10 @@ require_relative "squishling/router"
|
|
|
36
43
|
require_relative "squishling/wrapper"
|
|
37
44
|
require_relative "squishling/class_methods"
|
|
38
45
|
require_relative "squishling/collisions"
|
|
46
|
+
require_relative "squishling/class_registry"
|
|
47
|
+
require_relative "squishling/verification"
|
|
48
|
+
require_relative "squishling/verifier"
|
|
49
|
+
require_relative "squishling/railtie" if defined?(Rails::Railtie)
|
|
39
50
|
|
|
40
51
|
# Include Squishling in a class to make it elastic: its squished methods either run their
|
|
41
52
|
# Ruby implementation or send their inputs through an LLM and return schema-validated results.
|
|
@@ -74,6 +85,47 @@ module Squishling
|
|
|
74
85
|
Harness.types
|
|
75
86
|
end
|
|
76
87
|
|
|
88
|
+
# Registers a custom adapter class (see Adapter::Base) under a name, usable as config.adapter = :name, or
|
|
89
|
+
# config.adapter = { type: :name, option: value } for an adapter whose initialize takes options. To use an adapter
|
|
90
|
+
# without registering it, assign an instance: config.adapter = MyAdapter.new. Raises ConfigurationError for a name
|
|
91
|
+
# that is invalid or already taken, including the built-in :ruby_llm and :openai_compatible.
|
|
92
|
+
def register_adapter(name, klass)
|
|
93
|
+
Adapter::Registry.register(name, klass)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# The names of the adapters that can be configured: the built-ins, then those registered.
|
|
97
|
+
def adapters
|
|
98
|
+
Adapter::Registry.names
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Forgets the registered adapters (the built-ins stay), e.g. between specs.
|
|
102
|
+
def reset_adapters!
|
|
103
|
+
Adapter::Registry.reset!
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Every class that includes Squishling, and each subclass of one, that is loaded and hasn't been replaced by a
|
|
107
|
+
# reload. Classes are held weakly, so listing them doesn't keep reloaded ones alive. In Rails, eager load first
|
|
108
|
+
# (Rails.application.eager_load!) to include classes not yet autoloaded.
|
|
109
|
+
def squished_classes
|
|
110
|
+
ClassRegistry.classes
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Checks squished classes without making a request: each method's output schema and typed result, its resolved
|
|
114
|
+
# escalation, params, squawk and harness, and the keywords its include_when: and squish_when procs take. Checks
|
|
115
|
+
# the given classes, or every squished_classes when none are given. Returns a Verification::Report listing every
|
|
116
|
+
# problem found (ok? when none).
|
|
117
|
+
def verify(*classes)
|
|
118
|
+
Verifier.new(classes.empty? ? squished_classes : classes.flatten).call
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Like verify, but raises a VerificationError (a ConfigurationError) naming every problem. For CI and boot checks.
|
|
122
|
+
def verify!(*classes)
|
|
123
|
+
report = verify(*classes)
|
|
124
|
+
raise VerificationError, report unless report.ok?
|
|
125
|
+
|
|
126
|
+
report
|
|
127
|
+
end
|
|
128
|
+
|
|
77
129
|
def included(base)
|
|
78
130
|
raise ConfigurationError, "Squishling can only be included in a class" unless base.is_a?(Class)
|
|
79
131
|
|
|
@@ -108,10 +160,11 @@ module Squishling
|
|
|
108
160
|
# it); return it from the method. The overrides apply to this call only: append_to_purpose adds to (or,
|
|
109
161
|
# with false, replaces) the declared sections, context is sent alongside the declared squish_context,
|
|
110
162
|
# model/escalation (one or the other)/provider/purpose/harness replace the declared ones, and params merge
|
|
111
|
-
# key by key over them.
|
|
163
|
+
# key by key over them. max_total_output_tokens/max_total_input_tokens cap what the call may spend across all its
|
|
164
|
+
# requests (false lifts a declared cap).
|
|
112
165
|
def squish!(append_to_purpose: nil, context: nil, purpose: nil, model: nil, escalation: nil, provider: nil,
|
|
113
|
-
params: nil, harness: nil)
|
|
166
|
+
params: nil, harness: nil, max_total_output_tokens: nil, max_total_input_tokens: nil)
|
|
114
167
|
Router.hand_off(self, { append_to_purpose:, context:, purpose:, model:, escalation:, provider:, params:,
|
|
115
|
-
harness: })
|
|
168
|
+
harness:, max_total_output_tokens:, max_total_input_tokens: })
|
|
116
169
|
end
|
|
117
170
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: squishling
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Michael Carroll
|
|
@@ -23,20 +23,6 @@ dependencies:
|
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '2.0'
|
|
26
|
-
- !ruby/object:Gem::Dependency
|
|
27
|
-
name: ruby_llm
|
|
28
|
-
requirement: !ruby/object:Gem::Requirement
|
|
29
|
-
requirements:
|
|
30
|
-
- - "~>"
|
|
31
|
-
- !ruby/object:Gem::Version
|
|
32
|
-
version: '2.1'
|
|
33
|
-
type: :runtime
|
|
34
|
-
prerelease: false
|
|
35
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
-
requirements:
|
|
37
|
-
- - "~>"
|
|
38
|
-
- !ruby/object:Gem::Version
|
|
39
|
-
version: '2.1'
|
|
40
26
|
- !ruby/object:Gem::Dependency
|
|
41
27
|
name: schematist
|
|
42
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -53,12 +39,13 @@ dependencies:
|
|
|
53
39
|
version: '1.1'
|
|
54
40
|
description: 'Squishling makes Ruby classes elastic. Add a squishling to a class and
|
|
55
41
|
it can send inputs straight to an LLM (OpenAI, Anthropic Claude, Google Gemini,
|
|
56
|
-
or any provider RubyLLM supports
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
the
|
|
61
|
-
|
|
42
|
+
or any provider RubyLLM supports, or any OpenAI-compatible endpoint, or your own
|
|
43
|
+
adapter) and return strict-schema-validated, typed results: the same type your Ruby
|
|
44
|
+
code returns. Use it to gracefully maintain fickle integrations, build interfaces
|
|
45
|
+
for unknown data formats, and rescue production errors by handing the failing call
|
|
46
|
+
to the LLM with the source class as context. Write Ruby only for the paths where
|
|
47
|
+
token costs justify maintaining it. Per-input routing, validated retries, and fallbacks
|
|
48
|
+
for your LLM calls included.'
|
|
62
49
|
email:
|
|
63
50
|
- mc@coolhandlabs.com
|
|
64
51
|
executables: []
|
|
@@ -70,6 +57,7 @@ files:
|
|
|
70
57
|
- README.md
|
|
71
58
|
- Rakefile
|
|
72
59
|
- SECURITY.md
|
|
60
|
+
- docs/adapters.md
|
|
73
61
|
- docs/configuration.md
|
|
74
62
|
- docs/coolhand.md
|
|
75
63
|
- docs/failures.md
|
|
@@ -79,9 +67,18 @@ files:
|
|
|
79
67
|
- docs/observers.md
|
|
80
68
|
- docs/routing.md
|
|
81
69
|
- docs/schemas.md
|
|
70
|
+
- docs/testing.md
|
|
71
|
+
- docs/verifying.md
|
|
82
72
|
- lib/squishling.rb
|
|
73
|
+
- lib/squishling/adapter.rb
|
|
74
|
+
- lib/squishling/adapter/error_body.rb
|
|
75
|
+
- lib/squishling/adapters/openai_compatible.rb
|
|
76
|
+
- lib/squishling/adapters/openai_options.rb
|
|
77
|
+
- lib/squishling/adapters/ruby_llm.rb
|
|
83
78
|
- lib/squishling/appendices.rb
|
|
79
|
+
- lib/squishling/budget.rb
|
|
84
80
|
- lib/squishling/class_methods.rb
|
|
81
|
+
- lib/squishling/class_registry.rb
|
|
85
82
|
- lib/squishling/collisions.rb
|
|
86
83
|
- lib/squishling/configuration.rb
|
|
87
84
|
- lib/squishling/definition.rb
|
|
@@ -97,18 +94,40 @@ files:
|
|
|
97
94
|
- lib/squishling/judge.rb
|
|
98
95
|
- lib/squishling/llm_client.rb
|
|
99
96
|
- lib/squishling/model_path.rb
|
|
97
|
+
- lib/squishling/named_registry.rb
|
|
100
98
|
- lib/squishling/observer.rb
|
|
101
99
|
- lib/squishling/observers/coolhand.rb
|
|
102
100
|
- lib/squishling/output_check.rb
|
|
103
101
|
- lib/squishling/params.rb
|
|
102
|
+
- lib/squishling/railtie.rb
|
|
104
103
|
- lib/squishling/result.rb
|
|
105
104
|
- lib/squishling/rotation.rb
|
|
106
105
|
- lib/squishling/router.rb
|
|
106
|
+
- lib/squishling/rspec.rb
|
|
107
107
|
- lib/squishling/run.rb
|
|
108
108
|
- lib/squishling/run/messages.rb
|
|
109
|
+
- lib/squishling/run/rejections.rb
|
|
109
110
|
- lib/squishling/schema.rb
|
|
110
111
|
- lib/squishling/source.rb
|
|
111
112
|
- lib/squishling/squawk.rb
|
|
113
|
+
- lib/squishling/tasks/squishling.rake
|
|
114
|
+
- lib/squishling/testing.rb
|
|
115
|
+
- lib/squishling/testing/adapter.rb
|
|
116
|
+
- lib/squishling/testing/checks.rb
|
|
117
|
+
- lib/squishling/testing/direct.rb
|
|
118
|
+
- lib/squishling/testing/errors.rb
|
|
119
|
+
- lib/squishling/testing/fixtures.rb
|
|
120
|
+
- lib/squishling/testing/helpers.rb
|
|
121
|
+
- lib/squishling/testing/minitest.rb
|
|
122
|
+
- lib/squishling/testing/observer.rb
|
|
123
|
+
- lib/squishling/testing/reply.rb
|
|
124
|
+
- lib/squishling/testing/request.rb
|
|
125
|
+
- lib/squishling/testing/rspec.rb
|
|
126
|
+
- lib/squishling/testing/script.rb
|
|
127
|
+
- lib/squishling/testing/state.rb
|
|
128
|
+
- lib/squishling/testing/stubs.rb
|
|
129
|
+
- lib/squishling/verification.rb
|
|
130
|
+
- lib/squishling/verifier.rb
|
|
112
131
|
- lib/squishling/version.rb
|
|
113
132
|
- lib/squishling/wrapper.rb
|
|
114
133
|
homepage: https://github.com/Coolhand-Labs/squishling
|