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.
Files changed (146) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +116 -0
  3. data/.gitignore +12 -0
  4. data/CHANGELOG.md +135 -0
  5. data/Gemfile +4 -1
  6. data/Gemfile.lock +65 -15
  7. data/LICENSE +6 -4
  8. data/README.md +51 -1
  9. data/VERSION +1 -0
  10. data/bin/xip +3 -11
  11. data/lib/xip.rb +1 -3
  12. data/lib/xip/base.rb +189 -0
  13. data/lib/xip/cli.rb +273 -0
  14. data/lib/xip/cli_base.rb +24 -0
  15. data/lib/xip/commands/command.rb +13 -0
  16. data/lib/xip/commands/console.rb +74 -0
  17. data/lib/xip/commands/server.rb +63 -0
  18. data/lib/xip/configuration.rb +56 -0
  19. data/lib/xip/controller/callbacks.rb +63 -0
  20. data/lib/xip/controller/catch_all.rb +84 -0
  21. data/lib/xip/controller/controller.rb +274 -0
  22. data/lib/xip/controller/dev_jumps.rb +40 -0
  23. data/lib/xip/controller/dynamic_delay.rb +61 -0
  24. data/lib/xip/controller/helpers.rb +128 -0
  25. data/lib/xip/controller/interrupt_detect.rb +99 -0
  26. data/lib/xip/controller/messages.rb +283 -0
  27. data/lib/xip/controller/nlp.rb +49 -0
  28. data/lib/xip/controller/replies.rb +281 -0
  29. data/lib/xip/controller/unrecognized_message.rb +61 -0
  30. data/lib/xip/core_ext.rb +5 -0
  31. data/lib/xip/core_ext/numeric.rb +10 -0
  32. data/lib/xip/core_ext/string.rb +18 -0
  33. data/lib/xip/dispatcher.rb +68 -0
  34. data/lib/xip/errors.rb +55 -0
  35. data/lib/xip/flow/base.rb +69 -0
  36. data/lib/xip/flow/specification.rb +56 -0
  37. data/lib/xip/flow/state.rb +82 -0
  38. data/lib/xip/generators/builder.rb +41 -0
  39. data/lib/xip/generators/builder/.gitignore +30 -0
  40. data/lib/xip/generators/builder/Gemfile +19 -0
  41. data/lib/xip/generators/builder/Procfile.dev +2 -0
  42. data/lib/xip/generators/builder/README.md +9 -0
  43. data/lib/xip/generators/builder/Rakefile +2 -0
  44. data/lib/xip/generators/builder/bot/controllers/bot_controller.rb +55 -0
  45. data/lib/xip/generators/builder/bot/controllers/catch_alls_controller.rb +21 -0
  46. data/lib/xip/generators/builder/bot/controllers/concerns/.keep +0 -0
  47. data/lib/xip/generators/builder/bot/controllers/goodbyes_controller.rb +9 -0
  48. data/lib/xip/generators/builder/bot/controllers/hellos_controller.rb +9 -0
  49. data/lib/xip/generators/builder/bot/controllers/interrupts_controller.rb +9 -0
  50. data/lib/xip/generators/builder/bot/controllers/unrecognized_messages_controller.rb +9 -0
  51. data/lib/xip/generators/builder/bot/helpers/bot_helper.rb +2 -0
  52. data/lib/xip/generators/builder/bot/models/bot_record.rb +3 -0
  53. data/lib/xip/generators/builder/bot/models/concerns/.keep +0 -0
  54. data/lib/xip/generators/builder/bot/replies/catch_alls/level1.yml +2 -0
  55. data/lib/xip/generators/builder/bot/replies/goodbyes/say_goodbye.yml +2 -0
  56. data/lib/xip/generators/builder/bot/replies/hellos/say_hello.yml +2 -0
  57. data/lib/xip/generators/builder/config.ru +4 -0
  58. data/lib/xip/generators/builder/config/boot.rb +6 -0
  59. data/lib/xip/generators/builder/config/database.yml +25 -0
  60. data/lib/xip/generators/builder/config/environment.rb +2 -0
  61. data/lib/xip/generators/builder/config/flow_map.rb +25 -0
  62. data/lib/xip/generators/builder/config/initializers/autoload.rb +8 -0
  63. data/lib/xip/generators/builder/config/initializers/inflections.rb +16 -0
  64. data/lib/xip/generators/builder/config/puma.rb +25 -0
  65. data/lib/xip/generators/builder/config/services.yml +35 -0
  66. data/lib/xip/generators/builder/config/sidekiq.yml +3 -0
  67. data/lib/xip/generators/builder/db/seeds.rb +7 -0
  68. data/lib/xip/generators/generate.rb +39 -0
  69. data/lib/xip/generators/generate/flow/controllers/controller.tt +7 -0
  70. data/lib/xip/generators/generate/flow/helpers/helper.tt +3 -0
  71. data/lib/xip/generators/generate/flow/replies/ask_example.tt +9 -0
  72. data/lib/xip/helpers/redis.rb +40 -0
  73. data/lib/xip/jobs.rb +9 -0
  74. data/lib/xip/lock.rb +82 -0
  75. data/lib/xip/logger.rb +9 -3
  76. data/lib/xip/migrations/configurator.rb +73 -0
  77. data/lib/xip/migrations/generators.rb +16 -0
  78. data/lib/xip/migrations/railtie_config.rb +14 -0
  79. data/lib/xip/migrations/tasks.rb +43 -0
  80. data/lib/xip/nlp/client.rb +21 -0
  81. data/lib/xip/nlp/result.rb +56 -0
  82. data/lib/xip/reloader.rb +89 -0
  83. data/lib/xip/reply.rb +36 -0
  84. data/lib/xip/scheduled_reply.rb +18 -0
  85. data/lib/xip/server.rb +63 -0
  86. data/lib/xip/service_message.rb +17 -0
  87. data/lib/xip/service_reply.rb +44 -0
  88. data/lib/xip/services/base_client.rb +24 -0
  89. data/lib/xip/services/base_message_handler.rb +27 -0
  90. data/lib/xip/services/base_reply_handler.rb +72 -0
  91. data/lib/xip/services/jobs/handle_message_job.rb +21 -0
  92. data/lib/xip/session.rb +203 -0
  93. data/lib/xip/version.rb +7 -1
  94. data/logo.svg +17 -0
  95. data/spec/configuration_spec.rb +93 -0
  96. data/spec/controller/callbacks_spec.rb +217 -0
  97. data/spec/controller/catch_all_spec.rb +154 -0
  98. data/spec/controller/controller_spec.rb +889 -0
  99. data/spec/controller/dynamic_delay_spec.rb +70 -0
  100. data/spec/controller/helpers_spec.rb +119 -0
  101. data/spec/controller/interrupt_detect_spec.rb +171 -0
  102. data/spec/controller/messages_spec.rb +744 -0
  103. data/spec/controller/nlp_spec.rb +93 -0
  104. data/spec/controller/replies_spec.rb +694 -0
  105. data/spec/controller/unrecognized_message_spec.rb +168 -0
  106. data/spec/dispatcher_spec.rb +79 -0
  107. data/spec/flow/flow_spec.rb +82 -0
  108. data/spec/flow/state_spec.rb +109 -0
  109. data/spec/helpers/redis_spec.rb +77 -0
  110. data/spec/lock_spec.rb +100 -0
  111. data/spec/nlp/client_spec.rb +23 -0
  112. data/spec/nlp/result_spec.rb +57 -0
  113. data/spec/replies/hello.yml.erb +15 -0
  114. data/spec/replies/messages/say_hola.yml+facebook.erb +6 -0
  115. data/spec/replies/messages/say_hola.yml+twilio.erb +6 -0
  116. data/spec/replies/messages/say_hola.yml.erb +6 -0
  117. data/spec/replies/messages/say_howdy_with_dynamic.yml +79 -0
  118. data/spec/replies/messages/say_msgs_without_breaks.yml +4 -0
  119. data/spec/replies/messages/say_offer.yml +6 -0
  120. data/spec/replies/messages/say_offer_with_dynamic.yml +6 -0
  121. data/spec/replies/messages/say_oi.yml.erb +15 -0
  122. data/spec/replies/messages/say_randomize_speech.yml +10 -0
  123. data/spec/replies/messages/say_randomize_text.yml +10 -0
  124. data/spec/replies/messages/say_yo.yml +6 -0
  125. data/spec/replies/messages/say_yo.yml+twitter +6 -0
  126. data/spec/replies/messages/sub1/sub2/say_nested.yml +10 -0
  127. data/spec/reply_spec.rb +61 -0
  128. data/spec/scheduled_reply_spec.rb +23 -0
  129. data/spec/service_reply_spec.rb +92 -0
  130. data/spec/session_spec.rb +366 -0
  131. data/spec/spec_helper.rb +22 -66
  132. data/spec/support/alternate_helpers/foo_helper.rb +5 -0
  133. data/spec/support/controllers/vaders_controller.rb +24 -0
  134. data/spec/support/helpers/fun/games_helper.rb +7 -0
  135. data/spec/support/helpers/fun/pdf_helper.rb +7 -0
  136. data/spec/support/helpers/standalone_helper.rb +5 -0
  137. data/spec/support/helpers_typo/users_helper.rb +2 -0
  138. data/spec/support/nlp_clients/dialogflow.rb +9 -0
  139. data/spec/support/nlp_clients/luis.rb +9 -0
  140. data/spec/support/nlp_results/luis_result.rb +163 -0
  141. data/spec/support/sample_messages.rb +66 -0
  142. data/spec/support/services.yml +31 -0
  143. data/spec/support/services_with_erb.yml +31 -0
  144. data/spec/version_spec.rb +16 -0
  145. data/xip.gemspec +25 -14
  146. 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