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 +4 -4
- data/CHANGELOG.md +25 -0
- data/README.md +280 -78
- data/context7.json +1 -1
- data/lib/feels/active_model.rb +5 -0
- data/lib/feels/string.rb +12 -0
- data/lib/jev/active_model.rb +30 -0
- data/lib/jev/client.rb +16 -20
- data/lib/jev/definition.rb +91 -0
- data/lib/jev/errors.rb +1 -0
- data/lib/jev/feels.rb +12 -0
- data/lib/jev/harness.rb +215 -0
- data/lib/jev/match.rb +28 -0
- data/lib/jev/model.rb +111 -0
- data/lib/jev/query.rb +33 -0
- data/lib/jev/registry.rb +74 -10
- data/lib/jev/result.rb +165 -0
- data/lib/jev/version.rb +1 -1
- data/lib/jev-feels/active_model.rb +3 -0
- data/lib/jev.rb +187 -37
- metadata +15 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bf19b7812e88098da8abdb24e2c951f11ed906bf8f0200be8d889cc9c7565d13
|
|
4
|
+
data.tar.gz: 836af8d2b70dbf6828f170a01b8d00e19ffadb4a56b5e8409c845bef1500cda4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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.
|
|
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
|
-
|
|
50
|
-
|
|
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 "
|
|
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
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
##
|
|
115
|
+
## Definitions
|
|
98
116
|
|
|
99
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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
|
-
|
|
108
|
-
`feels?` is `feels >= threshold`. The default threshold is `0.5`.
|
|
124
|
+
A decision (`decide`):
|
|
109
125
|
|
|
110
126
|
```ruby
|
|
111
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
159
|
+
Jev.definitions(Comment)
|
|
138
160
|
```
|
|
139
161
|
|
|
140
|
-
`define` replaces an existing name in that scope.
|
|
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
|
-
|
|
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.
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
|
173
|
-
|
|
|
174
|
-
| `Jev::ConfigurationError`
|
|
175
|
-
| `Jev::UndefinedDefinition`
|
|
176
|
-
| `Jev::AuthenticationError`
|
|
177
|
-
| `Jev::RateLimitError`
|
|
178
|
-
| `Jev::InvalidResponseError` | Unparseable or shapeless body
|
|
179
|
-
| `Jev::RequestError`
|
|
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
|
|
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": "
|
|
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
|
}
|
data/lib/feels/string.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
25
|
+
Harness.current_transport(@configuration)
|
|
20
26
|
end
|
|
21
27
|
|
|
22
|
-
def payload(text,
|
|
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
|
|
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
|
-
|
|
42
|
-
|
|
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
|
-
|
|
51
|
-
|
|
46
|
+
[id, Result.parse(answer, definition)]
|
|
47
|
+
end
|
|
52
48
|
end
|
|
53
49
|
end
|
|
54
50
|
end
|