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.
- checksums.yaml +7 -0
- data/Changes.md +13 -0
- data/LICENSE +22 -0
- data/README.md +131 -0
- data/bin/riveshell +81 -0
- data/eg/brain/admin.rive +15 -0
- data/eg/brain/begin.rive +190 -0
- data/eg/brain/clients.rive +72 -0
- data/eg/brain/eliza.rive +301 -0
- data/eg/brain/myself.rive +61 -0
- data/eg/brain/rpg.rive +294 -0
- data/lib/rivescript/brain.rb +986 -0
- data/lib/rivescript/inheritance.rb +105 -0
- data/lib/rivescript/lang/ruby.rb +72 -0
- data/lib/rivescript/parser.rb +581 -0
- data/lib/rivescript/sessions.rb +205 -0
- data/lib/rivescript/sorting.rb +158 -0
- data/lib/rivescript/utils.rb +116 -0
- data/lib/rivescript/version.rb +9 -0
- data/lib/rivescript.rb +460 -0
- metadata +69 -0
|
@@ -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
|