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.
data/lib/jev/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Jev
4
- VERSION = "1.0.0"
4
+ VERSION = "1.1.0"
5
5
  end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "feels/active_model"
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
- if description.nil?
27
- if scope_or_name.is_a?(Module) || name_or_description.is_a?(Symbol)
28
- raise ArgumentError, "description is required"
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
- 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
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
- scope, predicate = unpack_predicate(scope_or_predicate, predicate)
55
- Client.new(configuration).probability(coerce_text(text), resolve(predicate, scope: scope))
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 feels?(text, scope_or_predicate, predicate = nil, threshold: configuration.threshold)
59
- feels(text, scope_or_predicate, predicate) >= normalize_threshold(threshold)
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
- raise ArgumentError, "threshold must be a Float between 0.0 and 1.0" unless value.is_a?(Numeric)
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, "threshold must be a Float between 0.0 and 1.0"
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
- private
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.0.0
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 checks. Jev.feels?(:urgent) is an ordinary
15
- condition, not an SDK session. Jev stays an implementation detail.
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/README.md
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: []