jev-feels 1.1.1 → 1.1.2
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 +13 -0
- data/README.md +6 -4
- data/lib/jev/definition.rb +2 -0
- data/lib/jev/harness.rb +7 -3
- data/lib/jev/result.rb +15 -33
- data/lib/jev/transport.rb +8 -20
- data/lib/jev/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a3eece3f0263fb96e25774fc923368f688a0d9f9fa20d753b2def6fea8bd218c
|
|
4
|
+
data.tar.gz: 3c2c4aba985b8110c1911d7e3de1a75df6c7556a40925739c7b79edc576cc1e7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 01c48dd2cf71d607b8fb0b6fd26c07ef29db4730c3875c21f8a41c75a6522ab32fcaedf12f1c2c270267f12b7b05b414112abc7df1a6d99e1b2792a59e7746cc
|
|
7
|
+
data.tar.gz: 5b2125b7d7055fed2b99dc65694043f42af4cc8071c4e516803e960304172ccf6a632a69f11f82ac52ca679258b8c003784a2c1be3e83791f0b2cfa3fd0c0043
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.1.2
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Enforce a maximum of 255 Choice options and 10 Score levels for named and ad-hoc questions.
|
|
8
|
+
- Generate Score stub probabilities between adjacent levels so their weighted mean matches the supplied score; reject out-of-range scores when generating probabilities.
|
|
9
|
+
- Preserve explicitly supplied stub probabilities and confidence.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Simplify response parsing to JSON number and string key types, and require a definition when parsing Choice answers.
|
|
14
|
+
- Read HTTP error details from `detail.message`; remove error-message redaction and truncation.
|
|
15
|
+
|
|
3
16
|
## 1.1.1
|
|
4
17
|
|
|
5
18
|
### Fixed
|
data/README.md
CHANGED
|
@@ -85,7 +85,7 @@ Jev.define :urgent, "Requires immediate attention or action"
|
|
|
85
85
|
Jev.define :spam, "Unsolicited or unwanted promotional content"
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
-
A decision (`decide`):
|
|
88
|
+
A decision (`decide`), with up to 255 choices:
|
|
89
89
|
|
|
90
90
|
```ruby
|
|
91
91
|
Jev.define :support_team,
|
|
@@ -98,7 +98,7 @@ Jev.define :support_team,
|
|
|
98
98
|
}
|
|
99
99
|
```
|
|
100
100
|
|
|
101
|
-
A scale (`score`). Hash order is low to high:
|
|
101
|
+
A scale (`score`), with 2 to 10 levels. Hash order is low to high:
|
|
102
102
|
|
|
103
103
|
```ruby
|
|
104
104
|
Jev.define :severity,
|
|
@@ -297,6 +297,8 @@ end
|
|
|
297
297
|
|
|
298
298
|
A full `decide` / `score` stub is a hash with `choice` or `score`, plus `confidence` and `probabilities`. To own the HTTP shape, set `config.transport`.
|
|
299
299
|
|
|
300
|
+
When Score probabilities are omitted, the stub distributes probability between adjacent levels so their weighted mean equals the score. The score must be within the declared scale (`0..levels.size - 1`); otherwise it raises `ArgumentError`. Explicit probabilities and confidence are preserved; confidence defaults to `1.0`.
|
|
301
|
+
|
|
300
302
|
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.
|
|
301
303
|
|
|
302
304
|
```ruby
|
|
@@ -330,7 +332,7 @@ end
|
|
|
330
332
|
|
|
331
333
|
## Errors
|
|
332
334
|
|
|
333
|
-
All errors inherit from `Jev::Error`. HTTP failures are wrapped; the original exception is available as `cause`.
|
|
335
|
+
All errors inherit from `Jev::Error`. HTTP failures are wrapped; the original exception is available as `cause`.
|
|
334
336
|
|
|
335
337
|
| Error | When |
|
|
336
338
|
| --------------------------- | --------------------------------------------- |
|
|
@@ -346,4 +348,4 @@ Malformed definitions (`choices:` and `levels:` together, empty Choice, too few
|
|
|
346
348
|
|
|
347
349
|
## What this talks to
|
|
348
350
|
|
|
349
|
-
[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`.
|
|
351
|
+
[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/lib/jev/definition.rb
CHANGED
|
@@ -48,6 +48,7 @@ module Jev
|
|
|
48
48
|
def self.normalize_choices(choices)
|
|
49
49
|
raise ArgumentError, "choices must be a Hash" unless choices.is_a?(Hash)
|
|
50
50
|
raise ArgumentError, "choices cannot be empty" if choices.empty?
|
|
51
|
+
raise ArgumentError, "choices must have at most 255 entries" if choices.size > 255
|
|
51
52
|
|
|
52
53
|
unique_keys(choices, "choice").freeze
|
|
53
54
|
end
|
|
@@ -56,6 +57,7 @@ module Jev
|
|
|
56
57
|
def self.normalize_levels(levels)
|
|
57
58
|
raise ArgumentError, "levels must be a Hash" unless levels.is_a?(Hash)
|
|
58
59
|
raise ArgumentError, "levels must have at least 2 entries" if levels.size < 2
|
|
60
|
+
raise ArgumentError, "levels must have at most 10 entries" if levels.size > 10
|
|
59
61
|
|
|
60
62
|
unique_keys(levels, "level").freeze
|
|
61
63
|
end
|
data/lib/jev/harness.rb
CHANGED
|
@@ -233,9 +233,13 @@ module Jev
|
|
|
233
233
|
end
|
|
234
234
|
|
|
235
235
|
def default_score_probabilities(criteria, score)
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
236
|
+
score = Float(score)
|
|
237
|
+
maximum = criteria.size - 1
|
|
238
|
+
unless score.finite? && score.between?(0, maximum)
|
|
239
|
+
raise ArgumentError, "stub score must be between 0 and #{maximum}"
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
criteria.size.times.to_h { |index| [index.to_s, [1.0 - (index - score).abs, 0.0].max] }
|
|
239
243
|
end
|
|
240
244
|
|
|
241
245
|
def score_probability_hash(probabilities, definition)
|
data/lib/jev/result.rb
CHANGED
|
@@ -15,14 +15,9 @@ module Jev
|
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def self.finite_unit(value, label)
|
|
18
|
-
raise InvalidResponseError, "Jev response is missing #{label}" unless value.is_a?(
|
|
18
|
+
raise InvalidResponseError, "Jev response is missing #{label}" unless value.is_a?(Integer) || value.is_a?(Float)
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
raise InvalidResponseError, "Jev #{label} is not finite" unless value.finite?
|
|
22
|
-
|
|
23
|
-
value
|
|
24
|
-
rescue ArgumentError, TypeError, RangeError
|
|
25
|
-
raise InvalidResponseError, "Jev #{label} is not a real number"
|
|
20
|
+
Float(value)
|
|
26
21
|
end
|
|
27
22
|
|
|
28
23
|
class Noul
|
|
@@ -35,13 +30,12 @@ module Jev
|
|
|
35
30
|
|
|
36
31
|
def self.parse(answer)
|
|
37
32
|
noul = answer["noul"]
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
raise InvalidResponseError, "Jev noul probability is not finite" unless noul.finite?
|
|
33
|
+
unless noul.is_a?(Integer) || noul.is_a?(Float)
|
|
34
|
+
raise InvalidResponseError, "Jev response is missing a noul probability"
|
|
35
|
+
end
|
|
42
36
|
|
|
43
37
|
# ponytail: clamp out-of-range noul; raise if Jev starts returning uncalibrated values
|
|
44
|
-
new(probability: noul.clamp(0.0, 1.0))
|
|
38
|
+
new(probability: Float(noul).clamp(0.0, 1.0))
|
|
45
39
|
end
|
|
46
40
|
|
|
47
41
|
def type
|
|
@@ -63,7 +57,7 @@ module Jev
|
|
|
63
57
|
freeze
|
|
64
58
|
end
|
|
65
59
|
|
|
66
|
-
def self.parse(answer, definition
|
|
60
|
+
def self.parse(answer, definition)
|
|
67
61
|
winner = choice_key(answer["choice"], definition)
|
|
68
62
|
new(
|
|
69
63
|
choice: winner,
|
|
@@ -73,12 +67,8 @@ module Jev
|
|
|
73
67
|
end
|
|
74
68
|
|
|
75
69
|
def self.choice_key(key, definition)
|
|
76
|
-
unless key.is_a?(String)
|
|
77
|
-
|
|
78
|
-
end
|
|
79
|
-
if definition && !definition.choices.key?(key.to_sym)
|
|
80
|
-
raise InvalidResponseError, "Jev response has an unknown choice"
|
|
81
|
-
end
|
|
70
|
+
raise InvalidResponseError, "Jev response is missing a valid choice" unless key.is_a?(String)
|
|
71
|
+
raise InvalidResponseError, "Jev response has an unknown choice" unless definition.choices.key?(key.to_sym)
|
|
82
72
|
|
|
83
73
|
key.to_sym
|
|
84
74
|
end
|
|
@@ -132,29 +122,21 @@ module Jev
|
|
|
132
122
|
|
|
133
123
|
raw.to_h do |key, value|
|
|
134
124
|
index = level_index(key, names)
|
|
135
|
-
[names
|
|
125
|
+
[names.fetch(index), Result.finite_unit(value, "score probability")]
|
|
136
126
|
end.freeze
|
|
137
127
|
end
|
|
138
128
|
|
|
139
129
|
def self.level_index(key, names)
|
|
140
|
-
index = parse_index(key)
|
|
141
|
-
if index.negative? || (names && index >= names.size)
|
|
142
|
-
raise InvalidResponseError, "Jev score probability has an unknown level"
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
index
|
|
146
|
-
end
|
|
147
|
-
private_class_method :level_index
|
|
148
|
-
|
|
149
|
-
def self.parse_index(key)
|
|
150
|
-
return key if key.is_a?(Integer)
|
|
151
130
|
unless key.is_a?(String) && key.match?(/\A\d+\z/)
|
|
152
131
|
raise InvalidResponseError, "Jev score probabilities are not keyed by level number"
|
|
153
132
|
end
|
|
154
133
|
|
|
155
|
-
Integer(key, 10)
|
|
134
|
+
index = Integer(key, 10)
|
|
135
|
+
raise InvalidResponseError, "Jev score probability has an unknown level" if index >= names.size
|
|
136
|
+
|
|
137
|
+
index
|
|
156
138
|
end
|
|
157
|
-
private_class_method :
|
|
139
|
+
private_class_method :level_index
|
|
158
140
|
|
|
159
141
|
def self.named_level(probabilities, names)
|
|
160
142
|
return unless names
|
data/lib/jev/transport.rb
CHANGED
|
@@ -20,7 +20,7 @@ module Jev
|
|
|
20
20
|
rescue Timeout::Error
|
|
21
21
|
raise RequestError, "Jev request timed out"
|
|
22
22
|
rescue SystemCallError, SocketError, IOError, OpenSSL::SSL::SSLError => e
|
|
23
|
-
raise RequestError,
|
|
23
|
+
raise RequestError, "Jev request failed: #{e.message}"
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
private
|
|
@@ -104,32 +104,20 @@ module Jev
|
|
|
104
104
|
end
|
|
105
105
|
|
|
106
106
|
def error_detail(raw)
|
|
107
|
-
text = extract_error_message(raw)
|
|
108
|
-
return if text.nil? || text.empty?
|
|
109
|
-
|
|
110
|
-
redact(text)[0, 200]
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
def extract_error_message(raw)
|
|
114
107
|
parsed = JSON.parse(raw)
|
|
115
|
-
return
|
|
108
|
+
return unless parsed.is_a?(Hash)
|
|
109
|
+
|
|
110
|
+
detail = parsed["detail"]
|
|
111
|
+
return unless detail.is_a?(Hash)
|
|
116
112
|
|
|
117
|
-
message =
|
|
118
|
-
message
|
|
119
|
-
message.to_s
|
|
113
|
+
message = detail["message"]
|
|
114
|
+
message if message.is_a?(String) && !message.empty?
|
|
120
115
|
rescue JSON::ParserError
|
|
121
|
-
|
|
116
|
+
nil
|
|
122
117
|
end
|
|
123
118
|
|
|
124
119
|
def join_detail(prefix, detail)
|
|
125
120
|
detail ? "#{prefix}: #{detail}" : prefix
|
|
126
121
|
end
|
|
127
|
-
|
|
128
|
-
def redact(text)
|
|
129
|
-
key = @configuration.api_key
|
|
130
|
-
return text.to_s if key.nil? || key.empty?
|
|
131
|
-
|
|
132
|
-
text.to_s.gsub(key, "[FILTERED]")
|
|
133
|
-
end
|
|
134
122
|
end
|
|
135
123
|
end
|
data/lib/jev/version.rb
CHANGED