natter 0.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 +7 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +39 -0
- data/Rakefile +2 -0
- data/lib/natter/entity.rb +40 -0
- data/lib/natter/intent.rb +28 -0
- data/lib/natter/parser.rb +306 -0
- data/lib/natter/rule.rb +93 -0
- data/lib/natter/version.rb +3 -0
- data/lib/natter.rb +16 -0
- data/natter.gemspec +29 -0
- metadata +111 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: b2654979a551d969bac208c18667ce2c7c74d7fc
|
|
4
|
+
data.tar.gz: 5726246cb0c3fdde38783f6e4d77f97a46a3cc7c
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 89a96014d6139600ed0728d18dc030dc5f753e2e1a4676cd7dd522684b0fd0b11785c84661b192ba8cf69cfff84bbc9ec888b55983396e711fafb4179e70b6dd
|
|
7
|
+
data.tar.gz: f6fd7870a3f35019341f6403791fc96c1a94c35c34d95ed0d3e3a099d2cd41d1ca9fb3b0573bc075821ef063d5778046c2cdf9445ad57dd1d5a82baf46a3d0cb
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2017 Garry Pettet
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Natter
|
|
2
|
+
|
|
3
|
+
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/natter`. To experiment with that code, run `bin/console` for an interactive prompt.
|
|
4
|
+
|
|
5
|
+
TODO: Delete this and the text above, and describe your gem
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Add this line to your application's Gemfile:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem 'natter'
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
And then execute:
|
|
16
|
+
|
|
17
|
+
$ bundle
|
|
18
|
+
|
|
19
|
+
Or install it yourself as:
|
|
20
|
+
|
|
21
|
+
$ gem install natter
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
TODO: Write usage instructions here
|
|
26
|
+
|
|
27
|
+
## Development
|
|
28
|
+
|
|
29
|
+
After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
30
|
+
|
|
31
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
32
|
+
|
|
33
|
+
## Contributing
|
|
34
|
+
|
|
35
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/natter.
|
|
36
|
+
|
|
37
|
+
## License
|
|
38
|
+
|
|
39
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module Natter
|
|
2
|
+
# Public: An entity is an attribute of an intent.
|
|
3
|
+
class Entity
|
|
4
|
+
attr_accessor :name, :type
|
|
5
|
+
attr_reader :value
|
|
6
|
+
|
|
7
|
+
# Public: Constructor.
|
|
8
|
+
#
|
|
9
|
+
# name - Must be unique within an individual intent.
|
|
10
|
+
# type - One of the pre-defined EntityType constants (default: 'generic')
|
|
11
|
+
# value - Will never be nil when generated by the parser but may be nil
|
|
12
|
+
# for some rule definitions (default: nil).
|
|
13
|
+
def initialize(name, type = EntityType::GENERIC, value = nil)
|
|
14
|
+
@name = name
|
|
15
|
+
@type = type
|
|
16
|
+
@value = value
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Public: Returns the value of this entity.
|
|
20
|
+
# If @type is `generic` then we just return @value, otherwise we will need
|
|
21
|
+
# to verify/compute the value to return.
|
|
22
|
+
#
|
|
23
|
+
# Returns object or nil (if invalid @value)
|
|
24
|
+
def value
|
|
25
|
+
case @type
|
|
26
|
+
when EntityType::TIME
|
|
27
|
+
return Chronic.parse(@value) # returns Time or nil
|
|
28
|
+
else
|
|
29
|
+
return @value
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Public: Exists only to expose its constants which are used to define
|
|
35
|
+
# Entity types.
|
|
36
|
+
class EntityType
|
|
37
|
+
GENERIC = 'generic'
|
|
38
|
+
TIME = 'time'
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module Natter
|
|
2
|
+
# Public: Represents an intent derived by the parser from an utterance.
|
|
3
|
+
class Intent
|
|
4
|
+
# The string name of this intent.
|
|
5
|
+
attr_accessor :name
|
|
6
|
+
# The name of the skill that this intent belongs to.
|
|
7
|
+
attr_accessor :skill
|
|
8
|
+
# How confident the parser is that this is the correct intent (from 0.0 - 1.0).
|
|
9
|
+
attr_accessor :confidence
|
|
10
|
+
# An array of entities discovered in the utterance. May be empty.
|
|
11
|
+
attr_accessor :entities
|
|
12
|
+
|
|
13
|
+
# Public: Constructor.
|
|
14
|
+
#
|
|
15
|
+
# name - The String name of this intent. Does not have to be unique.
|
|
16
|
+
# skill - The name of the skill that this intent belongs to. Defaults
|
|
17
|
+
# to the global namespace/skill (default: '')
|
|
18
|
+
# confidence - Float value between 0-1 reflects how confident the parser is
|
|
19
|
+
# that this intent is correct (default: 1.0).
|
|
20
|
+
# entities - An array of Natter::Entity objects (default: nil).
|
|
21
|
+
def initialize(name, skill = '', confidence = 1.0, entities = nil)
|
|
22
|
+
@name = name
|
|
23
|
+
@skill = skill
|
|
24
|
+
@confidence = confidence
|
|
25
|
+
@entities = entities || []
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
module Natter
|
|
2
|
+
# Public: The parser is the main workhorse, responsible for deriving the
|
|
3
|
+
# intent from an utterance.
|
|
4
|
+
class Parser
|
|
5
|
+
def initialize
|
|
6
|
+
@known_utterances = Hash.new # key = utterance, value = Intent
|
|
7
|
+
@contractions = init_contractions # key = contraction, value = expansion
|
|
8
|
+
@intent_cache = Hash.new # key = utterance, value = Intent
|
|
9
|
+
@rules = Hash.new # key = rule regex pattern, value = Rule object
|
|
10
|
+
end
|
|
11
|
+
# Public: Adds a regex-based Rule to the parser.
|
|
12
|
+
#
|
|
13
|
+
# rule - The Natter::Rule to add.
|
|
14
|
+
def add_rule(rule)
|
|
15
|
+
raise ArgumentError, "Expected Natter::Rule but got `#{rule}`" unless rule.is_a?(Rule)
|
|
16
|
+
if @rules.has_key?(rule.pattern)
|
|
17
|
+
raise ArgumentError, "Regex pattern already defined by " +\
|
|
18
|
+
"#{@rules[rule.pattern].identifier}: #{rule.pattern}"
|
|
19
|
+
end
|
|
20
|
+
@rules[rule.pattern] = rule
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Public: Adds a pre-computed utterance/intent pair to the parser.
|
|
24
|
+
# Used when a specific utterance(s) match a predetermined intent. This saves
|
|
25
|
+
# overhead as there is no regex processing required. These utterances are
|
|
26
|
+
# evaluated before the regex rules.
|
|
27
|
+
# Multiple examples can be added at once.
|
|
28
|
+
# Adding an utterance that already exists will overwrite the old one.
|
|
29
|
+
#
|
|
30
|
+
# example - A Hash where:
|
|
31
|
+
# key = A single utterance or array of utterances
|
|
32
|
+
# value = Natter::Intent
|
|
33
|
+
#
|
|
34
|
+
# Examples
|
|
35
|
+
#
|
|
36
|
+
# add_utterance('hello' => Intent.new('greeting'))
|
|
37
|
+
# add_utterance(['what time is it', 'what is the time'] => Intent.new('currentTime'))
|
|
38
|
+
# add_utterance(
|
|
39
|
+
# 'night night' => Intent.new('goodnight'),
|
|
40
|
+
# 'lock the door' => Intent.new('lock')
|
|
41
|
+
# )
|
|
42
|
+
#
|
|
43
|
+
# Returns nothing.
|
|
44
|
+
def add_utterance(example)
|
|
45
|
+
raise ArgumentError, "Expected {utterance => Intent} or {[utterances] => Intent}" unless example.is_a?(Hash)
|
|
46
|
+
example.map do |utterance, intent|
|
|
47
|
+
if utterance.kind_of?(Array)
|
|
48
|
+
utterance.each { |phrase| @known_utterances[phrase] = intent }
|
|
49
|
+
else
|
|
50
|
+
@known_utterances[utterance] = intent
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Public: Analyse an utterance and return any matching intents.
|
|
56
|
+
#
|
|
57
|
+
# utterance - The natural language string to analyse
|
|
58
|
+
# use_cache - If true then we will check a cache of previously returned
|
|
59
|
+
# utterance/intent pairs to return rather than re-parsing.
|
|
60
|
+
# (default: true)
|
|
61
|
+
#
|
|
62
|
+
# Returns an Intent, an array of Intents or nil if the intent cannot be
|
|
63
|
+
# determined.
|
|
64
|
+
def parse(text, use_cache = true)
|
|
65
|
+
raise ArgumentError, "Cannot parse thin air!" unless text.length > 0
|
|
66
|
+
|
|
67
|
+
# Store the original string for later
|
|
68
|
+
original = text
|
|
69
|
+
|
|
70
|
+
# Tidy up the string for parsing
|
|
71
|
+
utterance = purify(original)
|
|
72
|
+
|
|
73
|
+
if @known_utterances.has_key?(utterance)
|
|
74
|
+
return @known_utterances[utterance]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
if use_cache && @intent_cache.has_key?(utterance)
|
|
78
|
+
return @intent_cache[utterance]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
intents = []
|
|
82
|
+
@rules.each do |pattern, rule|
|
|
83
|
+
m = utterance.match(rule.pattern)
|
|
84
|
+
if m == nil
|
|
85
|
+
next
|
|
86
|
+
else
|
|
87
|
+
intent = intent_from_match(rule, m)
|
|
88
|
+
if intent then intents << intent end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
if intents.empty? then return nil end
|
|
93
|
+
|
|
94
|
+
# Calculate the confidence of each intent
|
|
95
|
+
intents = determine_confidences(intents)
|
|
96
|
+
|
|
97
|
+
# Cache the matches
|
|
98
|
+
@intent_cache[utterance] = intents
|
|
99
|
+
|
|
100
|
+
return intents
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Internal: Determines the confidence of each intent in the passed array
|
|
104
|
+
# and then sorts them based on the calculated confidence values.
|
|
105
|
+
# Basically, if we have more than one intent then whichever intent has the
|
|
106
|
+
# greatest number of entities is likely to be the best match.
|
|
107
|
+
#
|
|
108
|
+
# intents - An array of Intent objects.
|
|
109
|
+
#
|
|
110
|
+
# Returns a sorted (by confidence) array of Intent objects. Mutates original
|
|
111
|
+
# array.
|
|
112
|
+
def determine_confidences(intents)
|
|
113
|
+
# Handle where there's only one matching intent
|
|
114
|
+
if intents.length == 1
|
|
115
|
+
intents[0].confidence = 1.0
|
|
116
|
+
return intents
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# First determine the total number of entities in any of the intents
|
|
120
|
+
total = 0
|
|
121
|
+
intents.each { |i| total += i.entities.length }
|
|
122
|
+
|
|
123
|
+
if total == 0
|
|
124
|
+
# Edge case: all matching intents contain no entities.
|
|
125
|
+
# Assign equal confidence to all intents
|
|
126
|
+
result = intents.map do |i|
|
|
127
|
+
i.confidence = 1.0/intents.length
|
|
128
|
+
i # return this intent from the map
|
|
129
|
+
end
|
|
130
|
+
else
|
|
131
|
+
result = intents.map do |i|
|
|
132
|
+
i.confidence = i.entities.length.to_f/total
|
|
133
|
+
i # return this intent from the map
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Sort the array by descending confidence values
|
|
138
|
+
result.sort_by { |i| i.confidence }.reverse
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Internal: Converts a positive regex match and returns an Intent object.
|
|
142
|
+
# Note that the confidence is set to 0 as it will be determined later.
|
|
143
|
+
#
|
|
144
|
+
# rule - The Rule definining this intent.
|
|
145
|
+
# m - The positive regex match.
|
|
146
|
+
#
|
|
147
|
+
# Returns Intent.
|
|
148
|
+
def intent_from_match(rule, m)
|
|
149
|
+
if m.named_captures.empty?
|
|
150
|
+
# No capture groups found. Double-check the rule doesn't need any entities
|
|
151
|
+
if rule.entities.empty?
|
|
152
|
+
return Intent.new(rule.name, rule.skill, 0)
|
|
153
|
+
else
|
|
154
|
+
# Expected at least one entity. This can't be a valid match then
|
|
155
|
+
return nil
|
|
156
|
+
end
|
|
157
|
+
else
|
|
158
|
+
# Found some entities. Check they match up with the rule
|
|
159
|
+
intent = Intent.new(rule.name, rule.skill, 0)
|
|
160
|
+
rule.entities.each do |entity|
|
|
161
|
+
if m.named_captures.has_key?(entity.name)
|
|
162
|
+
e = Entity.new(entity.name, entity.type, m.named_captures[entity.name].strip)
|
|
163
|
+
intent.entities << e
|
|
164
|
+
else
|
|
165
|
+
# Found a named capture group that doesn't match an entity defined
|
|
166
|
+
# in the rule
|
|
167
|
+
return nil
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
if intent.entities.length != m.named_captures.length
|
|
171
|
+
# Found some entity matches but not all
|
|
172
|
+
return nil
|
|
173
|
+
else
|
|
174
|
+
return intent
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Internal: Tidies up the passed string to remove unnecessary characters
|
|
180
|
+
# and replace ambiguous phrases such as contractions.
|
|
181
|
+
#
|
|
182
|
+
# t - The string to purify.
|
|
183
|
+
#
|
|
184
|
+
# Examples
|
|
185
|
+
#
|
|
186
|
+
# str = "what're you doing?!"
|
|
187
|
+
# str = purify(str)
|
|
188
|
+
# # => "what are you doing"
|
|
189
|
+
def purify(t)
|
|
190
|
+
t = expand_contractions(t)
|
|
191
|
+
t = strip_trailing_punctuation(t)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Internal: Removes trailing '?' and '!' from the passed string.
|
|
195
|
+
#
|
|
196
|
+
# t - The string from which to remove superfluous trailing punctuation.
|
|
197
|
+
def strip_trailing_punctuation(t)
|
|
198
|
+
t.sub(/[?!]+\z/, '')
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Private: Initialise the @contractions Hash. Only needs doing once.
|
|
202
|
+
# OPTIMISE: Perhaps move these values to an editable text file?
|
|
203
|
+
def init_contractions
|
|
204
|
+
{
|
|
205
|
+
"that's" => "that is",
|
|
206
|
+
"aren't" => "are not",
|
|
207
|
+
"can't" => "can not",
|
|
208
|
+
"could've" => "could have",
|
|
209
|
+
"couldn't" => "could not",
|
|
210
|
+
"didn't" => "did not",
|
|
211
|
+
"doesn't" => "does not",
|
|
212
|
+
"don't" => "do not",
|
|
213
|
+
"dunno" => "do not know",
|
|
214
|
+
"gonna" => "going to",
|
|
215
|
+
"gotta" => "got to",
|
|
216
|
+
"hadn't" => "had not",
|
|
217
|
+
"hasn't" => "has not",
|
|
218
|
+
"haven't" => "have not",
|
|
219
|
+
"he'd" => "he had",
|
|
220
|
+
"he'll" => "he will",
|
|
221
|
+
"he's" => "he is",
|
|
222
|
+
"how'd" => "how would",
|
|
223
|
+
"how'll" => "how will",
|
|
224
|
+
"how're" => "how are",
|
|
225
|
+
"how's" => "how is",
|
|
226
|
+
"i'd" => "i would",
|
|
227
|
+
"i'll" => "i will",
|
|
228
|
+
"i'm" => "i am",
|
|
229
|
+
"i've" => "i have",
|
|
230
|
+
"isn't" => "is not",
|
|
231
|
+
"it'd" => "it would",
|
|
232
|
+
"it'll" => "it will",
|
|
233
|
+
"it's" => "it is",
|
|
234
|
+
"mightn't" => "might not",
|
|
235
|
+
"might've" => "might have",
|
|
236
|
+
"mustn't" => "must not",
|
|
237
|
+
"must've" => "must have",
|
|
238
|
+
"ol'" => "old",
|
|
239
|
+
"oughtn't" => "ought not",
|
|
240
|
+
"shan't" => "shall not",
|
|
241
|
+
"she'd" => "she would",
|
|
242
|
+
"she'll" => "she will",
|
|
243
|
+
"she's" => "she is",
|
|
244
|
+
"should've" => "should have",
|
|
245
|
+
"shouldn't" => "should not",
|
|
246
|
+
"somebody's" => "somebody is",
|
|
247
|
+
"someone'll" => "someone will",
|
|
248
|
+
"someone's" => "someone is",
|
|
249
|
+
"something'll" => "something will",
|
|
250
|
+
"something's" => "something is",
|
|
251
|
+
"that'll" => "that will",
|
|
252
|
+
"that'd" => "that would",
|
|
253
|
+
"there'd" => "there had",
|
|
254
|
+
"there's" => "there is",
|
|
255
|
+
"they'd" => "they would",
|
|
256
|
+
"they'll" => "they will",
|
|
257
|
+
"they're" => "they are",
|
|
258
|
+
"they've" => "they have",
|
|
259
|
+
"wasn't" => "was not",
|
|
260
|
+
"we'd" => "we had",
|
|
261
|
+
"we'll" => "we will",
|
|
262
|
+
"we're" => "we are",
|
|
263
|
+
"we've" => "we have",
|
|
264
|
+
"weren't" => "were not",
|
|
265
|
+
"what'd" => "what did",
|
|
266
|
+
"what'll" => "what will",
|
|
267
|
+
"what're" => "what are",
|
|
268
|
+
"what's" => "what is",
|
|
269
|
+
"what've" => "what have",
|
|
270
|
+
"when's" => "when is",
|
|
271
|
+
"where'd" => "where did",
|
|
272
|
+
"where's" => "where is",
|
|
273
|
+
"where've" => "where have",
|
|
274
|
+
"who'd" => "who would",
|
|
275
|
+
"who'll" => "who will",
|
|
276
|
+
"who's" => "who is",
|
|
277
|
+
"why'd" => "why did",
|
|
278
|
+
"why're" => "why are",
|
|
279
|
+
"why's" => "why is",
|
|
280
|
+
"won't" => "will not",
|
|
281
|
+
"won't've" => "will not have",
|
|
282
|
+
"would've" => "would have",
|
|
283
|
+
"wouldn't" => "would not",
|
|
284
|
+
"you'd" => "you would",
|
|
285
|
+
"you'll" => "you will",
|
|
286
|
+
"you're" => "you are",
|
|
287
|
+
"you've" => "you have"
|
|
288
|
+
}
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Expand the contractions within this string.
|
|
292
|
+
#
|
|
293
|
+
# Examples
|
|
294
|
+
#
|
|
295
|
+
# t = "I'm hot"
|
|
296
|
+
# t.expand_contractions!
|
|
297
|
+
# # => "I am hot"
|
|
298
|
+
def expand_contractions(text)
|
|
299
|
+
result = ''
|
|
300
|
+
text.strip.split(' ').each do |word|
|
|
301
|
+
result = result + @contractions.fetch(word, word) + ' '
|
|
302
|
+
end
|
|
303
|
+
return result.strip
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
end
|
data/lib/natter/rule.rb
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
module Natter
|
|
2
|
+
|
|
3
|
+
# Public: The Rule class represents a rule which defines an in intent that
|
|
4
|
+
# occurs within an utterance.
|
|
5
|
+
|
|
6
|
+
class Rule
|
|
7
|
+
|
|
8
|
+
# The string name of this rule. Defferent rules can share the same name.
|
|
9
|
+
attr_accessor :name
|
|
10
|
+
# The regex pattern to search for within the an utterance.
|
|
11
|
+
attr_accessor :pattern
|
|
12
|
+
# An array of Entity objects this rule expects. May be empty.
|
|
13
|
+
attr_accessor :entities
|
|
14
|
+
# The name of the skill that this rule belongs to. May be '' if global.
|
|
15
|
+
attr_accessor :skill
|
|
16
|
+
|
|
17
|
+
# Public: Constructor.
|
|
18
|
+
#
|
|
19
|
+
# name - The name of this intent. Names do not need to be unique
|
|
20
|
+
# within a parser.
|
|
21
|
+
# pattern - The regular expression that matches the contents of an
|
|
22
|
+
# utterance. If this intent contains entities then the regex
|
|
23
|
+
# must capture the entities within the utterance as named
|
|
24
|
+
# capture groups. See examples below. Default: //
|
|
25
|
+
# skill - The name of the skill that this rule belongs to. If none is
|
|
26
|
+
# specified then we'll assume it's a global rule (default: '').
|
|
27
|
+
# entities - Optional array of Entity objects in the form:
|
|
28
|
+
# Each entity must have a correspondingly named capture group
|
|
29
|
+
# within the regex.
|
|
30
|
+
def initialize(name, pattern = //, skill = '', *entities)
|
|
31
|
+
@name = name
|
|
32
|
+
@pattern = pattern
|
|
33
|
+
@skill = skill
|
|
34
|
+
@entities = entities || []
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Public: A convenience method to permit prettier building of rules.
|
|
38
|
+
# Returns the Rule back to the calling method to allow chaining.
|
|
39
|
+
#
|
|
40
|
+
# pattern - The regex pattern to assign to this rule
|
|
41
|
+
#
|
|
42
|
+
# Examples
|
|
43
|
+
#
|
|
44
|
+
# definition = Rule.new('reminder').regex(/remind me to/i)
|
|
45
|
+
# definition = Rule.new('reminder')\
|
|
46
|
+
# .regex(/remind me/)\
|
|
47
|
+
# .needs_entity('task', EntityType::GENERIC)
|
|
48
|
+
def regex(pattern)
|
|
49
|
+
@pattern = pattern
|
|
50
|
+
return self
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Public: Returns this rule's identifier. An identifier is in the format:
|
|
54
|
+
# skill.name unless skill = '' in which case we return global.name
|
|
55
|
+
#
|
|
56
|
+
# Returns string
|
|
57
|
+
def identifier
|
|
58
|
+
"#{@skill == '' ? 'global' : @skill}.#{@name}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Public: A convenience method to permit prettier building of rules.
|
|
62
|
+
# Returns the Rule back to the calling method to allowing chaining.
|
|
63
|
+
#
|
|
64
|
+
# skill - The name of the skill that this this rule belongs to.
|
|
65
|
+
#
|
|
66
|
+
# Example
|
|
67
|
+
#
|
|
68
|
+
# def = Rule.new('play').belongs_to('sonos')
|
|
69
|
+
def belongs_to(skill)
|
|
70
|
+
@skill = skill
|
|
71
|
+
return self
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Public: A nicer syntax method for adding an Entity to this rule.
|
|
75
|
+
#
|
|
76
|
+
# name - The name of this entity. Must be unique within this rule definition
|
|
77
|
+
# and must match a named capture group within the definition's regex.
|
|
78
|
+
#
|
|
79
|
+
# type - The entity's type. Should be one of the predefined constants in
|
|
80
|
+
# Natter::EntityType
|
|
81
|
+
#
|
|
82
|
+
# Example
|
|
83
|
+
#
|
|
84
|
+
# definition.needs_entity('artist', EntityType::GENERIC)
|
|
85
|
+
def needs_entity(name, type)
|
|
86
|
+
@entities.each do |entity|
|
|
87
|
+
return if entity.name == name # this rule already contains this entity
|
|
88
|
+
end
|
|
89
|
+
@entities << Entity.new(name, type)
|
|
90
|
+
return self
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
data/lib/natter.rb
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Natter is a rules-based natural language intent parser.
|
|
2
|
+
# Loads the files required by Natter.
|
|
3
|
+
|
|
4
|
+
# Third party dependencies
|
|
5
|
+
require 'chronic'
|
|
6
|
+
|
|
7
|
+
# # Modules/classes
|
|
8
|
+
require_relative 'natter/entity'
|
|
9
|
+
require_relative 'natter/intent'
|
|
10
|
+
require_relative 'natter/parser'
|
|
11
|
+
require_relative 'natter/rule'
|
|
12
|
+
require_relative 'natter/version'
|
|
13
|
+
|
|
14
|
+
module Natter
|
|
15
|
+
|
|
16
|
+
end
|
data/natter.gemspec
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
|
+
require "natter/version"
|
|
5
|
+
|
|
6
|
+
Gem::Specification.new do |spec|
|
|
7
|
+
spec.name = "natter"
|
|
8
|
+
spec.version = Natter::VERSION
|
|
9
|
+
spec.authors = ["Garry Pettet"]
|
|
10
|
+
spec.email = ["contact@garrypettet.com"]
|
|
11
|
+
|
|
12
|
+
spec.summary = 'Rules-based natural language intent parser.'
|
|
13
|
+
spec.description = 'A rules-based natural language intent parser written in pure Ruby.'
|
|
14
|
+
spec.homepage = 'https://github.com/BarnabyAI/natter'
|
|
15
|
+
spec.license = "MIT"
|
|
16
|
+
|
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
|
18
|
+
f.match(%r{^(test|spec|features)/})
|
|
19
|
+
end
|
|
20
|
+
spec.bindir = "exe"
|
|
21
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
22
|
+
spec.require_paths = ["lib"]
|
|
23
|
+
|
|
24
|
+
spec.add_runtime_dependency "chronic", "~> 0.10"
|
|
25
|
+
spec.add_runtime_dependency "oj", "~> 3.3"
|
|
26
|
+
|
|
27
|
+
spec.add_development_dependency "bundler", "~> 1.15"
|
|
28
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
|
29
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: natter
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Garry Pettet
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2017-10-27 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: chronic
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0.10'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0.10'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: oj
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '3.3'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '3.3'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: bundler
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '1.15'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '1.15'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: rake
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '10.0'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '10.0'
|
|
69
|
+
description: A rules-based natural language intent parser written in pure Ruby.
|
|
70
|
+
email:
|
|
71
|
+
- contact@garrypettet.com
|
|
72
|
+
executables: []
|
|
73
|
+
extensions: []
|
|
74
|
+
extra_rdoc_files: []
|
|
75
|
+
files:
|
|
76
|
+
- Gemfile
|
|
77
|
+
- LICENSE.txt
|
|
78
|
+
- README.md
|
|
79
|
+
- Rakefile
|
|
80
|
+
- lib/natter.rb
|
|
81
|
+
- lib/natter/entity.rb
|
|
82
|
+
- lib/natter/intent.rb
|
|
83
|
+
- lib/natter/parser.rb
|
|
84
|
+
- lib/natter/rule.rb
|
|
85
|
+
- lib/natter/version.rb
|
|
86
|
+
- natter.gemspec
|
|
87
|
+
homepage: https://github.com/BarnabyAI/natter
|
|
88
|
+
licenses:
|
|
89
|
+
- MIT
|
|
90
|
+
metadata: {}
|
|
91
|
+
post_install_message:
|
|
92
|
+
rdoc_options: []
|
|
93
|
+
require_paths:
|
|
94
|
+
- lib
|
|
95
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
96
|
+
requirements:
|
|
97
|
+
- - ">="
|
|
98
|
+
- !ruby/object:Gem::Version
|
|
99
|
+
version: '0'
|
|
100
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
101
|
+
requirements:
|
|
102
|
+
- - ">="
|
|
103
|
+
- !ruby/object:Gem::Version
|
|
104
|
+
version: '0'
|
|
105
|
+
requirements: []
|
|
106
|
+
rubyforge_project:
|
|
107
|
+
rubygems_version: 2.6.13
|
|
108
|
+
signing_key:
|
|
109
|
+
specification_version: 4
|
|
110
|
+
summary: Rules-based natural language intent parser.
|
|
111
|
+
test_files: []
|