jev-feels 0.2.0 → 1.1.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: 125dde58282251c7692d030e90352e098cf1785bc38904cf6021098d073d2f8a
4
- data.tar.gz: 6bdf7fb784623b94cde049148294a1513e59207ccde0fd683869bbb4cc639a6d
3
+ metadata.gz: bf19b7812e88098da8abdb24e2c951f11ed906bf8f0200be8d889cc9c7565d13
4
+ data.tar.gz: 836af8d2b70dbf6828f170a01b8d00e19ffadb4a56b5e8409c845bef1500cda4
5
5
  SHA512:
6
- metadata.gz: 2b5a17cbde5d54178e39eed6600fdff431f5fdfa0515af09f79f4711eb19b67148a87e669a973755d2afa9546c2fcd5e6e14e15b29d1ddc4908834bc24ca4570
7
- data.tar.gz: fb31b5c3f4fb3d90204cbfa815a5bf3aff537eb8611645f5099c79b775f1fd6d8f6427dc4ea603a8b7f670278aae61fe7b7e6e009e7136528fd75ab2149c0c82
6
+ metadata.gz: 17e714115b72dbbb3dc0a69881d135ddbb670238ce4eb6bd7221ee10db3ea6c12a4f0c7aab00c17cf78277352d3063499d9bfd507f4160e15f6aa7940716b053
7
+ data.tar.gz: 0c05ddcd181d7cfec1eaee0df0c2582e3f34431989cc3231014440657b67295cb2fac42b93eff61a2defbbfe567bb671924e5dfa86f1f6ab0b174a78187d7f3d
data/CHANGELOG.md ADDED
@@ -0,0 +1,25 @@
1
+ # Changelog
2
+
3
+ ## 1.1.0
4
+
5
+ ### Added
6
+
7
+ - `Jev.define` accepts `choices:` (`decide`) and `levels:` (`score`, Hash, low to high).
8
+ - `Jev.decide`, `Jev.score`, `Jev.measure`, and `Jev.match`.
9
+ - Typed results: `Jev::Result::Noul`, `Jev::Result::Choice`, `Jev::Result::Score`, `Jev::Result::Batch`.
10
+ - Batch `Jev.measure(state) { |q| ... }` — one request, defined names only.
11
+ - `at_least:` on `feels?` (true / false / nil) and `confidence:` on `decide` / `match`.
12
+ - Native `case`/`in` on a batch via collapsed values.
13
+ - `Jev::Model`: `feels` / `decide` / `score` bind a field; instance `feels?`, `decide`, `score`, `measure`, `match`.
14
+ - Optional `require "feels/active_model"` and `validates_feeling`.
15
+ - `Jev.stub`, `Jev.record`, and `Jev.replay`. Tapes omit API keys.
16
+ - Opt-in String refinement / extension: `decide`, `score`, `measure`.
17
+
18
+ ### Changed
19
+
20
+ - `feels` / `feels?` / `decide` / `score` collapse a typed `Jev.measure` result.
21
+ - Yes/no `Jev.definition` / `Jev.definitions` still return the instruction string. `choices:` / `levels:` return `Jev::Definition`.
22
+
23
+ ## 1.0.0
24
+
25
+ - Initial `Jev.define` / `Jev.feels` / `Jev.feels?` API, injectable transport, and opt-in String sugar.
data/README.md CHANGED
@@ -1,12 +1,13 @@
1
1
  # jev-feels
2
2
 
3
- Semantic checks as ordinary Ruby conditions.
3
+ Semantic decisions as ordinary Ruby.
4
4
 
5
5
  ```ruby
6
6
  # Gemfile
7
7
  gem "jev-feels"
8
8
 
9
9
  require "feels"
10
+ using Jev::Feels
10
11
 
11
12
  Jev.configure do |config|
12
13
  config.api_key = ENV.fetch("JEV_API_KEY")
@@ -14,67 +15,84 @@ end
14
15
 
15
16
  Jev.define :urgent, "Requires immediate attention or action"
16
17
 
17
- email = <<~EMAIL
18
- Our production server has stopped responding.
19
- Customers cannot access their accounts.
20
- Please investigate immediately.
21
- EMAIL
22
-
23
- if Jev.feels?(email, :urgent)
24
- puts "URGENT"
18
+ if email.body.feels?(:urgent)
19
+ email.mark(:urgent)
25
20
  end
26
21
  ```
27
22
 
28
- ## Ruby on Rails
29
-
30
- Gemfile, initializer, then an ordinary condition. Bundler loads the gem — no extra `require` unless you want sugar on `String`.
23
+ The same idea continues for a decision and a scale:
31
24
 
32
25
  ```ruby
33
- # Gemfile
34
- gem "jev-feels"
26
+ Jev.define :support_team,
27
+ "Which team should handle this?",
28
+ choices: {
29
+ billing: "payments and refunds",
30
+ technical: "bugs and outages",
31
+ sales: "purchase questions",
32
+ other: "none of the above"
33
+ }
34
+
35
+ team = Jev.decide(email.body, :support_team)
35
36
  ```
36
37
 
37
38
  ```ruby
38
- # config/initializers/jev.rb
39
- Jev.configure do |config|
40
- config.api_key = ENV.fetch("JEV_API_KEY")
41
- end
39
+ Jev.define :severity,
40
+ "How severe is this issue?",
41
+ levels: {
42
+ minor: "minor inconvenience",
43
+ degraded: "workaround exists",
44
+ blocking: "cannot complete the task",
45
+ critical: "major outage or severe impact"
46
+ }
42
47
 
43
- Jev.define :urgent, "Requires immediate attention or action"
44
- Jev.define SupportEmail, :urgent, "Outage, customers cannot sign in"
45
- Jev.define Comment, :urgent, "Legal takedown or self-harm"
48
+ severity = Jev.score(email.body, :severity)
46
49
  ```
47
50
 
51
+ You do not need the Jev JSON wire format or the HTTP API for typical use. `Jev.measure` keeps the full typed result when you do.
52
+
53
+ ## Ruby on Rails
54
+
55
+ Configure once, declare on the model, ask the record:
56
+
48
57
  ```ruby
49
- class SupportEmail < ApplicationRecord
50
- def urgent?
51
- Jev.feels?(body, SupportEmail, :urgent)
52
- end
53
- end
58
+ # Gemfile
59
+ gem "jev-feels"
54
60
  ```
55
61
 
56
- To use `#feels?` on strings across the app, require the String extension in the initializer:
57
-
58
62
  ```ruby
59
63
  # config/initializers/jev.rb
60
- require "jev-feels/string"
64
+ require "feels/active_model"
61
65
 
62
66
  Jev.configure do |config|
63
67
  config.api_key = ENV.fetch("JEV_API_KEY")
64
68
  end
65
69
 
66
70
  Jev.define :urgent, "Requires immediate attention or action"
67
- Jev.define SupportEmail, :urgent, "Outage, customers cannot sign in"
71
+ Jev.define :support_team, "Which team should handle this?", choices: {
72
+ billing: "payments and refunds",
73
+ technical: "bugs and outages",
74
+ other: "none of the above"
75
+ }
68
76
  ```
69
77
 
70
78
  ```ruby
71
79
  class SupportEmail < ApplicationRecord
72
- def urgent?
73
- body.feels?(SupportEmail, :urgent)
74
- end
80
+ include Jev::Model
81
+
82
+ feels :body, :urgent # Get definition from initializer
83
+ feels :subject, :not_important, "Subject of this email is not important" # Or define in model
84
+ decide :body, :support_team
85
+
86
+ validates_feeling :body, :urgent, threshold: 0.9
75
87
  end
88
+
89
+ email.feels?(:urgent)
90
+ email.feels?(:not_important)
91
+ email.decide(:support_team)
76
92
  ```
77
93
 
94
+ The class is the scope, the first argument is the field. `validates_feeling` accepts `allow_nil:`, `allow_blank:`, `if:`, `unless:`, `on:`, `message:`, `at_least:`. If those skip the check, there is no HTTP call. A `nil` from `at_least:` is a validation failure.
95
+
78
96
  ## String sugar
79
97
 
80
98
  `require "feels"` does not change `String`. Opt in with a refinement:
@@ -82,7 +100,9 @@ end
82
100
  ```ruby
83
101
  using Jev::Feels
84
102
 
85
- email.feels?(:urgent)
103
+ email.body.feels?(:urgent)
104
+ email.body.decide(:support_team)
105
+ email.body.score(:severity)
86
106
  ```
87
107
 
88
108
  Or, if you really want a global patch:
@@ -90,73 +110,242 @@ Or, if you really want a global patch:
90
110
  ```ruby
91
111
  require "feels/string"
92
112
  # or: require "jev-feels/string"
93
-
94
- email.feels?(:urgent)
95
113
  ```
96
114
 
97
- ## Probability vs predicate
115
+ ## Definitions
98
116
 
99
- ```ruby
100
- email.feels(:urgent)
101
- # => 0.94
117
+ `Jev.define` is the vocabulary of the app. A symbol without extras is a yes/no check:
102
118
 
103
- email.feels?(:urgent)
104
- # => true
119
+ ```ruby
120
+ Jev.define :urgent, "Requires immediate attention or action"
121
+ Jev.define :spam, "Unsolicited or unwanted promotional content"
105
122
  ```
106
123
 
107
- `feels` is the calibrated probability that the predicate holds (a Jev [Noul](https://docs.typesafe.ai/primitives/noul.md), `0.0..1.0`).
108
- `feels?` is `feels >= threshold`. The default threshold is `0.5`.
124
+ A decision (`decide`):
109
125
 
110
126
  ```ruby
111
- email.feels?(:urgent, threshold: 0.8)
127
+ Jev.define :support_team,
128
+ "Which support team should handle this?",
129
+ choices: {
130
+ billing: "payments, invoices, charges and refunds",
131
+ technical: "bugs, outages and integrations",
132
+ sales: "purchase and plan questions",
133
+ other: "none of the above"
134
+ }
112
135
  ```
113
136
 
114
- ## Ad-hoc predicates
115
-
116
- A `String` predicate is used as-is and is **not** registered:
137
+ A scale (`score`). Hash order is low to high:
117
138
 
118
139
  ```ruby
119
- email.feels?("sounds like the sender is about to cancel their subscription")
140
+ Jev.define :severity,
141
+ "How severe is this customer issue?",
142
+ levels: {
143
+ cosmetic: "minor visual or cosmetic issue",
144
+ degraded: "functionality is degraded but a workaround exists",
145
+ blocking: "the customer cannot complete an important task",
146
+ critical: "major outage, data loss, security issue, or severe business impact"
147
+ }
120
148
  ```
121
149
 
122
- A `Symbol` must already have a definition or you get `Jev::UndefinedDefinition`.
123
-
124
- ## Definitions
150
+ Scoped names still work:
125
151
 
126
152
  ```ruby
127
- Jev.define :urgent, "Requires immediate attention or action"
128
- Jev.define :spam, "Unsolicited or unwanted promotional content"
129
- Jev.define :angry, "Expresses anger or hostility"
130
-
131
153
  Jev.define Email, :urgent, "Outage, customers cannot sign in"
132
154
  Jev.define Comment, :urgent, "Legal takedown or self-harm"
133
155
 
134
156
  Jev.definition(:urgent)
135
157
  Jev.definition(Email, :urgent)
136
158
  Jev.definitions
137
- Jev.definitions(Email)
159
+ Jev.definitions(Comment)
138
160
  ```
139
161
 
140
- `define` replaces an existing name in that scope. `Jev.feels?(text, Email, :urgent)` uses the Email definition, then falls back to the global `:urgent` if Email has none. `definitions` / `definitions(Email)` return frozen copies.
162
+ `define` replaces an existing name in that scope. Scopes are stored by class name, so `Email`, `"Email"` and `:Email` are the same key. A yes/no definition reads back as its instruction string. `choices:` / `levels:` read back as a `Jev::Definition`. On a Rails model, `feels` / `decide` / `score` bind a field; see [Ruby on Rails](#ruby-on-rails).
141
163
 
142
- ## Configuration
164
+ A `Symbol` must already have a definition or you get `Jev::UndefinedDefinition`. A `String` is an ad-hoc question and is not registered.
165
+
166
+ ## Asking
143
167
 
144
168
  ```ruby
145
- Jev.configure do |config|
146
- config.api_key = ENV["JEV_API_KEY"]
147
- config.base_url = "https://api.typesafe.ai"
148
- config.timeout = 10
149
- config.threshold = 0.5
150
- end
169
+ Jev.feels(email.body, :urgent) # => 0.93
170
+ Jev.feels?(email.body, :urgent) # => true / false
171
+ Jev.decide(ticket, :support_team) # => :billing
172
+ Jev.score(ticket, :severity) # => 2.37
151
173
  ```
152
174
 
153
- For tests, inject a transport and skip the network:
175
+ `feels` is the calibrated probability (`0.0..1.0`). `feels?` is `feels >= threshold`. The default threshold is `0.5`.
154
176
 
155
177
  ```ruby
156
- Jev.configure do |config|
157
- config.transport = ->(_payload) {
158
- { "answers" => { "feels" => { "type" => "noul", "noul" => 0.9 } } }
178
+ email.body.feels?(:urgent, threshold: 0.8)
179
+ ```
180
+
181
+ `decide` returns the winning Symbol. `score` returns the fractional ordinal position — not a percentage.
182
+
183
+ Ad-hoc questions skip `define`:
184
+
185
+ ```ruby
186
+ email.body.feels?("sounds like the sender is about to cancel their subscription")
187
+
188
+ Jev.decide(
189
+ ticket,
190
+ "Which support team should handle this?",
191
+ choices: {
192
+ billing: "payments and refunds",
193
+ technical: "bugs and outages",
194
+ other: "none of the above"
159
195
  }
196
+ )
197
+ ```
198
+
199
+ `Jev.feels?(text, Email, :urgent)` uses Email's definition, then a superclass, then the global `:urgent`.
200
+
201
+ ## Uncertainty
202
+
203
+ `threshold:` is a single true/false cut. `0.51` and `0.99` are the same `true`.
204
+
205
+ `at_least:` is minimum certainty in either direction. The middle band is `nil`:
206
+
207
+ ```ruby
208
+ text.feels?(:urgent, at_least: 0.8)
209
+ # p >= 0.8 => true
210
+ # p <= 0.2 => false
211
+ # 0.2 < p < 0.8 => nil
212
+ ```
213
+
214
+ Without `at_least:`, `feels?` is still always true or false.
215
+
216
+ Do not pass both `threshold:` and `at_least:`.
217
+
218
+ `decide` uses confidence, which is not the winner's probability:
219
+
220
+ ```ruby
221
+ Jev.decide(ticket, :support_team, confidence: 0.8)
222
+ # => :billing or nil
223
+ ```
224
+
225
+ ## Typed results
226
+
227
+ Convenience methods collapse a result. `measure` keeps the rest:
228
+
229
+ ```ruby
230
+ result = Jev.measure(ticket, :urgent)
231
+ result.probability
232
+ result.type # => :noul
233
+
234
+ result = Jev.measure(ticket, :support_team)
235
+ result.choice
236
+ result.confidence
237
+ result.probabilities
238
+ result.type # => :choice
239
+
240
+ result = Jev.measure(ticket, :severity)
241
+ result.score
242
+ result.confidence
243
+ result.probabilities
244
+ result.levels
245
+ result.level # => :blocking
246
+ result.type # => :score
247
+ ```
248
+
249
+ ```ruby
250
+ feels? # boolean, or nil with at_least:
251
+ feels # probability
252
+ decide # Symbol, or nil with confidence:
253
+ score # Float
254
+ ```
255
+
256
+ ## Batch
257
+
258
+ Several questions, one Jev request, one `state`:
259
+
260
+ ```ruby
261
+ result = Jev.measure(ticket) do |q|
262
+ q.feels :urgent
263
+ q.feels :angry
264
+ q.decide :support_team
265
+ q.score :severity
266
+ end
267
+
268
+ result[:urgent] # => Jev::Result::Noul
269
+ result[:support_team] # => Jev::Result::Choice
270
+ result[:severity] # => Jev::Result::Score
271
+
272
+ result.to_h
273
+ # => { urgent: true, angry: false, support_team: :billing, severity: 2.37 }
274
+ ```
275
+
276
+ A batch only uses defined names. Ad-hoc strings stay on a single `feels?` / `decide` / `score`.
277
+
278
+ ## Pattern matching
279
+
280
+ `Jev::Result::Batch` uses collapsed values. No extra HTTP calls:
281
+
282
+ ```ruby
283
+ case Jev.measure(ticket) { |q|
284
+ q.feels :urgent
285
+ q.decide :support_team
286
+ q.score :severity
287
+ }
288
+ in { urgent: true, support_team: :billing }
289
+ escalate_billing(ticket)
290
+ in { support_team: :technical }
291
+ route_to_technical(ticket)
292
+ in { urgent: true, severity: 2.0.. }
293
+ escalate(ticket)
294
+ else
295
+ manual_review(ticket)
296
+ end
297
+ ```
298
+
299
+ `feels?` collapses to `true`/`false`, `decide` to a Symbol, `score` stays a Float so ranges work.
300
+
301
+ ## `Jev.match`
302
+
303
+ A small dispatch for one `decide`. Low confidence goes to `otherwise`:
304
+
305
+ ```ruby
306
+ Jev.match(ticket, :support_team) do
307
+ on(:billing) { route_to_billing(ticket) }
308
+ on(:technical) { route_to_technical(ticket) }
309
+ on(:sales) { route_to_sales(ticket) }
310
+ otherwise { manual_review(ticket) }
311
+ end
312
+
313
+ Jev.match(ticket, :support_team, confidence: 0.8) do
314
+ on(:billing, :sales) { route_to_commercial(ticket) }
315
+ otherwise { manual_review(ticket) }
316
+ end
317
+ ```
318
+
319
+ ## Tests: stub, record, replay
320
+
321
+ Stub by definition name. The block restores the previous transport, including in other threads:
322
+
323
+ ```ruby
324
+ Jev.stub(
325
+ urgent: 0.95,
326
+ support_team: :billing,
327
+ severity: 2.4
328
+ ) do
329
+ expect(ticket.feels?(:urgent)).to be(true)
330
+ expect(Jev.decide(ticket, :support_team)).to eq(:billing)
331
+ end
332
+ ```
333
+
334
+ A full `decide` / `score` stub is a hash with `choice` or `score`, plus `confidence` and `probabilities`. To own the HTTP shape, set `config.transport`.
335
+
336
+ Record real answers and replay them later. Replay never uses the network. The tape matches the request (state and questions), not a queue index. API keys and Authorization headers are not stored.
337
+
338
+ ```ruby
339
+ tape = Jev.record do
340
+ ticket.feels?(:urgent)
341
+ Jev.decide(ticket, :support_team)
342
+ end
343
+
344
+ json = tape.to_json
345
+
346
+ Jev.replay(tape) do
347
+ ticket.feels?(:urgent)
348
+ Jev.decide(ticket, :support_team)
160
349
  end
161
350
  ```
162
351
 
@@ -165,19 +354,32 @@ Jev.reset_configuration!
165
354
  Jev.reset_definitions!
166
355
  ```
167
356
 
357
+ ## Configuration
358
+
359
+ ```ruby
360
+ Jev.configure do |config|
361
+ config.api_key = ENV["JEV_API_KEY"]
362
+ end
363
+ ```
364
+
365
+ `base_url` defaults to `https://api.typesafe.ai`, `timeout` to 10 seconds, `threshold` to `0.5`. Override only if you need to.
366
+
168
367
  ## Errors
169
368
 
170
369
  All errors inherit from `Jev::Error`. HTTP failures are wrapped; the original exception is available as `cause`. API keys are redacted from messages.
171
370
 
172
- | Error | When |
173
- | --- | --- |
174
- | `Jev::ConfigurationError` | Missing API key |
175
- | `Jev::UndefinedDefinition` | Unknown symbol predicate |
176
- | `Jev::AuthenticationError` | HTTP 401 |
177
- | `Jev::RateLimitError` | HTTP 429 |
178
- | `Jev::InvalidResponseError` | Unparseable or shapeless body |
179
- | `Jev::RequestError` | Timeouts, network errors, other HTTP failures |
371
+ | Error | When |
372
+ | --------------------------- | --------------------------------------------- |
373
+ | `Jev::ConfigurationError` | Missing API key |
374
+ | `Jev::UndefinedDefinition` | Unknown symbol predicate |
375
+ | `Jev::AuthenticationError` | HTTP 401 |
376
+ | `Jev::RateLimitError` | HTTP 429 |
377
+ | `Jev::InvalidResponseError` | Unparseable or shapeless body |
378
+ | `Jev::RequestError` | Timeouts, network errors, other HTTP failures |
379
+ | `Jev::ReplayError` | Replay tape does not contain this request |
380
+
381
+ Malformed definitions (`choices:` and `levels:` together, empty Choice, too few Score levels, duplicate keys, invalid `threshold:` / `at_least:` / `confidence:`) raise `ArgumentError`.
180
382
 
181
383
  ## What this talks to
182
384
 
183
- [Jev](https://docs.typesafe.ai/introduction.md) / TypeSafe System One. `POST https://api.typesafe.ai/v1/systemone` with a Noul question. That is an implementation detail of `feels`; the public API is just `Jev.feels` / `Jev.feels?`.
385
+ [Jev](https://docs.typesafe.ai/introduction.md) / TypeSafe System One. `POST https://api.typesafe.ai/v1/systemone`. That is an implementation detail. The public API is `Jev.feels` / `Jev.feels?` / `Jev.decide` / `Jev.score` / `Jev.measure` / `Jev.match`.
data/context7.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://context7.com/schema/context7.json",
3
3
  "projectTitle": "jev-feels",
4
- "description": "Ruby-like semantic text predicates via Jev. Jev.feels?(:urgent) is an ordinary condition, not an SDK session.",
4
+ "description": "Semantic decisions as ordinary Ruby. Jev.define :urgent, then text.feels?(:urgent), model.decide(:support_team), model.score(:severity), and Jev.measure for typed or batched results. Jev stays an implementation detail.",
5
5
  "excludeFolders": ["spec"]
6
6
  }
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "feels"
4
+ require "active_model"
5
+ require_relative "../jev/active_model"
data/lib/feels/string.rb CHANGED
@@ -10,4 +10,16 @@ class String
10
10
  def feels?(...)
11
11
  Jev.feels?(self, ...)
12
12
  end
13
+
14
+ def decide(...)
15
+ Jev.decide(self, ...)
16
+ end
17
+
18
+ def score(...)
19
+ Jev.score(self, ...)
20
+ end
21
+
22
+ def measure(...)
23
+ Jev.measure(self, ...)
24
+ end
13
25
  end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jev
4
+ module ActiveModel
5
+ def self.included(base)
6
+ base.extend ClassMethods
7
+ end
8
+
9
+ module ClassMethods
10
+ def validates_feeling(attribute, predicate, message: nil, threshold: nil, at_least: nil, **options)
11
+ feeling = { predicate: predicate, message: message, threshold: threshold, at_least: at_least }
12
+ validates_each(attribute, **options) do |record, attr, value|
13
+ Jev::ActiveModel.validate(record, attr, value, feeling)
14
+ end
15
+ end
16
+ end
17
+
18
+ def self.validate(record, attr, value, feeling)
19
+ text = value.is_a?(String) ? value : value.to_s
20
+ kwargs = feeling.slice(:threshold, :at_least).compact
21
+ return if Jev.feels?(text, feeling[:predicate], **kwargs) == true
22
+
23
+ record.errors.add(attr, feeling[:message] || "is not #{feeling[:predicate]}")
24
+ end
25
+ end
26
+ end
27
+
28
+ if defined?(ActiveModel::Validations::HelperMethods)
29
+ ActiveModel::Validations::HelperMethods.include(Jev::ActiveModel::ClassMethods)
30
+ end
data/lib/jev/client.rb CHANGED
@@ -9,46 +9,42 @@ module Jev
9
9
  @configuration = configuration
10
10
  end
11
11
 
12
+ def ask(text, questions)
13
+ body = transport.call(payload(text, questions))
14
+ parse(body, questions)
15
+ end
16
+
12
17
  def probability(text, instructions)
13
- extract_noul(transport.call(payload(text, instructions)))
18
+ definition = Definition.build(name: :feels, instructions: instructions)
19
+ ask(text, { QUESTION_ID => definition }).fetch(QUESTION_ID).probability
14
20
  end
15
21
 
16
22
  private
17
23
 
18
24
  def transport
19
- @configuration.transport || Transport.new(@configuration)
25
+ Harness.current_transport(@configuration)
20
26
  end
21
27
 
22
- def payload(text, instructions)
28
+ def payload(text, questions)
23
29
  {
24
30
  "model" => MODEL,
25
31
  "state" => text,
26
- "questions" => {
27
- QUESTION_ID => {
28
- "type" => "noul",
29
- "instructions" => instructions
30
- }
31
- }
32
+ "questions" => questions.to_h { |id, definition| [id, definition.to_question] }
32
33
  }
33
34
  end
34
35
 
35
- def extract_noul(body)
36
+ def parse(body, questions)
36
37
  raise InvalidResponseError, "Jev response is not a JSON object" unless body.is_a?(Hash)
37
38
 
38
39
  answers = body["answers"]
39
40
  raise InvalidResponseError, "Jev response is missing answers" unless answers.is_a?(Hash)
40
41
 
41
- answer = answers[QUESTION_ID]
42
- raise InvalidResponseError, "Jev response is missing the feels answer" unless answer.is_a?(Hash)
43
-
44
- noul = answer["noul"]
45
- raise InvalidResponseError, "Jev response is missing a noul probability" unless noul.is_a?(Numeric)
46
-
47
- noul = Float(noul)
48
- raise InvalidResponseError, "Jev noul probability is not finite" unless noul.finite?
42
+ questions.to_h do |id, definition|
43
+ answer = answers[id]
44
+ raise InvalidResponseError, "Jev response is missing the #{id} answer" unless answer.is_a?(Hash)
49
45
 
50
- # ponytail: clamp out-of-range noul; raise if Jev starts returning uncalibrated values
51
- noul.clamp(0.0, 1.0)
46
+ [id, Result.parse(answer, definition)]
47
+ end
52
48
  end
53
49
  end
54
50
  end