jev-feels 0.1.0 → 1.0.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/README.md +38 -1
- data/context7.json +1 -1
- data/lib/jev/model.rb +51 -0
- data/lib/jev/registry.rb +60 -6
- data/lib/jev/version.rb +1 -1
- data/lib/jev-feels/string.rb +3 -0
- data/lib/jev.rb +46 -13
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e06314aa51799f9434268858022138a8a871b4f58332907b4be0f5d187885073
|
|
4
|
+
data.tar.gz: 8e5e28527de0e411f38ca91b660cb6a43c77a955af2a0780227c9401d4fdfe35
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 11ff5c82f81642c4e60c1c39f4567de4b3d4e4a7c0ab6bd31a4a19be9cca47b5e2642f1b1388b2cc507ab45b563d02e7ea09767f489954ef1b3bb3d57659a798
|
|
7
|
+
data.tar.gz: 184c045587028d305a7fa8310c298687998981b655ded9e71e8c7990cb30bfeeaecac59bed11940ef3903bde5dfd49eb99a78c37e0c7e3a83f5a8274e8692589
|
data/README.md
CHANGED
|
@@ -25,6 +25,37 @@ if Jev.feels?(email, :urgent)
|
|
|
25
25
|
end
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
+
## Ruby on Rails
|
|
29
|
+
|
|
30
|
+
Configure once, declare predicates on the model, ask the record:
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
# Gemfile
|
|
34
|
+
gem "jev-feels"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
# config/initializers/jev.rb
|
|
39
|
+
Jev.configure do |config|
|
|
40
|
+
config.api_key = ENV.fetch("JEV_API_KEY")
|
|
41
|
+
end
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
class SupportEmail < ApplicationRecord
|
|
46
|
+
include Jev::Model
|
|
47
|
+
|
|
48
|
+
feels :body, :urgent, "Outage, customers cannot sign in"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
email.feels?(:urgent)
|
|
52
|
+
email.feels(:urgent)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The class is the scope, the first argument is the field. No model name, no `email.body.feels?`.
|
|
56
|
+
|
|
57
|
+
String sugar is still opt-in (`using Jev::Feels` or `require "jev-feels/string"`) if you want `body.feels?(:urgent)` on a raw string.
|
|
58
|
+
|
|
28
59
|
## String sugar
|
|
29
60
|
|
|
30
61
|
`require "feels"` does not change `String`. Opt in with a refinement:
|
|
@@ -39,6 +70,7 @@ Or, if you really want a global patch:
|
|
|
39
70
|
|
|
40
71
|
```ruby
|
|
41
72
|
require "feels/string"
|
|
73
|
+
# or: require "jev-feels/string"
|
|
42
74
|
|
|
43
75
|
email.feels?(:urgent)
|
|
44
76
|
```
|
|
@@ -77,11 +109,16 @@ Jev.define :urgent, "Requires immediate attention or action"
|
|
|
77
109
|
Jev.define :spam, "Unsolicited or unwanted promotional content"
|
|
78
110
|
Jev.define :angry, "Expresses anger or hostility"
|
|
79
111
|
|
|
112
|
+
Jev.define Email, :urgent, "Outage, customers cannot sign in"
|
|
113
|
+
Jev.define Comment, :urgent, "Legal takedown or self-harm"
|
|
114
|
+
|
|
80
115
|
Jev.definition(:urgent)
|
|
116
|
+
Jev.definition(Email, :urgent)
|
|
81
117
|
Jev.definitions
|
|
118
|
+
Jev.definitions(Comment)
|
|
82
119
|
```
|
|
83
120
|
|
|
84
|
-
`define` replaces an existing name. `definitions` returns a frozen copy
|
|
121
|
+
`define` replaces an existing name in that scope. Scopes are stored by class name, so `Email`, `"Email"` and `:Email` are the same key. `include Jev::Model` plus `feels :body, :urgent, "..."` does the same define and remembers the field. `Jev.feels?(text, Email, :urgent)` uses Email's definition, then a superclass, then the global `:urgent`. `definitions` returns a frozen copy.
|
|
85
122
|
|
|
86
123
|
## Configuration
|
|
87
124
|
|
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)
|
|
4
|
+
"description": "Ruby-like semantic text predicates via Jev. Jev.feels?(:urgent) and model#feels?(:urgent) are ordinary conditions, not an SDK session.",
|
|
5
5
|
"excludeFolders": ["spec"]
|
|
6
6
|
}
|
data/lib/jev/model.rb
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jev
|
|
4
|
+
module Model
|
|
5
|
+
def self.included(base)
|
|
6
|
+
base.extend ClassMethods
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
module ClassMethods
|
|
10
|
+
def inherited(subclass)
|
|
11
|
+
super
|
|
12
|
+
subclass.extend ClassMethods
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def feels(attribute, name, description)
|
|
16
|
+
raise ArgumentError, "attribute must be a Symbol" unless attribute.is_a?(Symbol)
|
|
17
|
+
|
|
18
|
+
Jev.define(self, name, description)
|
|
19
|
+
(@jev_feels_attributes ||= {})[name.to_sym] = attribute
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def jev_feels_attribute(name)
|
|
23
|
+
current = self
|
|
24
|
+
while current && current != Object
|
|
25
|
+
found = current.instance_variable_get(:@jev_feels_attributes)&.[](name)
|
|
26
|
+
return found if found
|
|
27
|
+
|
|
28
|
+
current = current.superclass
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def feels(predicate)
|
|
34
|
+
Jev.feels(jev_text_for(predicate), self.class, predicate)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def feels?(predicate, threshold: Jev.configuration.threshold)
|
|
38
|
+
Jev.feels?(jev_text_for(predicate), self.class, predicate, threshold: threshold)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def jev_text_for(predicate)
|
|
44
|
+
attribute = self.class.jev_feels_attribute(predicate.to_sym) || raise(
|
|
45
|
+
UndefinedDefinition, "Undefined Jev definition: #{predicate.inspect} for #{self.class}"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
public_send(attribute).to_s
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
data/lib/jev/registry.rb
CHANGED
|
@@ -7,23 +7,77 @@ module Jev
|
|
|
7
7
|
@definitions = {}
|
|
8
8
|
end
|
|
9
9
|
|
|
10
|
-
def define(name, description)
|
|
10
|
+
def define(scope, name, description)
|
|
11
11
|
key = name.to_sym
|
|
12
12
|
value = description.to_s.dup.freeze
|
|
13
|
-
|
|
13
|
+
scope_key = storage_key(scope)
|
|
14
|
+
@mutex.synchronize do
|
|
15
|
+
(@definitions[scope_key] ||= {})[key] = value
|
|
16
|
+
end
|
|
14
17
|
value
|
|
15
18
|
end
|
|
16
19
|
|
|
17
|
-
def fetch(name)
|
|
18
|
-
|
|
20
|
+
def fetch(name, scope: nil, fallback: true, inherit: true)
|
|
21
|
+
key = name.to_sym
|
|
22
|
+
@mutex.synchronize do
|
|
23
|
+
return @definitions.dig(nil, key) if scope.nil?
|
|
24
|
+
|
|
25
|
+
lookup_keys(scope, inherit: inherit).each do |scope_key|
|
|
26
|
+
found = @definitions.dig(scope_key, key)
|
|
27
|
+
return found if found
|
|
28
|
+
end
|
|
29
|
+
return @definitions.dig(nil, key) if fallback
|
|
30
|
+
|
|
31
|
+
nil
|
|
32
|
+
end
|
|
19
33
|
end
|
|
20
34
|
|
|
21
|
-
def all
|
|
22
|
-
@mutex.synchronize { @definitions.dup.freeze }
|
|
35
|
+
def all(scope = nil)
|
|
36
|
+
@mutex.synchronize { (@definitions[storage_key(scope)] || {}).dup.freeze }
|
|
23
37
|
end
|
|
24
38
|
|
|
25
39
|
def reset!
|
|
26
40
|
@mutex.synchronize { @definitions.clear }
|
|
27
41
|
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def storage_key(scope)
|
|
46
|
+
return if scope.nil?
|
|
47
|
+
|
|
48
|
+
key =
|
|
49
|
+
case scope
|
|
50
|
+
when String, Symbol then scope.to_s
|
|
51
|
+
when Module then scope.name || scope
|
|
52
|
+
else raise ArgumentError, "scope must be a Module, String, or Symbol"
|
|
53
|
+
end
|
|
54
|
+
raise ArgumentError, "scope must be a non-empty String" if key.is_a?(String) && key.empty?
|
|
55
|
+
|
|
56
|
+
key
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def lookup_keys(scope, inherit:)
|
|
60
|
+
keys = [storage_key(scope)]
|
|
61
|
+
return keys unless inherit
|
|
62
|
+
|
|
63
|
+
klass = scope.is_a?(Class) ? scope : constantize(scope)
|
|
64
|
+
return keys unless klass.is_a?(Class)
|
|
65
|
+
|
|
66
|
+
current = klass.superclass
|
|
67
|
+
while current && current != Object
|
|
68
|
+
keys << (current.name || current)
|
|
69
|
+
current = current.superclass
|
|
70
|
+
end
|
|
71
|
+
keys.uniq
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def constantize(name)
|
|
75
|
+
name = name.to_s if name.is_a?(Symbol)
|
|
76
|
+
return unless name.is_a?(String)
|
|
77
|
+
|
|
78
|
+
name.split("::").reduce(Object) { |mod, part| mod.const_get(part, false) }
|
|
79
|
+
rescue NameError
|
|
80
|
+
nil
|
|
81
|
+
end
|
|
28
82
|
end
|
|
29
83
|
end
|
data/lib/jev/version.rb
CHANGED
data/lib/jev.rb
CHANGED
|
@@ -7,6 +7,7 @@ require_relative "jev/registry"
|
|
|
7
7
|
require_relative "jev/transport"
|
|
8
8
|
require_relative "jev/client"
|
|
9
9
|
require_relative "jev/feels"
|
|
10
|
+
require_relative "jev/model"
|
|
10
11
|
|
|
11
12
|
module Jev
|
|
12
13
|
class << self
|
|
@@ -21,28 +22,41 @@ module Jev
|
|
|
21
22
|
@configuration = Configuration.new
|
|
22
23
|
end
|
|
23
24
|
|
|
24
|
-
def define(
|
|
25
|
-
|
|
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
|
|
26
35
|
end
|
|
27
36
|
|
|
28
|
-
def definition(name)
|
|
29
|
-
|
|
37
|
+
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
|
|
30
43
|
end
|
|
31
44
|
|
|
32
|
-
def definitions
|
|
33
|
-
registry.all
|
|
45
|
+
def definitions(scope = nil)
|
|
46
|
+
registry.all(scope)
|
|
34
47
|
end
|
|
35
48
|
|
|
36
49
|
def reset_definitions!
|
|
37
50
|
registry.reset!
|
|
38
51
|
end
|
|
39
52
|
|
|
40
|
-
def feels(text, predicate)
|
|
41
|
-
|
|
53
|
+
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))
|
|
42
56
|
end
|
|
43
57
|
|
|
44
|
-
def feels?(text, predicate, threshold: configuration.threshold)
|
|
45
|
-
feels(text, predicate) >= normalize_threshold(threshold)
|
|
58
|
+
def feels?(text, scope_or_predicate, predicate = nil, threshold: configuration.threshold)
|
|
59
|
+
feels(text, scope_or_predicate, predicate) >= normalize_threshold(threshold)
|
|
46
60
|
end
|
|
47
61
|
|
|
48
62
|
def normalize_threshold(value)
|
|
@@ -67,17 +81,36 @@ module Jev
|
|
|
67
81
|
text
|
|
68
82
|
end
|
|
69
83
|
|
|
70
|
-
def
|
|
84
|
+
def unpack_predicate(scope_or_predicate, predicate)
|
|
85
|
+
return [nil, scope_or_predicate] if predicate.nil?
|
|
86
|
+
raise ArgumentError, "scope must be a Module, String, or Symbol" unless scoped?(scope_or_predicate)
|
|
87
|
+
|
|
88
|
+
[scope_or_predicate, predicate]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def scoped?(value)
|
|
92
|
+
value.is_a?(Module) || value.is_a?(String) || value.is_a?(Symbol)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def resolve(predicate, scope: nil)
|
|
71
96
|
case predicate
|
|
72
97
|
when Symbol
|
|
73
|
-
|
|
74
|
-
raise(UndefinedDefinition,
|
|
98
|
+
registry.fetch(predicate, scope: scope) ||
|
|
99
|
+
raise(UndefinedDefinition, undefined_message(predicate, scope))
|
|
75
100
|
when String
|
|
76
101
|
predicate
|
|
77
102
|
else
|
|
78
103
|
raise ArgumentError, "predicate must be a Symbol or String"
|
|
79
104
|
end
|
|
80
105
|
end
|
|
106
|
+
|
|
107
|
+
def undefined_message(predicate, scope)
|
|
108
|
+
if scope
|
|
109
|
+
"Undefined Jev definition: #{predicate.inspect} for #{scope}"
|
|
110
|
+
else
|
|
111
|
+
"Undefined Jev definition: #{predicate.inspect}"
|
|
112
|
+
end
|
|
113
|
+
end
|
|
81
114
|
end
|
|
82
115
|
|
|
83
116
|
reset_configuration!
|
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:
|
|
4
|
+
version: 1.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Maxim Veysgeym
|
|
@@ -25,11 +25,13 @@ files:
|
|
|
25
25
|
- lib/feels.rb
|
|
26
26
|
- lib/feels/string.rb
|
|
27
27
|
- lib/jev-feels.rb
|
|
28
|
+
- lib/jev-feels/string.rb
|
|
28
29
|
- lib/jev.rb
|
|
29
30
|
- lib/jev/client.rb
|
|
30
31
|
- lib/jev/configuration.rb
|
|
31
32
|
- lib/jev/errors.rb
|
|
32
33
|
- lib/jev/feels.rb
|
|
34
|
+
- lib/jev/model.rb
|
|
33
35
|
- lib/jev/registry.rb
|
|
34
36
|
- lib/jev/transport.rb
|
|
35
37
|
- lib/jev/version.rb
|
|
@@ -39,7 +41,7 @@ licenses:
|
|
|
39
41
|
metadata:
|
|
40
42
|
homepage_uri: https://github.com/Qew7/jev-feels
|
|
41
43
|
source_code_uri: https://github.com/Qew7/jev-feels
|
|
42
|
-
changelog_uri: https://github.com/Qew7/jev-feels/blob/
|
|
44
|
+
changelog_uri: https://github.com/Qew7/jev-feels/blob/master/README.md
|
|
43
45
|
allowed_push_host: https://rubygems.org
|
|
44
46
|
post_install_message:
|
|
45
47
|
rdoc_options: []
|