xip 0.0.1 → 2.0.0.beta2
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/.circleci/config.yml +116 -0
- data/.gitignore +12 -0
- data/CHANGELOG.md +135 -0
- data/Gemfile +4 -1
- data/Gemfile.lock +65 -15
- data/LICENSE +6 -4
- data/README.md +51 -1
- data/VERSION +1 -0
- data/bin/xip +3 -11
- data/lib/xip.rb +1 -3
- data/lib/xip/base.rb +189 -0
- data/lib/xip/cli.rb +273 -0
- data/lib/xip/cli_base.rb +24 -0
- data/lib/xip/commands/command.rb +13 -0
- data/lib/xip/commands/console.rb +74 -0
- data/lib/xip/commands/server.rb +63 -0
- data/lib/xip/configuration.rb +56 -0
- data/lib/xip/controller/callbacks.rb +63 -0
- data/lib/xip/controller/catch_all.rb +84 -0
- data/lib/xip/controller/controller.rb +274 -0
- data/lib/xip/controller/dev_jumps.rb +40 -0
- data/lib/xip/controller/dynamic_delay.rb +61 -0
- data/lib/xip/controller/helpers.rb +128 -0
- data/lib/xip/controller/interrupt_detect.rb +99 -0
- data/lib/xip/controller/messages.rb +283 -0
- data/lib/xip/controller/nlp.rb +49 -0
- data/lib/xip/controller/replies.rb +281 -0
- data/lib/xip/controller/unrecognized_message.rb +61 -0
- data/lib/xip/core_ext.rb +5 -0
- data/lib/xip/core_ext/numeric.rb +10 -0
- data/lib/xip/core_ext/string.rb +18 -0
- data/lib/xip/dispatcher.rb +68 -0
- data/lib/xip/errors.rb +55 -0
- data/lib/xip/flow/base.rb +69 -0
- data/lib/xip/flow/specification.rb +56 -0
- data/lib/xip/flow/state.rb +82 -0
- data/lib/xip/generators/builder.rb +41 -0
- data/lib/xip/generators/builder/.gitignore +30 -0
- data/lib/xip/generators/builder/Gemfile +19 -0
- data/lib/xip/generators/builder/Procfile.dev +2 -0
- data/lib/xip/generators/builder/README.md +9 -0
- data/lib/xip/generators/builder/Rakefile +2 -0
- data/lib/xip/generators/builder/bot/controllers/bot_controller.rb +55 -0
- data/lib/xip/generators/builder/bot/controllers/catch_alls_controller.rb +21 -0
- data/lib/xip/generators/builder/bot/controllers/concerns/.keep +0 -0
- data/lib/xip/generators/builder/bot/controllers/goodbyes_controller.rb +9 -0
- data/lib/xip/generators/builder/bot/controllers/hellos_controller.rb +9 -0
- data/lib/xip/generators/builder/bot/controllers/interrupts_controller.rb +9 -0
- data/lib/xip/generators/builder/bot/controllers/unrecognized_messages_controller.rb +9 -0
- data/lib/xip/generators/builder/bot/helpers/bot_helper.rb +2 -0
- data/lib/xip/generators/builder/bot/models/bot_record.rb +3 -0
- data/lib/xip/generators/builder/bot/models/concerns/.keep +0 -0
- data/lib/xip/generators/builder/bot/replies/catch_alls/level1.yml +2 -0
- data/lib/xip/generators/builder/bot/replies/goodbyes/say_goodbye.yml +2 -0
- data/lib/xip/generators/builder/bot/replies/hellos/say_hello.yml +2 -0
- data/lib/xip/generators/builder/config.ru +4 -0
- data/lib/xip/generators/builder/config/boot.rb +6 -0
- data/lib/xip/generators/builder/config/database.yml +25 -0
- data/lib/xip/generators/builder/config/environment.rb +2 -0
- data/lib/xip/generators/builder/config/flow_map.rb +25 -0
- data/lib/xip/generators/builder/config/initializers/autoload.rb +8 -0
- data/lib/xip/generators/builder/config/initializers/inflections.rb +16 -0
- data/lib/xip/generators/builder/config/puma.rb +25 -0
- data/lib/xip/generators/builder/config/services.yml +35 -0
- data/lib/xip/generators/builder/config/sidekiq.yml +3 -0
- data/lib/xip/generators/builder/db/seeds.rb +7 -0
- data/lib/xip/generators/generate.rb +39 -0
- data/lib/xip/generators/generate/flow/controllers/controller.tt +7 -0
- data/lib/xip/generators/generate/flow/helpers/helper.tt +3 -0
- data/lib/xip/generators/generate/flow/replies/ask_example.tt +9 -0
- data/lib/xip/helpers/redis.rb +40 -0
- data/lib/xip/jobs.rb +9 -0
- data/lib/xip/lock.rb +82 -0
- data/lib/xip/logger.rb +9 -3
- data/lib/xip/migrations/configurator.rb +73 -0
- data/lib/xip/migrations/generators.rb +16 -0
- data/lib/xip/migrations/railtie_config.rb +14 -0
- data/lib/xip/migrations/tasks.rb +43 -0
- data/lib/xip/nlp/client.rb +21 -0
- data/lib/xip/nlp/result.rb +56 -0
- data/lib/xip/reloader.rb +89 -0
- data/lib/xip/reply.rb +36 -0
- data/lib/xip/scheduled_reply.rb +18 -0
- data/lib/xip/server.rb +63 -0
- data/lib/xip/service_message.rb +17 -0
- data/lib/xip/service_reply.rb +44 -0
- data/lib/xip/services/base_client.rb +24 -0
- data/lib/xip/services/base_message_handler.rb +27 -0
- data/lib/xip/services/base_reply_handler.rb +72 -0
- data/lib/xip/services/jobs/handle_message_job.rb +21 -0
- data/lib/xip/session.rb +203 -0
- data/lib/xip/version.rb +7 -1
- data/logo.svg +17 -0
- data/spec/configuration_spec.rb +93 -0
- data/spec/controller/callbacks_spec.rb +217 -0
- data/spec/controller/catch_all_spec.rb +154 -0
- data/spec/controller/controller_spec.rb +889 -0
- data/spec/controller/dynamic_delay_spec.rb +70 -0
- data/spec/controller/helpers_spec.rb +119 -0
- data/spec/controller/interrupt_detect_spec.rb +171 -0
- data/spec/controller/messages_spec.rb +744 -0
- data/spec/controller/nlp_spec.rb +93 -0
- data/spec/controller/replies_spec.rb +694 -0
- data/spec/controller/unrecognized_message_spec.rb +168 -0
- data/spec/dispatcher_spec.rb +79 -0
- data/spec/flow/flow_spec.rb +82 -0
- data/spec/flow/state_spec.rb +109 -0
- data/spec/helpers/redis_spec.rb +77 -0
- data/spec/lock_spec.rb +100 -0
- data/spec/nlp/client_spec.rb +23 -0
- data/spec/nlp/result_spec.rb +57 -0
- data/spec/replies/hello.yml.erb +15 -0
- data/spec/replies/messages/say_hola.yml+facebook.erb +6 -0
- data/spec/replies/messages/say_hola.yml+twilio.erb +6 -0
- data/spec/replies/messages/say_hola.yml.erb +6 -0
- data/spec/replies/messages/say_howdy_with_dynamic.yml +79 -0
- data/spec/replies/messages/say_msgs_without_breaks.yml +4 -0
- data/spec/replies/messages/say_offer.yml +6 -0
- data/spec/replies/messages/say_offer_with_dynamic.yml +6 -0
- data/spec/replies/messages/say_oi.yml.erb +15 -0
- data/spec/replies/messages/say_randomize_speech.yml +10 -0
- data/spec/replies/messages/say_randomize_text.yml +10 -0
- data/spec/replies/messages/say_yo.yml +6 -0
- data/spec/replies/messages/say_yo.yml+twitter +6 -0
- data/spec/replies/messages/sub1/sub2/say_nested.yml +10 -0
- data/spec/reply_spec.rb +61 -0
- data/spec/scheduled_reply_spec.rb +23 -0
- data/spec/service_reply_spec.rb +92 -0
- data/spec/session_spec.rb +366 -0
- data/spec/spec_helper.rb +22 -66
- data/spec/support/alternate_helpers/foo_helper.rb +5 -0
- data/spec/support/controllers/vaders_controller.rb +24 -0
- data/spec/support/helpers/fun/games_helper.rb +7 -0
- data/spec/support/helpers/fun/pdf_helper.rb +7 -0
- data/spec/support/helpers/standalone_helper.rb +5 -0
- data/spec/support/helpers_typo/users_helper.rb +2 -0
- data/spec/support/nlp_clients/dialogflow.rb +9 -0
- data/spec/support/nlp_clients/luis.rb +9 -0
- data/spec/support/nlp_results/luis_result.rb +163 -0
- data/spec/support/sample_messages.rb +66 -0
- data/spec/support/services.yml +31 -0
- data/spec/support/services_with_erb.yml +31 -0
- data/spec/version_spec.rb +16 -0
- data/xip.gemspec +25 -14
- metadata +320 -18
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Xip
|
|
4
|
+
class Controller
|
|
5
|
+
module DevJumps
|
|
6
|
+
|
|
7
|
+
DEV_JUMP_REGEX = /\A\/(.*)\/(.*)\z|\A\/\/(.*)\z|\A\/(.*)\z/
|
|
8
|
+
|
|
9
|
+
extend ActiveSupport::Concern
|
|
10
|
+
|
|
11
|
+
included do
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def dev_jump_detected?
|
|
15
|
+
if Xip.env.development?
|
|
16
|
+
if current_message.message&.match(DEV_JUMP_REGEX)
|
|
17
|
+
handle_dev_jump
|
|
18
|
+
return true
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
false
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def handle_dev_jump
|
|
26
|
+
_, flow, state = current_message.message.split('/')
|
|
27
|
+
flow = nil if flow.blank?
|
|
28
|
+
|
|
29
|
+
Xip::Logger.l(
|
|
30
|
+
topic: 'dev_jump',
|
|
31
|
+
message: "Dev Jump detected: Flow: #{flow.inspect} State: #{state.inspect}"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
step_to flow: flow, state: state
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Xip
|
|
4
|
+
class Controller
|
|
5
|
+
|
|
6
|
+
module DynamicDelay
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
SHORT_DELAY = 3.0
|
|
10
|
+
STANDARD_DELAY = 4.3
|
|
11
|
+
LONG_DELAY = 7.0
|
|
12
|
+
|
|
13
|
+
included do
|
|
14
|
+
def dynamic_delay(previous_reply:)
|
|
15
|
+
calculate_delay(previous_reply: previous_reply)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def calculate_delay(previous_reply:)
|
|
21
|
+
return SHORT_DELAY if previous_reply.blank?
|
|
22
|
+
|
|
23
|
+
case previous_reply['reply_type']
|
|
24
|
+
when 'text'
|
|
25
|
+
calculate_delay_from_text(previous_reply['text'])
|
|
26
|
+
when 'image'
|
|
27
|
+
STANDARD_DELAY
|
|
28
|
+
when 'audio'
|
|
29
|
+
STANDARD_DELAY
|
|
30
|
+
when 'video'
|
|
31
|
+
STANDARD_DELAY
|
|
32
|
+
when 'file'
|
|
33
|
+
STANDARD_DELAY
|
|
34
|
+
when 'cards'
|
|
35
|
+
STANDARD_DELAY
|
|
36
|
+
when 'list'
|
|
37
|
+
STANDARD_DELAY
|
|
38
|
+
when nil
|
|
39
|
+
SHORT_DELAY
|
|
40
|
+
else
|
|
41
|
+
SHORT_DELAY
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def calculate_delay_from_text(text)
|
|
47
|
+
case text.size
|
|
48
|
+
when 0..55
|
|
49
|
+
SHORT_DELAY
|
|
50
|
+
when 56..140
|
|
51
|
+
STANDARD_DELAY
|
|
52
|
+
when 141..256
|
|
53
|
+
STANDARD_DELAY * 1.5
|
|
54
|
+
else
|
|
55
|
+
LONG_DELAY
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/dependencies"
|
|
4
|
+
|
|
5
|
+
module Xip
|
|
6
|
+
class Controller
|
|
7
|
+
module Helpers
|
|
8
|
+
|
|
9
|
+
extend ActiveSupport::Concern
|
|
10
|
+
|
|
11
|
+
class MissingHelperError < LoadError
|
|
12
|
+
def initialize(error, path)
|
|
13
|
+
@error = error
|
|
14
|
+
@path = "helpers/#{path}.rb"
|
|
15
|
+
set_backtrace error.backtrace
|
|
16
|
+
|
|
17
|
+
if error.path =~ /^#{path}(\.rb)?$/
|
|
18
|
+
super("Missing helper file helpers/%s.rb" % path)
|
|
19
|
+
else
|
|
20
|
+
raise error
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# class << self; attr_accessor :helpers_path; end
|
|
26
|
+
|
|
27
|
+
included do
|
|
28
|
+
class_attribute :_helpers, default: Module.new
|
|
29
|
+
class_attribute :helpers_path, default: ["bot/helpers"]
|
|
30
|
+
class_attribute :include_all_helpers, default: true
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class_methods do
|
|
34
|
+
# When a class is inherited, wrap its helper module in a new module.
|
|
35
|
+
# This ensures that the parent class's module can be changed
|
|
36
|
+
# independently of the child class's.
|
|
37
|
+
def inherited(subclass)
|
|
38
|
+
helpers = _helpers
|
|
39
|
+
subclass._helpers = Module.new { include helpers }
|
|
40
|
+
|
|
41
|
+
if subclass.superclass == Xip::Controller && Xip::Controller.include_all_helpers
|
|
42
|
+
subclass.helper :all
|
|
43
|
+
else
|
|
44
|
+
subclass.class_eval { default_helper_module! } unless subclass.anonymous?
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
include subclass._helpers
|
|
48
|
+
|
|
49
|
+
super
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def modules_for_helpers(args)
|
|
53
|
+
# Allow all helpers to be included
|
|
54
|
+
args += all_bot_helpers if args.delete(:all)
|
|
55
|
+
|
|
56
|
+
# Add each helper_path to the LOAD_PATH
|
|
57
|
+
Array(helpers_path).each {|path| $:.unshift(path) }
|
|
58
|
+
|
|
59
|
+
args.flatten.map! do |arg|
|
|
60
|
+
case arg
|
|
61
|
+
when String, Symbol
|
|
62
|
+
file_name = "#{arg.to_s.underscore}_helper"
|
|
63
|
+
begin
|
|
64
|
+
require_dependency(file_name)
|
|
65
|
+
rescue LoadError => e
|
|
66
|
+
raise Xip::Controller::Helpers::MissingHelperError.new(e, file_name)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
mod_name = file_name.camelize
|
|
70
|
+
begin
|
|
71
|
+
mod_name.constantize
|
|
72
|
+
rescue LoadError
|
|
73
|
+
raise NameError, "Couldn't find #{mod_name}, expected it to be defined in helpers/#{file_name}.rb"
|
|
74
|
+
end
|
|
75
|
+
when Module
|
|
76
|
+
arg
|
|
77
|
+
else
|
|
78
|
+
raise ArgumentError, "helper must be a String, Symbol, or Module"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def helper(*args, &block)
|
|
84
|
+
modules_for_helpers(args).each do |mod|
|
|
85
|
+
add_template_helper(mod)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
_helpers.module_eval(&block) if block_given?
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def default_helper_module!
|
|
92
|
+
module_name = name.sub(/Controller$/, "".freeze)
|
|
93
|
+
module_path = module_name.underscore
|
|
94
|
+
helper module_path
|
|
95
|
+
rescue LoadError => e
|
|
96
|
+
raise e unless e.is_missing? "helpers/#{module_path}_helper"
|
|
97
|
+
rescue NameError => e
|
|
98
|
+
raise e unless e.missing_name? "#{module_name}Helper"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Returns a list of helper names in a given path.
|
|
102
|
+
#
|
|
103
|
+
# Xip::Controller.all_helpers_from_path 'bot/helpers'
|
|
104
|
+
# # => ["bot", "estimates", "tickets"]
|
|
105
|
+
def all_helpers_from_path(path)
|
|
106
|
+
helpers = Array(path).flat_map do |_path|
|
|
107
|
+
extract = /^#{Regexp.quote(_path.to_s)}\/?(.*)_helper.rb$/
|
|
108
|
+
names = Dir["#{_path}/**/*_helper.rb"].map { |file| file.sub(extract, '\1'.freeze) }
|
|
109
|
+
names.sort!
|
|
110
|
+
end
|
|
111
|
+
helpers.uniq!
|
|
112
|
+
helpers
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
def add_template_helper(mod)
|
|
117
|
+
_helpers.module_eval { include mod }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Extract helper names from files in "bot/helpers/**/*_helper.rb"
|
|
121
|
+
def all_bot_helpers
|
|
122
|
+
all_helpers_from_path(helpers_path)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Xip
|
|
4
|
+
class Controller
|
|
5
|
+
module InterruptDetect
|
|
6
|
+
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
include Xip::Redis
|
|
10
|
+
|
|
11
|
+
included do
|
|
12
|
+
|
|
13
|
+
attr_reader :current_lock
|
|
14
|
+
|
|
15
|
+
def current_lock
|
|
16
|
+
@current_lock ||= Xip::Lock.find_lock(
|
|
17
|
+
session_id: current_session_id
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def run_interrupt_action
|
|
22
|
+
Xip::Logger.l(
|
|
23
|
+
topic: 'interrupt',
|
|
24
|
+
message: "Interrupt detected for session #{current_session_id}"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
unless defined?(InterruptsController)
|
|
28
|
+
Xip::Logger.l(
|
|
29
|
+
topic: 'interrupt',
|
|
30
|
+
message: 'Ignoring interrupt; InterruptsController not defined.'
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
return false
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
interrupt_controller = InterruptsController.new(
|
|
37
|
+
service_message: current_message
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
begin
|
|
41
|
+
# Run say_interrupted action
|
|
42
|
+
interrupt_controller.say_interrupted
|
|
43
|
+
|
|
44
|
+
unless interrupt_controller.progressed?
|
|
45
|
+
# Log, but we cannot run the catch_all here
|
|
46
|
+
Xip::Logger.l(
|
|
47
|
+
topic: 'interrupt',
|
|
48
|
+
message: 'Did not send replies, update session, or step'
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
rescue StandardError => e
|
|
52
|
+
# Log, but we cannot run the catch_all here
|
|
53
|
+
Xip::Logger.l(
|
|
54
|
+
topic: 'interrupt',
|
|
55
|
+
message: [e.message, e.backtrace.join("\n")].join("\n")
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def interrupt_detected?
|
|
63
|
+
# No interruption if there isn't an existing lock for this session
|
|
64
|
+
return false if current_lock.blank?
|
|
65
|
+
|
|
66
|
+
# No interruption if we are in the same thread
|
|
67
|
+
return false if current_thread_has_control?
|
|
68
|
+
|
|
69
|
+
true
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def current_thread_has_control?
|
|
73
|
+
current_lock.tid == Xip.tid
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def lock_session!(session_slug:, position: nil)
|
|
77
|
+
lock = Xip::Lock.new(
|
|
78
|
+
session_id: current_session_id,
|
|
79
|
+
session_slug: session_slug,
|
|
80
|
+
position: position
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
lock.create
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Yields control to other threads to take action on this session
|
|
87
|
+
# by releasing the lock.
|
|
88
|
+
def release_lock!
|
|
89
|
+
# We don't want to release the lock from within InterruptsController
|
|
90
|
+
# otherwise the InterruptsController can get interrupted.
|
|
91
|
+
unless self.class.to_s == 'InterruptsController'
|
|
92
|
+
current_lock&.release
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Xip
|
|
4
|
+
class Controller
|
|
5
|
+
module Messages
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
included do
|
|
9
|
+
attr_accessor :normalized_msg, :homophone_translated_msg
|
|
10
|
+
|
|
11
|
+
unless defined?(ALPHA_ORDINALS)
|
|
12
|
+
ALPHA_ORDINALS = ('A'..'Z').to_a.freeze
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
unless defined?(NO_MATCH)
|
|
16
|
+
NO_MATCH = 0xdeadbeef
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
unless defined?(HOMOPHONES)
|
|
20
|
+
HOMOPHONES = {
|
|
21
|
+
'EH' => 'A',
|
|
22
|
+
'BE' => 'B',
|
|
23
|
+
'BEE' => 'B',
|
|
24
|
+
'CEE' => 'C',
|
|
25
|
+
'SEA' => 'C',
|
|
26
|
+
'SEE' => 'C',
|
|
27
|
+
'DEE' => 'D',
|
|
28
|
+
'GEE' => 'G',
|
|
29
|
+
'EYE' => 'I',
|
|
30
|
+
'AYE' => 'I',
|
|
31
|
+
'JAY' => 'J',
|
|
32
|
+
'KAY' => 'K',
|
|
33
|
+
'KAYE' => 'K',
|
|
34
|
+
'OH' => 'O',
|
|
35
|
+
'OWE' => 'O',
|
|
36
|
+
'PEA' => 'P',
|
|
37
|
+
'PEE' => 'P',
|
|
38
|
+
'CUE' => 'Q',
|
|
39
|
+
'QUEUE' => 'Q',
|
|
40
|
+
'ARR' => 'R',
|
|
41
|
+
'YOU' => 'U',
|
|
42
|
+
'YEW' => 'U',
|
|
43
|
+
'EX' => 'X',
|
|
44
|
+
'WHY' => 'Y',
|
|
45
|
+
'ZEE' => 'Z'
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def normalized_msg
|
|
50
|
+
@normalized_msg ||= current_message.message.normalize
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Converts homophones into alpha-ordinals
|
|
54
|
+
def homophone_translated_msg
|
|
55
|
+
@homophone_translated_msg ||= begin
|
|
56
|
+
ord = normalized_msg.without_punctuation
|
|
57
|
+
if HOMOPHONES[ord].present?
|
|
58
|
+
HOMOPHONES[ord]
|
|
59
|
+
else
|
|
60
|
+
ord
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Hash for message and lambda pairs. If the message is matched, the
|
|
66
|
+
# lambda will be called.
|
|
67
|
+
#
|
|
68
|
+
# Example: {
|
|
69
|
+
# "100k" => proc { step_back }, "200k" => proc { step_to flow :hello }
|
|
70
|
+
# }
|
|
71
|
+
def handle_message(message_tuples)
|
|
72
|
+
match = NO_MATCH # dummy value since nils are used for matching
|
|
73
|
+
|
|
74
|
+
if reserved_homophones_used = contains_homophones?(message_tuples.keys)
|
|
75
|
+
raise(
|
|
76
|
+
Xip::Errors::ReservedHomophoneUsed,
|
|
77
|
+
"Cannot use `#{reserved_homophones_used.join(', ')}`. Reserved for homophones."
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Before checking content, match against our ordinals
|
|
82
|
+
if idx = message_is_an_ordinal?
|
|
83
|
+
# find the value stored in the message tuple via the index
|
|
84
|
+
matched_value = message_tuples.keys[idx]
|
|
85
|
+
match = matched_value unless matched_value.nil?
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
if match == NO_MATCH
|
|
89
|
+
message_tuples.keys.each_with_index do |msg, i|
|
|
90
|
+
# intent detection
|
|
91
|
+
if msg.is_a?(Symbol)
|
|
92
|
+
perform_nlp! unless nlp_result.present?
|
|
93
|
+
|
|
94
|
+
if intent_matched?(msg)
|
|
95
|
+
match = msg
|
|
96
|
+
break
|
|
97
|
+
else
|
|
98
|
+
next
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
if msg.is_a?(Regexp)
|
|
103
|
+
if normalized_msg =~ msg
|
|
104
|
+
match = msg
|
|
105
|
+
break
|
|
106
|
+
else
|
|
107
|
+
next
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# custom mismatch handler; any nil key results in a match
|
|
112
|
+
if msg.nil?
|
|
113
|
+
match = msg
|
|
114
|
+
break
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# check if the normalized message matches exactly
|
|
118
|
+
if message_matches?(msg)
|
|
119
|
+
match = msg
|
|
120
|
+
break
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
if match != NO_MATCH
|
|
126
|
+
instance_eval(&message_tuples[match])
|
|
127
|
+
else
|
|
128
|
+
handle_mismatch(true)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Matches the message or the oridinal value entered (via SMS)
|
|
133
|
+
# Ignores case and strips leading and trailing whitespace before matching.
|
|
134
|
+
def get_match(messages, raise_on_mismatch: true, fuzzy_match: true)
|
|
135
|
+
if reserved_homophones_used = contains_homophones?(messages)
|
|
136
|
+
raise(
|
|
137
|
+
Xip::Errors::ReservedHomophoneUsed,
|
|
138
|
+
"Cannot use `#{reserved_homophones_used.join(', ')}`. Reserved for homophones."
|
|
139
|
+
)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Before checking content, match against our ordinals
|
|
143
|
+
if idx = message_is_an_ordinal?
|
|
144
|
+
return messages[idx] unless messages[idx].nil?
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
messages.each_with_index do |msg, i|
|
|
148
|
+
# entity detection
|
|
149
|
+
if msg.is_a?(Symbol)
|
|
150
|
+
perform_nlp! unless nlp_result.present?
|
|
151
|
+
|
|
152
|
+
if match = entity_matched?(msg, fuzzy_match)
|
|
153
|
+
return match
|
|
154
|
+
else
|
|
155
|
+
next
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# multi-entity detection
|
|
160
|
+
if msg.is_a?(Array)
|
|
161
|
+
perform_nlp! unless nlp_result.present?
|
|
162
|
+
|
|
163
|
+
if match = entities_matched?(msg, fuzzy_match)
|
|
164
|
+
return match
|
|
165
|
+
else
|
|
166
|
+
next
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
if message_matches?(msg)
|
|
171
|
+
return msg
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
handle_mismatch(raise_on_mismatch)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
private
|
|
179
|
+
|
|
180
|
+
def handle_mismatch(raise_on_mismatch)
|
|
181
|
+
log_nlp_result unless Xip.config.log_all_nlp_results # Already logged
|
|
182
|
+
|
|
183
|
+
if raise_on_mismatch
|
|
184
|
+
raise(
|
|
185
|
+
Xip::Errors::UnrecognizedMessage,
|
|
186
|
+
"The reply '#{current_message.message}' was not recognized."
|
|
187
|
+
)
|
|
188
|
+
else
|
|
189
|
+
current_message.message
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def contains_homophones?(arr)
|
|
194
|
+
arr = arr.map do |elem|
|
|
195
|
+
elem.normalize if elem.is_a?(String)
|
|
196
|
+
end.compact
|
|
197
|
+
|
|
198
|
+
homophones = arr & HOMOPHONES.keys
|
|
199
|
+
homophones.any? ? homophones : false
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Returns the index of the ordinal, nil if not found
|
|
203
|
+
def message_is_an_ordinal?
|
|
204
|
+
ALPHA_ORDINALS.index(homophone_translated_msg)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def message_matches?(msg)
|
|
208
|
+
normalized_msg == msg.upcase
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def intent_matched?(intent)
|
|
212
|
+
nlp_result.intent == intent
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def entity_matched?(entity, fuzzy_match)
|
|
216
|
+
if nlp_result.entities.has_key?(entity)
|
|
217
|
+
match_count = nlp_result.entities[entity].size
|
|
218
|
+
if match_count > 1 && !fuzzy_match
|
|
219
|
+
log_nlp_result unless Xip.config.log_all_nlp_results # Already logged
|
|
220
|
+
|
|
221
|
+
raise(
|
|
222
|
+
Xip::Errors::UnrecognizedMessage,
|
|
223
|
+
"Encountered #{match_count} entity matches of type #{entity.inspect} and expected 1. To allow, set fuzzy_match to true."
|
|
224
|
+
)
|
|
225
|
+
else
|
|
226
|
+
# For single entity matches, just return the value
|
|
227
|
+
# rather than a single-element array
|
|
228
|
+
matched_entity = nlp_result.entities[entity].first
|
|
229
|
+
|
|
230
|
+
# Custom LUIS List entities return a single element array for some
|
|
231
|
+
# reason
|
|
232
|
+
if matched_entity.is_a?(Array) && matched_entity.size == 1
|
|
233
|
+
matched_entity.first
|
|
234
|
+
else
|
|
235
|
+
matched_entity
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def entities_matched?(entities, fuzzy_match)
|
|
242
|
+
nlp_entities = nlp_result.entities.deep_dup
|
|
243
|
+
results = []
|
|
244
|
+
|
|
245
|
+
entities.each do |entity|
|
|
246
|
+
# If we run out of matches for the entity type
|
|
247
|
+
# (or never had any to begin with)
|
|
248
|
+
return false if nlp_entities[entity].blank?
|
|
249
|
+
|
|
250
|
+
results << nlp_entities[entity].shift
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Check for leftover entities for the types we were looking for
|
|
254
|
+
unless fuzzy_match
|
|
255
|
+
entities.each do |entity|
|
|
256
|
+
unless nlp_entities[entity].blank?
|
|
257
|
+
log_nlp_result unless Xip.config.log_all_nlp_results # Already logged
|
|
258
|
+
leftover_count = nlp_entities[entity].size
|
|
259
|
+
raise(
|
|
260
|
+
Xip::Errors::UnrecognizedMessage,
|
|
261
|
+
"Encountered #{leftover_count} additional entity matches of type #{entity.inspect} for match #{entities.inspect}. To allow, set fuzzy_match to true."
|
|
262
|
+
)
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
results
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def log_nlp_result
|
|
271
|
+
# Log the results from the nlp_result if NLP was performed
|
|
272
|
+
if nlp_result.present?
|
|
273
|
+
Xip::Logger.l(
|
|
274
|
+
topic: :nlp,
|
|
275
|
+
message: "User #{current_session_id} -> NLP Result: #{nlp_result.parsed_result.inspect}"
|
|
276
|
+
)
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
end
|