rivescript 0.1.1

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.
@@ -0,0 +1,205 @@
1
+ # frozen_string_literal: true
2
+
3
+ # RiveScript Ruby port
4
+ # Byte <byte@jvmlab.org>, https://jvmlab.org/
5
+ # MIT License
6
+
7
+ class RiveScript
8
+ # SessionManager is the interface for session managers that store user
9
+ # variables for RiveScript. User variables include those set with the
10
+ # <set> tag or set_uservar, as well as recent reply history and private
11
+ # internal state variables.
12
+ #
13
+ # The default session manager keeps the variables in memory. To use a
14
+ # custom backend, subclass SessionManager and pass an instance to
15
+ # RiveScript.new(session_manager: manager).
16
+ class SessionManager
17
+ # Set user variables for +username+. +data+ is a Hash of key/value pairs.
18
+ # A value of +nil+ for a variable means it should be deleted.
19
+ def set(username, data)
20
+ raise NotImplementedError
21
+ end
22
+
23
+ # Retrieve a stored variable for a user.
24
+ # Returns +nil+ if the user does not exist, or the string "undefined"
25
+ # if the user exists but the key does not.
26
+ def get(username, key)
27
+ raise NotImplementedError
28
+ end
29
+
30
+ # Retrieve all stored user variables for +username+.
31
+ # Returns +nil+ if the user does not exist.
32
+ def get_any(username)
33
+ raise NotImplementedError
34
+ end
35
+
36
+ # Retrieve all variables about all users.
37
+ def get_all
38
+ raise NotImplementedError
39
+ end
40
+
41
+ # Reset all variables stored about a particular user.
42
+ def reset(username)
43
+ raise NotImplementedError
44
+ end
45
+
46
+ # Reset all data about all users.
47
+ def reset_all
48
+ raise NotImplementedError
49
+ end
50
+
51
+ # Make a snapshot of the user's variables so that they can be restored
52
+ # later via thaw.
53
+ def freeze(username)
54
+ raise NotImplementedError
55
+ end
56
+
57
+ # Restore the frozen snapshot of variables for a user.
58
+ # +action+ may be "thaw" (default), "discard", or "keep".
59
+ def thaw(username, action = "thaw")
60
+ raise NotImplementedError
61
+ end
62
+
63
+ # Default session variables for a new user.
64
+ def default_session
65
+ { "topic" => "random" }
66
+ end
67
+ end
68
+
69
+ # Default in-memory session store for RiveScript.
70
+ class MemorySessionManager < SessionManager
71
+ def initialize
72
+ super
73
+ @users = {}
74
+ @frozen = {}
75
+ @mutex = Mutex.new
76
+ end
77
+
78
+ def init(username)
79
+ @users[username] = default_session if @users[username].nil?
80
+ end
81
+
82
+ def set(username, data)
83
+ @mutex.synchronize do
84
+ init(username)
85
+ data.each do |key, value|
86
+ if value.nil?
87
+ @users[username].delete(key)
88
+ else
89
+ @users[username][key] = value
90
+ end
91
+ end
92
+ nil
93
+ end
94
+ end
95
+
96
+ def get(username, key)
97
+ @mutex.synchronize do
98
+ return nil if @users[username].nil?
99
+
100
+ if @users[username].key?(key)
101
+ @users[username][key]
102
+ else
103
+ "undefined"
104
+ end
105
+ end
106
+ end
107
+
108
+ def get_any(username)
109
+ @mutex.synchronize do
110
+ return nil if @users[username].nil?
111
+
112
+ Utils.clone(@users[username])
113
+ end
114
+ end
115
+
116
+ def get_all
117
+ @mutex.synchronize { Utils.clone(@users) }
118
+ end
119
+
120
+ def reset(username)
121
+ @mutex.synchronize do
122
+ @users.delete(username)
123
+ @frozen.delete(username)
124
+ nil
125
+ end
126
+ end
127
+
128
+ def reset_all
129
+ @mutex.synchronize do
130
+ @users = {}
131
+ @frozen = {}
132
+ nil
133
+ end
134
+ end
135
+
136
+ def freeze(username)
137
+ @mutex.synchronize do
138
+ if @users[username].nil?
139
+ raise "freeze(#{username}): user not found"
140
+ end
141
+
142
+ @frozen[username] = Utils.clone(@users[username])
143
+ nil
144
+ end
145
+ end
146
+
147
+ def thaw(username, action = "thaw")
148
+ @mutex.synchronize do
149
+ if @frozen[username].nil?
150
+ raise "thaw(#{username}): no frozen variables found"
151
+ end
152
+
153
+ case action
154
+ when "thaw"
155
+ @users[username] = Utils.clone(@frozen[username])
156
+ @frozen.delete(username)
157
+ when "discard"
158
+ @frozen.delete(username)
159
+ when "keep"
160
+ @users[username] = Utils.clone(@frozen[username])
161
+ else
162
+ raise "bad thaw action"
163
+ end
164
+
165
+ nil
166
+ end
167
+ end
168
+ end
169
+
170
+ # Session manager that does not remember any user variables.
171
+ # Mostly useful for unit tests.
172
+ class NullSessionManager < SessionManager
173
+ def set(_username, _data)
174
+ nil
175
+ end
176
+
177
+ def get(_username, _key)
178
+ "undefined"
179
+ end
180
+
181
+ def get_any(_username)
182
+ nil
183
+ end
184
+
185
+ def get_all
186
+ {}
187
+ end
188
+
189
+ def reset(_username)
190
+ nil
191
+ end
192
+
193
+ def reset_all
194
+ nil
195
+ end
196
+
197
+ def freeze(_username)
198
+ nil
199
+ end
200
+
201
+ def thaw(_username, _action = "thaw")
202
+ nil
203
+ end
204
+ end
205
+ end
@@ -0,0 +1,158 @@
1
+ # RiveScript Ruby port, https://jvmlab.org/, MIT License
2
+
3
+ # Data sorting functions
4
+
5
+ require_relative "utils"
6
+
7
+ class RiveScript
8
+ module Sorting
9
+ module_function
10
+
11
+ # Sort a group of triggers in an optimal sorting order.
12
+ def sort_trigger_set(triggers, exclude_previous = true, say = nil)
13
+ say ||= ->(_what) {}
14
+
15
+ prior = { "0" => [] }
16
+
17
+ triggers.each do |trig|
18
+ if exclude_previous && !trig[1]["previous"].nil?
19
+ next
20
+ end
21
+
22
+ match = trig[0].match(/\{weight=(\d+)\}/i)
23
+ weight = "0"
24
+ weight = match[1] if match && match[1]
25
+
26
+ prior[weight] ||= []
27
+ prior[weight].push(trig)
28
+ end
29
+
30
+ running = []
31
+ prior_sort = prior.keys.sort_by { |k| -k.to_i }
32
+
33
+ prior_sort.each do |p|
34
+ say.call("Sorting triggers with priority #{p}")
35
+
36
+ inherits = -1
37
+ highest_inherits = -1
38
+ track = { inherits => init_sort_track }
39
+
40
+ prior[p].each do |trig|
41
+ pattern = trig[0]
42
+ say.call("Looking at trigger: #{pattern}")
43
+
44
+ match = pattern.match(/\{inherits=(\d+)\}/i)
45
+ if match
46
+ inherits = match[1].to_i
47
+ highest_inherits = inherits if inherits > highest_inherits
48
+ say.call("Trigger belongs to a topic that inherits other topics. Level=#{inherits}")
49
+ pattern = pattern.gsub(/\{inherits=\d+\}/i, "")
50
+ trig[0] = pattern
51
+ else
52
+ inherits = -1
53
+ end
54
+
55
+ track[inherits] ||= init_sort_track
56
+
57
+ if pattern.include?("_")
58
+ cnt = Utils.word_count(pattern)
59
+ say.call("Has a _ wildcard with #{cnt} words.")
60
+ if cnt > 0
61
+ track[inherits]["alpha"][cnt] ||= []
62
+ track[inherits]["alpha"][cnt].push(trig)
63
+ else
64
+ track[inherits]["under"].push(trig)
65
+ end
66
+ elsif pattern.include?("#")
67
+ cnt = Utils.word_count(pattern)
68
+ say.call("Has a # wildcard with #{cnt} words.")
69
+ if cnt > 0
70
+ track[inherits]["number"][cnt] ||= []
71
+ track[inherits]["number"][cnt].push(trig)
72
+ else
73
+ track[inherits]["pound"].push(trig)
74
+ end
75
+ elsif pattern.include?("*")
76
+ cnt = Utils.word_count(pattern)
77
+ say.call("Has a * wildcard with #{cnt} words.")
78
+ if cnt > 0
79
+ track[inherits]["wild"][cnt] ||= []
80
+ track[inherits]["wild"][cnt].push(trig)
81
+ else
82
+ track[inherits]["star"].push(trig)
83
+ end
84
+ elsif pattern.include?("[")
85
+ cnt = Utils.word_count(pattern)
86
+ say.call("Has optionals with #{cnt} words.")
87
+ track[inherits]["option"][cnt] ||= []
88
+ track[inherits]["option"][cnt].push(trig)
89
+ else
90
+ cnt = Utils.word_count(pattern)
91
+ say.call("Totally atomic trigger with #{cnt} words.")
92
+ track[inherits]["atomic"][cnt] ||= []
93
+ track[inherits]["atomic"][cnt].push(trig)
94
+ end
95
+ end
96
+
97
+ track[highest_inherits + 1] = track[-1]
98
+ track.delete(-1)
99
+
100
+ track_sorted = track.keys.sort
101
+ track_sorted.each do |ip|
102
+ say.call("ip=#{ip}")
103
+
104
+ %w[atomic option alpha number wild].each do |kind|
105
+ kind_sorted = track[ip][kind].keys.sort.reverse
106
+ kind_sorted.each do |wordcnt|
107
+ sorted_by_length = track[ip][kind][wordcnt].sort { |a, b| b[0].length <=> a[0].length }
108
+ running.concat(sorted_by_length)
109
+ end
110
+ end
111
+
112
+ under_sorted = track[ip]["under"].sort { |a, b| b[0].length <=> a[0].length }
113
+ pound_sorted = track[ip]["pound"].sort { |a, b| b[0].length <=> a[0].length }
114
+ star_sorted = track[ip]["star"].sort { |a, b| b[0].length <=> a[0].length }
115
+ running.concat(under_sorted)
116
+ running.concat(pound_sorted)
117
+ running.concat(star_sorted)
118
+ end
119
+ end
120
+
121
+ running
122
+ end
123
+
124
+ # Sort a list of strings by their word counts and lengths.
125
+ def sort_list(items)
126
+ track = {}
127
+
128
+ items.each do |item|
129
+ cnt = Utils.word_count(item, true)
130
+ track[cnt] ||= []
131
+ track[cnt].push(item)
132
+ end
133
+
134
+ output = []
135
+ sorted = track.keys.sort.reverse
136
+ sorted.each do |count|
137
+ bylen = track[count].sort_by { |a| -a.length }
138
+ output.concat(bylen)
139
+ end
140
+
141
+ output
142
+ end
143
+
144
+ def init_sort_track
145
+ {
146
+ "atomic" => {},
147
+ "option" => {},
148
+ "alpha" => {},
149
+ "number" => {},
150
+ "wild" => {},
151
+ "pound" => [],
152
+ "under" => [],
153
+ "star" => []
154
+ }
155
+ end
156
+ private_class_method :init_sort_track
157
+ end
158
+ end
@@ -0,0 +1,116 @@
1
+ # RiveScript Ruby port, https://jvmlab.org/, MIT License
2
+
3
+ # Miscellaneous utility functions.
4
+
5
+ class RiveScript
6
+ module Utils
7
+ module_function
8
+
9
+ def strip(text)
10
+ text.gsub(/^[\s\t]+/, "").gsub(/[\s\t]+$/, "").gsub(/[\x0D\x0A]+/, "")
11
+ end
12
+
13
+ def trim(text)
14
+ text.gsub(/^[\x0D\x0A\s\t]+/, "").gsub(/[\x0D\x0A\s\t]+$/, "")
15
+ end
16
+
17
+ def extend(a, b)
18
+ b.each do |attr, value|
19
+ a[attr] = value
20
+ end
21
+ end
22
+
23
+ def word_count(trigger, all = false)
24
+ words = if all
25
+ trigger.split(/\s+/)
26
+ else
27
+ trigger.split(/[\s\*#_\|]+/)
28
+ end
29
+ words.count { |word| !word.empty? }
30
+ end
31
+
32
+ def strip_nasties(string, utf8)
33
+ if utf8
34
+ string.gsub(/[\\<>]+/, "")
35
+ else
36
+ string.gsub(/[^A-Za-z0-9 ]/, "")
37
+ end
38
+ end
39
+
40
+ def quotemeta(string)
41
+ unsafe = "\\.+*?[^]$(){}=!<>|:".chars
42
+ unsafe.each do |char|
43
+ string = string.gsub(char, "\\#{char}")
44
+ end
45
+ string
46
+ end
47
+
48
+ def is_atomic(trigger)
49
+ specials = ["*", "#", "_", "(", "[", "<", "@"]
50
+ specials.none? { |special| trigger.include?(special) }
51
+ end
52
+
53
+ def string_format(type, string)
54
+ case type
55
+ when "uppercase"
56
+ string.upcase
57
+ when "lowercase"
58
+ string.downcase
59
+ when "sentence"
60
+ string = string.to_s
61
+ string[0].upcase + string[1..]
62
+ when "formal"
63
+ string.split(/\s+/).map do |word|
64
+ word[0].upcase + word[1..]
65
+ end.join(" ")
66
+ else
67
+ string
68
+ end
69
+ end
70
+
71
+ def parse_call_args(str)
72
+ result = []
73
+ buff = ""
74
+ inside_a_string = false
75
+
76
+ str.each_char do |c|
77
+ if c.match?(/\s/) && !inside_a_string
78
+ unless buff.empty?
79
+ result.push(buff)
80
+ buff = ""
81
+ end
82
+ elsif c == '"'
83
+ unless inside_a_string
84
+ # opening quote - don't add to buff
85
+ else
86
+ result.push(buff) unless buff.empty?
87
+ buff = ""
88
+ end
89
+ inside_a_string = !inside_a_string
90
+ else
91
+ buff += c
92
+ end
93
+ end
94
+
95
+ result.push(buff) unless buff.empty?
96
+ result
97
+ end
98
+
99
+ def clone(obj)
100
+ case obj
101
+ when nil
102
+ nil
103
+ when Array
104
+ obj.map { |item| clone(item) }
105
+ when Hash
106
+ obj.transform_values { |v| clone(v) }
107
+ else
108
+ obj
109
+ end
110
+ end
111
+
112
+ def n_index_of(string, match, index)
113
+ string.split(match, index).join(match).length
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ # RiveScript Ruby port
4
+ # Byte <byte@jvmlab.org>, https://jvmlab.org/
5
+ # MIT License
6
+
7
+ class RiveScript
8
+ VERSION = "0.1.1"
9
+ end