jev-feels 1.0.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 -59
- 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 +64 -4
- data/lib/jev/query.rb +33 -0
- data/lib/jev/registry.rb +24 -5
- 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 +183 -34
- metadata +14 -4
data/lib/jev/version.rb
CHANGED
data/lib/jev.rb
CHANGED
|
@@ -3,9 +3,14 @@
|
|
|
3
3
|
require_relative "jev/version"
|
|
4
4
|
require_relative "jev/errors"
|
|
5
5
|
require_relative "jev/configuration"
|
|
6
|
+
require_relative "jev/definition"
|
|
6
7
|
require_relative "jev/registry"
|
|
8
|
+
require_relative "jev/result"
|
|
7
9
|
require_relative "jev/transport"
|
|
8
10
|
require_relative "jev/client"
|
|
11
|
+
require_relative "jev/query"
|
|
12
|
+
require_relative "jev/match"
|
|
13
|
+
require_relative "jev/harness"
|
|
9
14
|
require_relative "jev/feels"
|
|
10
15
|
require_relative "jev/model"
|
|
11
16
|
|
|
@@ -22,24 +27,20 @@ module Jev
|
|
|
22
27
|
@configuration = Configuration.new
|
|
23
28
|
end
|
|
24
29
|
|
|
25
|
-
def define(scope_or_name, name_or_description, description = nil)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
registry.define(nil, scope_or_name, name_or_description)
|
|
32
|
-
else
|
|
33
|
-
registry.define(scope_or_name, name_or_description, description)
|
|
34
|
-
end
|
|
30
|
+
def define(scope_or_name, name_or_description, description = nil, **options)
|
|
31
|
+
scope, name, instructions = unpack_define(scope_or_name, name_or_description, description)
|
|
32
|
+
extras = adhoc_options(options)
|
|
33
|
+
registry.define(scope, name, Definition.build(name: name, instructions: instructions, **extras))
|
|
35
34
|
end
|
|
36
35
|
|
|
37
36
|
def definition(scope_or_name, name = nil)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
found =
|
|
38
|
+
if name.nil?
|
|
39
|
+
registry.fetch(scope_or_name, scope: nil, fallback: false, inherit: false)
|
|
40
|
+
else
|
|
41
|
+
registry.fetch(name, scope: scope_or_name, fallback: false, inherit: false)
|
|
42
|
+
end
|
|
43
|
+
found&.public_value
|
|
43
44
|
end
|
|
44
45
|
|
|
45
46
|
def definitions(scope = nil)
|
|
@@ -51,26 +52,106 @@ module Jev
|
|
|
51
52
|
end
|
|
52
53
|
|
|
53
54
|
def feels(text, scope_or_predicate, predicate = nil)
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
measure_one(text, scope_or_predicate, predicate, id: Client::QUESTION_ID).probability
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def feels?(text, scope_or_predicate, predicate = nil, **opts)
|
|
59
|
+
validate_noul_decision!(opts)
|
|
60
|
+
decide_noul(feels(text, scope_or_predicate, predicate), opts)
|
|
56
61
|
end
|
|
57
62
|
|
|
58
|
-
def
|
|
59
|
-
|
|
63
|
+
def decide(text, scope_or_predicate, predicate = nil, confidence: nil, **options)
|
|
64
|
+
require_adhoc_type(scope_or_predicate, predicate, options, :choices)
|
|
65
|
+
confidence = normalize_unit(confidence, "confidence") unless confidence.nil?
|
|
66
|
+
result = typed_measure(:choice, text, scope_or_predicate, predicate, **options)
|
|
67
|
+
return if confidence && result.confidence < confidence
|
|
68
|
+
|
|
69
|
+
result.choice
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def score(text, scope_or_predicate, predicate = nil, **options)
|
|
73
|
+
require_adhoc_type(scope_or_predicate, predicate, options, :levels)
|
|
74
|
+
typed_measure(:score, text, scope_or_predicate, predicate, **options).score
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def measure(text, scope_or_predicate = nil, predicate = nil, **options, &block)
|
|
78
|
+
if block
|
|
79
|
+
raise ArgumentError, "unknown keyword: #{options.keys.first}" unless options.empty?
|
|
80
|
+
raise ArgumentError, "predicate is not used with a measure block" unless predicate.nil?
|
|
81
|
+
|
|
82
|
+
measure_batch(text, scope_or_predicate, &block)
|
|
83
|
+
else
|
|
84
|
+
raise ArgumentError, "question is required" if scope_or_predicate.nil?
|
|
85
|
+
|
|
86
|
+
measure_one(text, scope_or_predicate, predicate, **options)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def match(text, scope_or_predicate, predicate = nil, confidence: nil, **, &block)
|
|
91
|
+
raise ArgumentError, "Jev.match requires a block" unless block
|
|
92
|
+
|
|
93
|
+
matcher = Matcher.new(decide(text, scope_or_predicate, predicate, confidence: confidence, **))
|
|
94
|
+
block.arity.zero? ? matcher.instance_eval(&block) : yield(matcher)
|
|
95
|
+
matcher.result
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def stub(answers, &)
|
|
99
|
+
Harness.stub(answers, &)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def record(&)
|
|
103
|
+
Harness.record(&)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def replay(tape, &)
|
|
107
|
+
Harness.replay(tape, &)
|
|
60
108
|
end
|
|
61
109
|
|
|
62
110
|
def normalize_threshold(value)
|
|
63
|
-
|
|
111
|
+
normalize_unit(value, "threshold")
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def resolve_definition(predicate, scope: nil, **options)
|
|
115
|
+
case predicate
|
|
116
|
+
when Symbol
|
|
117
|
+
raise ArgumentError, "unknown keyword: #{options.keys.first}" unless options.empty?
|
|
118
|
+
|
|
119
|
+
registry.fetch(predicate, scope: scope) ||
|
|
120
|
+
raise(UndefinedDefinition, undefined_message(predicate, scope))
|
|
121
|
+
when String
|
|
122
|
+
Definition.build(name: :adhoc, instructions: predicate, **adhoc_options(options))
|
|
123
|
+
else
|
|
124
|
+
raise ArgumentError, "predicate must be a Symbol or String"
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def name_for_instructions(instructions)
|
|
129
|
+
registry.name_for_instructions(instructions)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def level_names_for(instructions)
|
|
133
|
+
registry.find_by_instructions(instructions)&.level_names
|
|
134
|
+
end
|
|
135
|
+
private :resolve_definition, :name_for_instructions, :level_names_for
|
|
136
|
+
|
|
137
|
+
private
|
|
138
|
+
|
|
139
|
+
def normalize_unit(value, name)
|
|
140
|
+
raise ArgumentError, "#{name} must be a Float between 0.0 and 1.0" unless value.is_a?(Numeric)
|
|
64
141
|
|
|
65
142
|
value = Float(value)
|
|
66
143
|
unless value.finite? && value.between?(0.0, 1.0)
|
|
67
|
-
raise ArgumentError, "
|
|
144
|
+
raise ArgumentError, "#{name} must be a Float between 0.0 and 1.0"
|
|
68
145
|
end
|
|
69
146
|
|
|
70
147
|
value
|
|
71
148
|
end
|
|
72
149
|
|
|
73
|
-
|
|
150
|
+
def require_adhoc_type(scope_or_predicate, predicate, options, key)
|
|
151
|
+
_scope, name = unpack_predicate(scope_or_predicate, predicate)
|
|
152
|
+
return unless name.is_a?(String)
|
|
153
|
+
raise ArgumentError, "#{key} is required" unless options.key?(key)
|
|
154
|
+
end
|
|
74
155
|
|
|
75
156
|
attr_reader :registry
|
|
76
157
|
|
|
@@ -92,18 +173,6 @@ module Jev
|
|
|
92
173
|
value.is_a?(Module) || value.is_a?(String) || value.is_a?(Symbol)
|
|
93
174
|
end
|
|
94
175
|
|
|
95
|
-
def resolve(predicate, scope: nil)
|
|
96
|
-
case predicate
|
|
97
|
-
when Symbol
|
|
98
|
-
registry.fetch(predicate, scope: scope) ||
|
|
99
|
-
raise(UndefinedDefinition, undefined_message(predicate, scope))
|
|
100
|
-
when String
|
|
101
|
-
predicate
|
|
102
|
-
else
|
|
103
|
-
raise ArgumentError, "predicate must be a Symbol or String"
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
|
-
|
|
107
176
|
def undefined_message(predicate, scope)
|
|
108
177
|
if scope
|
|
109
178
|
"Undefined Jev definition: #{predicate.inspect} for #{scope}"
|
|
@@ -111,8 +180,88 @@ module Jev
|
|
|
111
180
|
"Undefined Jev definition: #{predicate.inspect}"
|
|
112
181
|
end
|
|
113
182
|
end
|
|
183
|
+
|
|
184
|
+
def unpack_define(scope_or_name, name_or_description, description)
|
|
185
|
+
if description.nil?
|
|
186
|
+
if scope_or_name.is_a?(Module) || name_or_description.is_a?(Symbol)
|
|
187
|
+
raise ArgumentError, "description is required"
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
[nil, scope_or_name, name_or_description]
|
|
191
|
+
else
|
|
192
|
+
[scope_or_name, name_or_description, description]
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def adhoc_options(options)
|
|
197
|
+
choices = options.delete(:choices)
|
|
198
|
+
levels = options.delete(:levels)
|
|
199
|
+
raise ArgumentError, "unknown keyword: #{options.keys.first}" unless options.empty?
|
|
200
|
+
|
|
201
|
+
{ choices: choices, levels: levels }
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def measure_one(text, scope_or_predicate, predicate, id: nil, **)
|
|
205
|
+
scope, predicate = unpack_predicate(scope_or_predicate, predicate)
|
|
206
|
+
definition = resolve_definition(predicate, scope: scope, **)
|
|
207
|
+
question_id = id || (predicate.is_a?(Symbol) ? predicate.to_s : definition.type.to_s)
|
|
208
|
+
Client.new(configuration).ask(coerce_text(text), { question_id => definition }).fetch(question_id)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def typed_measure(type, text, scope_or_predicate, predicate, **)
|
|
212
|
+
result = measure_one(text, scope_or_predicate, predicate, **)
|
|
213
|
+
return result if result.type == type
|
|
214
|
+
|
|
215
|
+
label = predicate_label(scope_or_predicate, predicate)
|
|
216
|
+
raise ArgumentError, "#{label} is a #{result.type} definition, not a #{type}"
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def predicate_label(scope_or_predicate, predicate)
|
|
220
|
+
(_scope, name) = unpack_predicate(scope_or_predicate, predicate)
|
|
221
|
+
name.inspect
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def measure_batch(text, scope, &)
|
|
225
|
+
questions = collect_questions(scope, &)
|
|
226
|
+
answers = Client.new(configuration).ask(coerce_text(text), questions.transform_keys(&:to_s))
|
|
227
|
+
Result::Batch.new(questions.keys.to_h { |key| [key, answers.fetch(key.to_s)] })
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def collect_questions(scope)
|
|
231
|
+
query = Query.new(scope)
|
|
232
|
+
yield query
|
|
233
|
+
raise ArgumentError, "measure block must declare at least one question" if query.questions.empty?
|
|
234
|
+
|
|
235
|
+
query.questions
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def validate_noul_decision!(opts)
|
|
239
|
+
unknown = opts.keys - %i[threshold at_least]
|
|
240
|
+
raise ArgumentError, "unknown keyword: #{unknown.first}" unless unknown.empty?
|
|
241
|
+
if opts.key?(:at_least) && opts.key?(:threshold)
|
|
242
|
+
raise ArgumentError,
|
|
243
|
+
"cannot use threshold: and at_least: together"
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
normalize_unit(opts[:at_least], "at_least") if opts.key?(:at_least)
|
|
247
|
+
normalize_threshold(opts[:threshold]) if opts.key?(:threshold)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def decide_noul(probability, opts)
|
|
251
|
+
if opts.key?(:at_least)
|
|
252
|
+
at_least = normalize_unit(opts[:at_least], "at_least")
|
|
253
|
+
return true if probability >= at_least
|
|
254
|
+
return false if probability + at_least <= 1.0
|
|
255
|
+
|
|
256
|
+
nil
|
|
257
|
+
else
|
|
258
|
+
probability >= normalize_threshold(opts.fetch(:threshold, configuration.threshold))
|
|
259
|
+
end
|
|
260
|
+
end
|
|
114
261
|
end
|
|
115
262
|
|
|
263
|
+
private_constant :Query, :Matcher, :Harness
|
|
264
|
+
|
|
116
265
|
reset_configuration!
|
|
117
266
|
@registry = Registry.new
|
|
118
267
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jev-feels
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Maxim Veysgeym
|
|
@@ -11,28 +11,38 @@ cert_chain: []
|
|
|
11
11
|
date: 2026-09-20 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: |
|
|
14
|
-
Tiny Ruby API for semantic text
|
|
15
|
-
|
|
14
|
+
Tiny Ruby API for semantic text decisions. Jev.feels?(:urgent), Jev.decide,
|
|
15
|
+
and Jev.score are ordinary Ruby, not an SDK session. Jev stays an
|
|
16
|
+
implementation detail.
|
|
16
17
|
email:
|
|
17
18
|
- Qew7@users.noreply.github.com
|
|
18
19
|
executables: []
|
|
19
20
|
extensions: []
|
|
20
21
|
extra_rdoc_files: []
|
|
21
22
|
files:
|
|
23
|
+
- CHANGELOG.md
|
|
22
24
|
- LICENSE
|
|
23
25
|
- README.md
|
|
24
26
|
- context7.json
|
|
25
27
|
- lib/feels.rb
|
|
28
|
+
- lib/feels/active_model.rb
|
|
26
29
|
- lib/feels/string.rb
|
|
27
30
|
- lib/jev-feels.rb
|
|
31
|
+
- lib/jev-feels/active_model.rb
|
|
28
32
|
- lib/jev-feels/string.rb
|
|
29
33
|
- lib/jev.rb
|
|
34
|
+
- lib/jev/active_model.rb
|
|
30
35
|
- lib/jev/client.rb
|
|
31
36
|
- lib/jev/configuration.rb
|
|
37
|
+
- lib/jev/definition.rb
|
|
32
38
|
- lib/jev/errors.rb
|
|
33
39
|
- lib/jev/feels.rb
|
|
40
|
+
- lib/jev/harness.rb
|
|
41
|
+
- lib/jev/match.rb
|
|
34
42
|
- lib/jev/model.rb
|
|
43
|
+
- lib/jev/query.rb
|
|
35
44
|
- lib/jev/registry.rb
|
|
45
|
+
- lib/jev/result.rb
|
|
36
46
|
- lib/jev/transport.rb
|
|
37
47
|
- lib/jev/version.rb
|
|
38
48
|
homepage: https://github.com/Qew7/jev-feels
|
|
@@ -41,7 +51,7 @@ licenses:
|
|
|
41
51
|
metadata:
|
|
42
52
|
homepage_uri: https://github.com/Qew7/jev-feels
|
|
43
53
|
source_code_uri: https://github.com/Qew7/jev-feels
|
|
44
|
-
changelog_uri: https://github.com/Qew7/jev-feels/blob/master/
|
|
54
|
+
changelog_uri: https://github.com/Qew7/jev-feels/blob/master/CHANGELOG.md
|
|
45
55
|
allowed_push_host: https://rubygems.org
|
|
46
56
|
post_install_message:
|
|
47
57
|
rdoc_options: []
|