hunch 0.3.0 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 385258ecd737c836bd9b677e023cbc5ffc653770ee046aba43aeeac3225c8cc2
4
- data.tar.gz: 335bd7011b904736715d5490ca56645ce9a012faf7aff0fd485929d8ec5e5cf3
3
+ metadata.gz: e7f094c2b401f7331d45ca78228ceec41c45aacd76104ca3af0a9bf7708362f4
4
+ data.tar.gz: 57450f766b733602126cf65e128aa98245926a7b73679c5684f9aacd6e824d47
5
5
  SHA512:
6
- metadata.gz: 05473ca9fbca95957fecd251d2dc53ee239f5e12b4b579846dbccf20c92f78a36ee0d4697b5a410ef89ddf406829e85bcead5aa0361307e32a3eb97984f9fd98
7
- data.tar.gz: 9e50657e98f6e290fe96f08e161416ead241aae674dad5a9862c38eaac60232fc3fb314f60ae0cbee342f3d51b71fa2e5d8a0ce7cfb2eaaafa9411cb3fb08ca0
6
+ metadata.gz: bbef66e803a4362cc7e72f52096d01f412060ddb2d68fb7fd8b79172de9acb335ddd724a0b0d566b8c3a497d36779ff9c1f5fb8e31c0d17680d6038b50f69293
7
+ data.tar.gz: 99ccaed307b8501dd8ea9ecc1aab28dcaf6d86accb24e79a2107caf0e3971dfd354c17ea5a157211a738b1fae74665a8cc98cb95f6fa0049b2068ba3e7f793c3
data/README.md CHANGED
@@ -2,9 +2,8 @@
2
2
 
3
3
  Probabilistic control flow for Ruby.
4
4
 
5
- Ruby gives you `if`, `case`, and `<=>` for facts. Hunch gives you the same
6
- three moves for judgment calls. Every question is a conditional probability —
7
- _how likely is this, given that_ — and the API reads that way:
5
+ Hunch lets you ask questions about text or application data and use the
6
+ answers in your Ruby code.
8
7
 
9
8
  ```ruby
10
9
  if Hunch.almost_certainly?("this order is fraudulent", given: order.attributes)
@@ -12,10 +11,11 @@ if Hunch.almost_certainly?("this order is fraudulent", given: order.attributes)
12
11
  end
13
12
  ```
14
13
 
15
- The English is the configuration. No prompts, no parsing, no tuning DSL.
16
- Answers come from [TypeSafe's Jev](https://typesafe.ai), a System One model:
17
- a single fast parallel pass that returns typed, calibrated probabilities
18
- instead of text — fast and cheap enough to sit inside a request cycle.
14
+ You can get a probability, choose between options, or rate something on a
15
+ scale. Questions are written in plain English; answers come back as Ruby values.
16
+
17
+ Hunch asks a System One model, such as [TypeSafe's Jev](https://typesafe.ai),
18
+ which answers with calibrated probabilities rather than text.
19
19
 
20
20
  ## Installation
21
21
 
@@ -23,21 +23,29 @@ instead of text — fast and cheap enough to sit inside a request cycle.
23
23
  gem "hunch"
24
24
  ```
25
25
 
26
+ Point Hunch at a System One endpoint. Jev is available through
27
+ [OpenRouter](https://openrouter.ai/typesafe):
28
+
26
29
  ```ruby
30
+ # config/initializers/hunch.rb
27
31
  Hunch.configure do |config|
28
- config.api_key = ENV["TYPESAFE_API_KEY"]
32
+ config.backend = Hunch::Backends::SystemOne.new(
33
+ url: "https://openrouter.ai/api/alpha/decisions",
34
+ api_key: ENV["OPENROUTER_API_KEY"],
35
+ model: "typesafe/jev-1.13"
36
+ )
29
37
  end
30
38
  ```
31
39
 
32
- ## The three primitives
40
+ ## Usage
33
41
 
34
- **`chance`** answers _whether_, as a probability:
42
+ **`chance`** returns a probability between 0 and 1:
35
43
 
36
44
  ```ruby
37
45
  Hunch.chance("written by a real human, not spam", given: bio) # => 0.87
38
46
  ```
39
47
 
40
- Named levels collapse it into predicates:
48
+ Use a named threshold when you want a boolean:
41
49
 
42
50
  ```ruby
43
51
  Hunch.possibly?("fraudulent", given: order) # chance >= 0.25
@@ -45,14 +53,14 @@ Hunch.likely?("fraudulent", given: order) # chance >= 0.5
45
53
  Hunch.probably?("fraudulent", given: order) # chance >= 0.75
46
54
  Hunch.almost_certainly?("fraudulent", given: order) # chance >= 0.93
47
55
 
48
- Hunch.configure { |c| c.levels[:paranoid] = 0.99 }
49
- Hunch.paranoid?("fraudulent", given: order) # chance >= 0.99
56
+ Hunch.configure { |c| c.levels[:definitely] = 0.99 }
57
+ Hunch.definitely?("fraudulent", given: order) # chance >= 0.99
50
58
  ```
51
59
 
52
60
  Configure built-in thresholds with the matching level names, for example
53
61
  `Hunch.configure { |c| c.levels[:probably] = 0.8 }`.
54
62
 
55
- **`pick`** answers _which_:
63
+ **`pick`** chooses one of the options you provide:
56
64
 
57
65
  ```ruby
58
66
  Hunch.pick(:ham, :spam, given: email) # => :spam
@@ -60,7 +68,7 @@ Hunch.pick(urgent: "needs a reply today", routine: "can wait",
60
68
  given: ticket) # => :routine
61
69
  ```
62
70
 
63
- **`rate`** answers _how much_, on an ordered scale:
71
+ **`rate`** returns a rating with a position on an ordered scale:
64
72
 
65
73
  ```ruby
66
74
  mood = Hunch.rate(:calm, :frustrated, :livid, given: email)
@@ -69,14 +77,16 @@ mood.position # => 1.4
69
77
  mood >= :livid # => false
70
78
  ```
71
79
 
72
- If shuffling the options wouldn't change their meaning, use `pick`; if they
73
- form a ladder, use `rate`. Options are bare symbols or `symbol: "description"`,
74
- mixed freely, plus an optional `question:` when the options alone don't carry
75
- it. The keywords `given` and `question` are reserved.
80
+ Use `pick` for categories such as billing, support, and sales. Use `rate`
81
+ for ordered levels such as low, medium, and high.
82
+
83
+ Both accept symbols or `symbol: "description"` pairs, or a mix of the two.
84
+ Add `question:` if the options need more context. `given:` supplies the data
85
+ to evaluate. These two keywords are reserved and can't be used as option names.
76
86
 
77
87
  ## Batching
78
88
 
79
- Several questions about one piece of state cost one API call:
89
+ Use `Hunch.decide` to ask several questions about the same data in one API call:
80
90
 
81
91
  ```ruby
82
92
  result = Hunch.decide(given: mail.raw_source) do |q|
@@ -94,13 +104,11 @@ result.mood.level # => :livid
94
104
 
95
105
  ## In a Rails app
96
106
 
97
- Everything below is lifted from [`example/`](example), a Rails app in this
98
- repo — its test suite covers each snippet, and all of them have been run
99
- against the live model.
107
+ The [`example/`](example) Rails app contains these examples and their tests.
100
108
 
101
109
  ### Validations
102
110
 
103
- A validation method is just an `if`:
111
+ Check a display name and bio during validation:
104
112
 
105
113
  ```ruby
106
114
  class Signup < ApplicationRecord
@@ -136,13 +144,13 @@ signup.valid? # => false
136
144
  signup.errors[:bio] # => ["reads like spam"]
137
145
  ```
138
146
 
139
- The `rescue nil` is a deliberate policy: if the API is unreachable at save
140
- time, the record saves anyway. Fail closed instead where it matters more,
141
- like a spam gate.
147
+ These validations rescue `Hunch::APIError`, so an API failure adds no
148
+ validation error. Other validations, including the email presence check,
149
+ still apply.
142
150
 
143
151
  ### Inbound email
144
152
 
145
- ActionMailbox routing without the regex graveyard:
153
+ Route incoming email by its contents:
146
154
 
147
155
  ```ruby
148
156
  class SortingMailbox < ApplicationMailbox
@@ -162,7 +170,7 @@ end
162
170
 
163
171
  ### Error triage
164
172
 
165
- Replace the hand-maintained ignore-list with one judgment:
173
+ Choose whether to ignore an error, send a notification, or page someone:
166
174
 
167
175
  ```ruby
168
176
  class ErrorTriage
@@ -189,7 +197,7 @@ end
189
197
 
190
198
  ### Job retries
191
199
 
192
- Exception classes don't tell you whether a failure is transient. Ask:
200
+ Use the error message and attempt count to decide whether to retry:
193
201
 
194
202
  ```ruby
195
203
  class WebhookDeliveryJob < ApplicationJob
@@ -208,12 +216,9 @@ class WebhookDeliveryJob < ApplicationJob
208
216
  end
209
217
  ```
210
218
 
211
- A `503 upstream timeout` retries; a `404 endpoint not found` doesn't.
212
-
213
219
  ### Enum coercion
214
220
 
215
- Messy import data, typed by construction — `pick` can only return one of
216
- your enum's values:
221
+ Map imported text to an existing enum value:
217
222
 
218
223
  ```ruby
219
224
  class Order < ApplicationRecord
@@ -249,21 +254,26 @@ end
249
254
 
250
255
  ## Testing
251
256
 
252
- The stub backend is just another backend:
257
+ Use the stub backend to supply answers without making API calls:
253
258
 
254
259
  ```ruby
255
- Hunch.backend = Hunch::Backends::Stub.new(fraud: 0.95, team: :billing, mood: :calm)
260
+ Hunch.backend = Hunch::Backends::Stub.new({ fraud: 0.95, team: :billing, mood: :calm })
256
261
  ```
257
262
 
258
- Stub values are keyed by question key (`:answer` for the single-shot
259
- methods) and follow the question type: a probability or boolean for chance
260
- and its predicates, a symbol or probabilities hash for `pick`, a level
261
- symbol or position for `rate`. Unstubbed questions raise unless you pass
262
- `default:`. The stub records `calls` for assertions:
263
+ Use the question's key to supply its answer, or `:answer` for calls outside
264
+ a `Hunch.decide` block. Stub values depend on the method:
265
+
266
+ - `chance` and its predicates: a probability or boolean.
267
+ - `pick`: a symbol or a hash of probabilities.
268
+ - `rate`: a level symbol or numeric position.
269
+
270
+ Missing answers raise unless you pass a fallback, as in
271
+ `Hunch::Backends::Stub.new({}, fallback: 0.5)`. The stub records each call
272
+ in `calls` for assertions.
263
273
 
264
274
  ```ruby
265
275
  test "spam is dropped without a ticket" do
266
- Hunch.backend = Hunch::Backends::Stub.new(answer: :spam)
276
+ Hunch.backend = Hunch::Backends::Stub.new({ answer: :spam })
267
277
  assert_no_difference -> { Ticket.count } do
268
278
  receive_inbound_email_from_mail(subject: "You have WON", body: "claim your prize")
269
279
  end
@@ -274,38 +284,32 @@ end
274
284
 
275
285
  ```ruby
276
286
  Hunch.configure do |config|
277
- config.api_key = "..." # default: ENV["TYPESAFE_API_KEY"]
278
- config.model = "jev-latest"
279
- config.url = "https://api.typesafe.ai/v1/systemone"
280
- config.timeout = 5
281
- config.open_timeout = 2
282
- config.max_retries = 2 # 429/5xx/timeouts, with backoff, honours Retry-After
283
- config.levels[:paranoid] = 0.99
287
+ config.backend = Hunch::Backends::SystemOne.new(
288
+ url: "https://openrouter.ai/api/alpha/decisions",
289
+ api_key: ENV["OPENROUTER_API_KEY"], # a missing key raises when you ask
290
+ model: "typesafe/jev-1.13",
291
+ timeout: 5, # seconds to read the response
292
+ open_timeout: 2, # seconds to connect
293
+ max_retries: 2 # 429/5xx/timeouts, with backoff, honours Retry-After
294
+ )
295
+ config.levels[:definitely] = 0.99
284
296
  end
285
297
  ```
286
298
 
287
- ### Via OpenRouter
299
+ ## Errors
288
300
 
289
- Jev speaks the same wire format through
290
- [OpenRouter's Decisions endpoint](https://openrouter.ai/typesafe), so an
291
- OpenRouter key works today without the TypeSafe waitlist:
292
-
293
- ```ruby
294
- Hunch.configure do |config|
295
- config.api_key = ENV["OPENROUTER_API_KEY"]
296
- config.url = "https://openrouter.ai/api/alpha/decisions"
297
- config.model = "typesafe/jev-1.13"
298
- end
299
- ```
301
+ `Hunch::APIError` covers failures worth working around: timeouts, lost
302
+ connections, rate limits, server errors, and answers the backend got wrong
303
+ (`Hunch::InvalidAnswerError`). `AuthenticationError`, `ValidationError`, and
304
+ `ConfigurationError` mean your setup needs fixing, so they are not
305
+ `APIError`s.
300
306
 
301
- ## Honesty about backends
307
+ ## Backends
302
308
 
303
- The interface is uniform; the guarantees are not. Jev's probabilities are
304
- calibrated, its answers are typed by construction, and it responds in
305
- milliseconds. A future LLM backend can implement the same three primitives,
306
- but its confidences are estimates, not calibrated probabilities, and it is
307
- orders of magnitude slower and more expensive. Same interface, different
308
- guarantees — choose accordingly.
309
+ `Hunch::Backends::SystemOne` talks to any endpoint that speaks the System One
310
+ decisions protocol of noul, choice, and score questions. `Hunch::Backends::Stub`
311
+ is for [testing](#testing). A backend is any object with a
312
+ `decide(state:, questions:)` method that returns `{ "answers" => ... }`.
309
313
 
310
314
  ## License
311
315
 
@@ -3,13 +3,13 @@ module Hunch
3
3
  class Stub
4
4
  attr_reader :calls
5
5
 
6
- def initialize(default: nil, **answers)
6
+ def initialize(answers = {}, fallback: nil)
7
7
  @answers = answers
8
- @default = default
8
+ @fallback = fallback
9
9
  @calls = []
10
10
  end
11
11
 
12
- def decide(state:, questions:, model: nil)
12
+ def decide(state:, questions:)
13
13
  @calls << { state:, questions: }
14
14
  answers = questions.to_h do |key, question|
15
15
  [key.to_s, answer_for(key, question)]
@@ -20,10 +20,10 @@ module Hunch
20
20
  private
21
21
 
22
22
  def answer_for(key, question)
23
- value = @answers.fetch(key, @default)
23
+ value = @answers.fetch(key, @fallback)
24
24
  if value.nil?
25
25
  raise MissingStubAnswer,
26
- "no stubbed answer for #{key.inspect}: Stub.new(#{key}: ...) or pass default:"
26
+ "no stubbed answer for #{key.inspect}: Stub.new({ #{key}: ... }) or pass fallback:"
27
27
  end
28
28
 
29
29
  case question.type
@@ -4,17 +4,24 @@ require "uri"
4
4
 
5
5
  module Hunch
6
6
  module Backends
7
- class Jev
8
- def initialize(config = Hunch.configuration, transport: nil, sleeper: nil)
9
- @config = config
7
+ class SystemOne
8
+ def initialize(url:, api_key:, model:, timeout: 5, open_timeout: 2, max_retries: 2, transport: nil, sleeper: nil)
9
+ @url = url
10
+ @api_key = api_key
11
+ @model = model
12
+ @timeout = timeout
13
+ @open_timeout = open_timeout
14
+ @max_retries = max_retries
10
15
  @transport = transport || method(:http_post)
11
16
  @sleeper = sleeper || ->(seconds) { sleep(seconds) }
12
17
  end
13
18
 
14
- def decide(state:, questions:, model: nil)
19
+ def decide(state:, questions:)
20
+ raise ConfigurationError, "no API key for #{@url}" if @api_key.nil? || @api_key.empty?
21
+
15
22
  payload = {
16
23
  "state" => state,
17
- "model" => model || @config.model,
24
+ "model" => @model,
18
25
  "questions" => questions.to_h { |key, question| [key.to_s, question.payload] }
19
26
  }
20
27
  with_retries { handle(*@transport.call(payload)) }
@@ -28,7 +35,7 @@ module Hunch
28
35
  attempts += 1
29
36
  yield
30
37
  rescue RateLimitError, OverloadedError, ServerError, TimeoutError, ConnectionError => e
31
- raise if attempts > @config.max_retries
38
+ raise if attempts > @max_retries
32
39
 
33
40
  @sleeper.call(e.respond_to?(:retry_after) && e.retry_after || backoff(attempts))
34
41
  retry
@@ -41,7 +48,7 @@ module Hunch
41
48
 
42
49
  def handle(status, headers, body)
43
50
  case status
44
- when 200..299 then JSON.parse(body)
51
+ when 200..299 then parse(body)
45
52
  when 401 then raise AuthenticationError, error_message(body, "missing or invalid API key")
46
53
  when 422 then raise ValidationError, error_message(body, "invalid request")
47
54
  when 429 then raise RateLimitError.new(error_message(body, "rate limited"), retry_after: retry_after(headers))
@@ -51,6 +58,12 @@ module Hunch
51
58
  end
52
59
  end
53
60
 
61
+ def parse(body)
62
+ JSON.parse(body)
63
+ rescue JSON::ParserError, TypeError
64
+ raise APIError, "response was not valid JSON"
65
+ end
66
+
54
67
  def error_message(body, fallback)
55
68
  parsed = JSON.parse(body)
56
69
  error = parsed["error"] || parsed["message"] || fallback
@@ -65,9 +78,9 @@ module Hunch
65
78
  end
66
79
 
67
80
  def http_post(payload)
68
- uri = URI(@config.url)
81
+ uri = URI(@url)
69
82
  request = Net::HTTP::Post.new(uri)
70
- request["Authorization"] = "Bearer #{api_key!}"
83
+ request["Authorization"] = "Bearer #{@api_key}"
71
84
  request["Content-Type"] = "application/json"
72
85
  request["User-Agent"] = "hunch-ruby/#{VERSION}"
73
86
  request.body = JSON.generate(payload)
@@ -75,8 +88,8 @@ module Hunch
75
88
  response = Net::HTTP.start(
76
89
  uri.host, uri.port,
77
90
  use_ssl: uri.scheme == "https",
78
- open_timeout: @config.open_timeout,
79
- read_timeout: @config.timeout
91
+ open_timeout: @open_timeout,
92
+ read_timeout: @timeout
80
93
  ) { |http| http.request(request) }
81
94
 
82
95
  [response.code.to_i, response.each_header.to_h, response.body]
@@ -85,11 +98,6 @@ module Hunch
85
98
  rescue SocketError, SystemCallError, EOFError, OpenSSL::SSL::SSLError => e
86
99
  raise ConnectionError, e.message
87
100
  end
88
-
89
- def api_key!
90
- @config.api_key or raise ConfigurationError,
91
- "no API key: set TYPESAFE_API_KEY or Hunch.configure { |c| c.api_key = ... }"
92
- end
93
101
  end
94
102
  end
95
103
  end
@@ -2,16 +2,10 @@ module Hunch
2
2
  class Configuration
3
3
  LEVELS = { possibly: 0.25, likely: 0.5, probably: 0.75, almost_certainly: 0.93 }.freeze
4
4
 
5
- attr_accessor :api_key, :model, :url, :timeout, :open_timeout, :max_retries, :levels
5
+ attr_accessor :levels
6
6
  attr_writer :backend
7
7
 
8
8
  def initialize
9
- @api_key = ENV["TYPESAFE_API_KEY"]
10
- @model = "jev-latest"
11
- @url = "https://api.typesafe.ai/v1/systemone"
12
- @timeout = 5
13
- @open_timeout = 2
14
- @max_retries = 2
15
9
  @levels = LEVELS.dup
16
10
  @backend = nil
17
11
  end
@@ -29,18 +23,8 @@ module Hunch
29
23
  end
30
24
 
31
25
  def backend
32
- @backend = resolve(@backend)
33
- end
34
-
35
- private
36
-
37
- def resolve(backend)
38
- case backend
39
- when nil, :jev then Backends::Jev.new(self)
40
- when :stub then Backends::Stub.new
41
- when Symbol then raise ConfigurationError, "unknown backend #{backend.inspect}"
42
- else backend
43
- end
26
+ @backend or raise ConfigurationError,
27
+ "no backend: Hunch.configure { |c| c.backend = Hunch::Backends::SystemOne.new(url:, api_key:, model:) }"
44
28
  end
45
29
  end
46
30
  end
@@ -56,6 +56,9 @@ module Hunch
56
56
  end
57
57
 
58
58
  def add(question)
59
+ if Result::RESERVED_KEYS.include?(question.key)
60
+ raise ArgumentError, "#{question.key} is reserved on results; choose another key"
61
+ end
59
62
  raise ArgumentError, "duplicate question key #{question.key}" if @questions.key?(question.key)
60
63
 
61
64
  @questions[question.key] = question
data/lib/hunch/errors.rb CHANGED
@@ -5,6 +5,7 @@ module Hunch
5
5
  class AuthenticationError < Error; end
6
6
  class ValidationError < Error; end
7
7
  class APIError < Error; end
8
+ class InvalidAnswerError < APIError; end
8
9
  class ServerError < APIError; end
9
10
  class OverloadedError < APIError; end
10
11
  class TimeoutError < APIError; end
data/lib/hunch/result.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  module Hunch
2
2
  class Result
3
+ RESERVED_KEYS = %i[model usage to_h].freeze
4
+
3
5
  attr_reader :model, :usage
4
6
 
5
7
  def initialize(questions:, answers:, model: nil, usage: nil)
@@ -8,7 +10,7 @@ module Hunch
8
10
  @values = {}
9
11
  questions.each do |key, question|
10
12
  raw = answers[key.to_s] || answers[key]
11
- raise Error, "backend returned no answer for #{key}" unless raw
13
+ raise InvalidAnswerError, "backend returned no answer for #{key}" unless raw
12
14
 
13
15
  build(key, question, raw)
14
16
  end
@@ -25,26 +27,38 @@ module Hunch
25
27
  def build(key, question, raw)
26
28
  case question.type
27
29
  when :noul then build_noul(key, question, raw)
28
- when :choice then build_choice(key, raw)
30
+ when :choice then build_choice(key, question, raw)
29
31
  when :rate then build_rate(key, question, raw)
30
32
  end
31
33
  end
32
34
 
33
35
  def build_noul(key, question, raw)
34
- probability = raw["noul"].to_f
35
- set(key, probability)
36
- define(:"#{key}?") { probability >= question.threshold }
36
+ probability = raw["noul"]
37
+ unless probability.is_a?(Numeric) && probability.between?(0, 1)
38
+ raise InvalidAnswerError, "#{key} needs a probability, got #{probability.inspect}"
39
+ end
40
+
41
+ set(key, probability.to_f)
42
+ define(:"#{key}?") { probability >= question.threshold } if question.threshold
37
43
  end
38
44
 
39
- def build_choice(key, raw)
45
+ def build_choice(key, question, raw)
40
46
  probabilities = (raw["probabilities"] || {}).transform_keys(&:to_sym)
41
47
  choice = raw["choice"]&.to_sym || probabilities.max_by { |_, p| p }&.first
48
+ unless question.options.key?(choice)
49
+ raise InvalidAnswerError, "#{key} needs one of #{question.options.keys.join(", ")}, got #{choice.inspect}"
50
+ end
51
+
42
52
  set(key, choice)
43
53
  define(:"#{key}_probabilities") { probabilities }
44
54
  define(:"#{key}_confidence") { raw["confidence"] }
45
55
  end
46
56
 
47
57
  def build_rate(key, question, raw)
58
+ unless raw["score"].is_a?(Numeric) && raw["score"].finite?
59
+ raise InvalidAnswerError, "#{key} needs a position, got #{raw["score"].inspect}"
60
+ end
61
+
48
62
  levels = question.levels.keys
49
63
  by_index = raw["probabilities"] || {}
50
64
  probabilities = levels.each_with_index.to_h do |level, i|
data/lib/hunch/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Hunch
2
- VERSION = "0.3.0"
2
+ VERSION = "0.4.0"
3
3
  end
data/lib/hunch.rb CHANGED
@@ -5,7 +5,7 @@ require_relative "hunch/rating"
5
5
  require_relative "hunch/configuration"
6
6
  require_relative "hunch/decision"
7
7
  require_relative "hunch/result"
8
- require_relative "hunch/backends/jev"
8
+ require_relative "hunch/backends/system_one"
9
9
  require_relative "hunch/backends/stub"
10
10
 
11
11
  module Hunch
@@ -32,17 +32,11 @@ module Hunch
32
32
  yield decision
33
33
  raise ArgumentError, "decide needs at least one question" if decision.questions.empty?
34
34
 
35
- raw = configuration.backend.decide(state: given, questions: decision.questions, model: configuration.model)
36
- Result.new(
37
- questions: decision.questions,
38
- answers: raw["answers"] || {},
39
- model: raw["model"],
40
- usage: raw["usage"]
41
- )
35
+ ask(decision.questions, given)
42
36
  end
43
37
 
44
38
  def chance(question, given:, yes: nil, no: nil)
45
- decide(given:) { |q| q.likely?(:answer, question, yes:, no:) }.answer
39
+ ask({ answer: Questions::Noul.new(key: :answer, question:, yes:, no:, threshold: nil) }, given).answer
46
40
  end
47
41
 
48
42
  Configuration::LEVELS.each_key do |level|
@@ -61,6 +55,11 @@ module Hunch
61
55
 
62
56
  private
63
57
 
58
+ def ask(questions, state)
59
+ raw = configuration.backend.decide(state:, questions:)
60
+ Result.new(questions:, answers: raw["answers"] || {}, model: raw["model"], usage: raw["usage"])
61
+ end
62
+
64
63
  def custom_level(name)
65
64
  return unless name.to_s.end_with?("?")
66
65
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hunch
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Carl Dawson
@@ -20,8 +20,8 @@ files:
20
20
  - LICENSE.txt
21
21
  - README.md
22
22
  - lib/hunch.rb
23
- - lib/hunch/backends/jev.rb
24
23
  - lib/hunch/backends/stub.rb
24
+ - lib/hunch/backends/system_one.rb
25
25
  - lib/hunch/configuration.rb
26
26
  - lib/hunch/decision.rb
27
27
  - lib/hunch/errors.rb