stealth 1.1.2 → 2.0.0.beta1

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 (103) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +18 -8
  3. data/CHANGELOG.md +100 -0
  4. data/Gemfile +1 -1
  5. data/Gemfile.lock +49 -43
  6. data/LICENSE +4 -17
  7. data/README.md +9 -17
  8. data/VERSION +1 -1
  9. data/lib/stealth/base.rb +62 -13
  10. data/lib/stealth/cli.rb +1 -2
  11. data/lib/stealth/commands/console.rb +1 -1
  12. data/lib/stealth/configuration.rb +0 -3
  13. data/lib/stealth/controller/callbacks.rb +1 -1
  14. data/lib/stealth/controller/catch_all.rb +27 -4
  15. data/lib/stealth/controller/controller.rb +168 -49
  16. data/lib/stealth/controller/dev_jumps.rb +41 -0
  17. data/lib/stealth/controller/dynamic_delay.rb +4 -6
  18. data/lib/stealth/controller/interrupt_detect.rb +100 -0
  19. data/lib/stealth/controller/messages.rb +283 -0
  20. data/lib/stealth/controller/nlp.rb +50 -0
  21. data/lib/stealth/controller/replies.rb +179 -41
  22. data/lib/stealth/controller/unrecognized_message.rb +62 -0
  23. data/lib/stealth/core_ext.rb +5 -0
  24. data/lib/stealth/{flow/core_ext.rb → core_ext/numeric.rb} +0 -1
  25. data/lib/stealth/core_ext/string.rb +18 -0
  26. data/lib/stealth/dispatcher.rb +21 -0
  27. data/lib/stealth/errors.rb +12 -0
  28. data/lib/stealth/flow/base.rb +1 -2
  29. data/lib/stealth/flow/specification.rb +3 -2
  30. data/lib/stealth/flow/state.rb +3 -3
  31. data/lib/stealth/generators/builder/Gemfile +4 -3
  32. data/lib/stealth/generators/builder/bot/controllers/bot_controller.rb +42 -0
  33. data/lib/stealth/generators/builder/bot/controllers/catch_alls_controller.rb +2 -0
  34. data/lib/stealth/generators/builder/bot/controllers/goodbyes_controller.rb +2 -0
  35. data/lib/stealth/generators/builder/bot/controllers/hellos_controller.rb +2 -0
  36. data/lib/stealth/generators/builder/bot/controllers/interrupts_controller.rb +9 -0
  37. data/lib/stealth/generators/builder/bot/controllers/unrecognized_messages_controller.rb +9 -0
  38. data/lib/stealth/generators/builder/config/flow_map.rb +8 -0
  39. data/lib/stealth/generators/builder/config/initializers/autoload.rb +8 -0
  40. data/lib/stealth/generators/builder/config/initializers/inflections.rb +16 -0
  41. data/lib/stealth/generators/builder/config/puma.rb +15 -0
  42. data/lib/stealth/helpers/redis.rb +40 -0
  43. data/lib/stealth/lock.rb +83 -0
  44. data/lib/stealth/logger.rb +27 -18
  45. data/lib/stealth/nlp/client.rb +22 -0
  46. data/lib/stealth/nlp/result.rb +57 -0
  47. data/lib/stealth/reloader.rb +90 -0
  48. data/lib/stealth/reply.rb +17 -0
  49. data/lib/stealth/scheduled_reply.rb +3 -3
  50. data/lib/stealth/server.rb +4 -4
  51. data/lib/stealth/service_message.rb +3 -2
  52. data/lib/stealth/service_reply.rb +5 -1
  53. data/lib/stealth/services/base_reply_handler.rb +2 -2
  54. data/lib/stealth/session.rb +106 -53
  55. data/spec/configuration_spec.rb +9 -2
  56. data/spec/controller/callbacks_spec.rb +23 -28
  57. data/spec/controller/catch_all_spec.rb +81 -29
  58. data/spec/controller/controller_spec.rb +444 -43
  59. data/spec/controller/dynamic_delay_spec.rb +16 -18
  60. data/spec/controller/helpers_spec.rb +1 -2
  61. data/spec/controller/interrupt_detect_spec.rb +171 -0
  62. data/spec/controller/messages_spec.rb +744 -0
  63. data/spec/controller/nlp_spec.rb +93 -0
  64. data/spec/controller/replies_spec.rb +446 -11
  65. data/spec/controller/unrecognized_message_spec.rb +168 -0
  66. data/spec/dispatcher_spec.rb +79 -0
  67. data/spec/flow/flow_spec.rb +1 -2
  68. data/spec/flow/state_spec.rb +14 -3
  69. data/spec/helpers/redis_spec.rb +77 -0
  70. data/spec/lock_spec.rb +100 -0
  71. data/spec/nlp/client_spec.rb +23 -0
  72. data/spec/nlp/result_spec.rb +57 -0
  73. data/spec/replies/messages/say_msgs_without_breaks.yml +4 -0
  74. data/spec/replies/messages/say_randomize_speech.yml +10 -0
  75. data/spec/replies/messages/say_randomize_text.yml +10 -0
  76. data/spec/replies/messages/sub1/sub2/say_nested.yml +10 -0
  77. data/spec/reply_spec.rb +61 -0
  78. data/spec/scheduled_reply_spec.rb +23 -0
  79. data/spec/service_reply_spec.rb +1 -2
  80. data/spec/session_spec.rb +251 -12
  81. data/spec/spec_helper.rb +21 -0
  82. data/spec/support/controllers/vaders_controller.rb +24 -0
  83. data/spec/support/nlp_clients/dialogflow.rb +9 -0
  84. data/spec/support/nlp_clients/luis.rb +9 -0
  85. data/spec/support/nlp_results/luis_result.rb +163 -0
  86. data/spec/version_spec.rb +1 -2
  87. data/stealth.gemspec +6 -6
  88. metadata +83 -39
  89. data/docs/00-introduction.md +0 -37
  90. data/docs/01-getting-started.md +0 -21
  91. data/docs/02-local-development.md +0 -40
  92. data/docs/03-basics.md +0 -171
  93. data/docs/04-sessions.md +0 -29
  94. data/docs/05-controllers.md +0 -179
  95. data/docs/06-models.md +0 -39
  96. data/docs/07-replies.md +0 -114
  97. data/docs/08-catchalls.md +0 -49
  98. data/docs/09-messaging-integrations.md +0 -80
  99. data/docs/10-nlp-integrations.md +0 -13
  100. data/docs/11-analytics.md +0 -13
  101. data/docs/12-commands.md +0 -62
  102. data/docs/13-deployment.md +0 -50
  103. data/lib/stealth/generators/builder/config/initializers/.keep +0 -0
@@ -80,7 +80,6 @@ module Stealth
80
80
 
81
81
  $ > stealth console --engine=pry
82
82
  EOS
83
- method_option :environment, desc: 'Path to environment configuration (config/environment.rb)'
84
83
  method_option :engine, desc: "Choose a specific console engine: (#{Stealth::Commands::Console::ENGINES.keys.join('/')})"
85
84
  method_option :help, desc: 'Displays the usage method'
86
85
  def console
@@ -114,7 +113,7 @@ module Stealth
114
113
  EOS
115
114
  define_method 'sessions:clear' do
116
115
  Stealth.load_environment
117
- $redis.flushdb if Stealth.env == 'development'
116
+ $redis.flushdb if Stealth.env.development?
118
117
  end
119
118
 
120
119
 
@@ -53,7 +53,7 @@ module Stealth
53
53
  # Add convenience methods to the main:Object binding
54
54
  TOPLEVEL_BINDING.eval('self').__send__(:include, CodeReloading)
55
55
 
56
- Stealth.load_environment
56
+ Stealth.boot
57
57
  end
58
58
 
59
59
  def engine_lookup
@@ -1,8 +1,6 @@
1
1
  # coding: utf-8
2
2
  # frozen_string_literal: true
3
3
 
4
- require 'thread'
5
-
6
4
  module Stealth
7
5
  class Configuration < Hash
8
6
 
@@ -20,7 +18,6 @@ module Stealth
20
18
  if setter?(method)
21
19
  self[key] = args.first
22
20
  else
23
- super(method, args) && return unless key?(key)
24
21
  self[key]
25
22
  end
26
23
  end
@@ -13,7 +13,7 @@ module Stealth
13
13
  define_callbacks :action, skip_after_callbacks_if_terminated: true
14
14
  end
15
15
 
16
- module ClassMethods
16
+ class_methods do
17
17
  def _normalize_callback_options(options)
18
18
  _normalize_callback_option(options, :only, :if)
19
19
  _normalize_callback_option(options, :except, :unless)
@@ -9,18 +9,41 @@ module Stealth
9
9
 
10
10
  included do
11
11
 
12
- def run_catch_all(reason: nil)
12
+ def run_catch_all(err:)
13
13
  error_level = fetch_error_level
14
- Stealth::Logger.l(topic: "catch_all", message: "CatchAll #{calculate_catch_all_state(error_level)} triggered for #{error_slug}: #{reason}")
14
+
15
+ if err.class == Stealth::Errors::UnrecognizedMessage
16
+ Stealth::Logger.l(
17
+ topic: 'catch_all',
18
+ message: "[Level #{error_level}] for user #{current_session_id} #{err.message}"
19
+ )
20
+ else
21
+ Stealth::Logger.l(
22
+ topic: 'catch_all',
23
+ message: "[Level #{error_level}] for user #{current_session_id} #{[err.class, err.message, err.backtrace.join("\n")].join("\n")}"
24
+ )
25
+ end
26
+
27
+ # Store the reason so it can be accessed by the CatchAllsController
28
+ current_message.catch_all_reason = {
29
+ err: err.class,
30
+ err_msg: err.message
31
+ }
32
+
33
+ # Don't run catch_all from the catch_all controller
34
+ if current_session.flow_string == 'catch_all'
35
+ Stealth::Logger.l(topic: 'catch_all', message: "CatchAll triggered for user #{current_session_id} from within CatchAll; ignoring.")
36
+ return false
37
+ end
15
38
 
16
39
  if defined?(CatchAllsController) && FlowMap.flow_spec[:catch_all].present?
17
40
  catch_all_state = calculate_catch_all_state(error_level)
18
41
 
19
42
  if FlowMap.flow_spec[:catch_all].states.keys.include?(catch_all_state.to_sym)
20
- step_to flow: 'catch_all', state: catch_all_state
43
+ step_to flow: :catch_all, state: catch_all_state
21
44
  else
22
45
  # We are out of bounds, do nothing to prevent an infinite loop
23
- Stealth::Logger.l(topic: "catch_all", message: "Stopping; we've exceeded the number of defined catch_all states.")
46
+ Stealth::Logger.l(topic: 'catch_all', message: "Stopping; we\'ve exceeded the number of defined catch_all states for user #{current_session_id}.")
24
47
  return false
25
48
  end
26
49
  end
@@ -7,18 +7,24 @@ module Stealth
7
7
  include Stealth::Controller::Callbacks
8
8
  include Stealth::Controller::DynamicDelay
9
9
  include Stealth::Controller::Replies
10
+ include Stealth::Controller::Messages
11
+ include Stealth::Controller::UnrecognizedMessage
10
12
  include Stealth::Controller::CatchAll
11
13
  include Stealth::Controller::Helpers
14
+ include Stealth::Controller::InterruptDetect
15
+ include Stealth::Controller::DevJumps
16
+ include Stealth::Controller::Nlp
12
17
 
13
- attr_reader :current_message, :current_user_id, :current_flow,
14
- :current_service, :flow_controller, :action_name,
15
- :current_session_id
18
+ attr_reader :current_message, :current_service, :flow_controller,
19
+ :action_name, :current_session_id
20
+ attr_accessor :nlp_result, :pos
16
21
 
17
- def initialize(service_message:, current_flow: nil)
22
+ def initialize(service_message:, pos: nil)
18
23
  @current_message = service_message
19
24
  @current_service = service_message.service
20
- @current_user_id = @current_session_id = service_message.sender_id
21
- @current_flow = current_flow
25
+ @current_session_id = service_message.sender_id
26
+ @nlp_result = service_message.nlp_result
27
+ @pos = pos
22
28
  @progressed = false
23
29
  end
24
30
 
@@ -31,7 +37,7 @@ module Stealth
31
37
  end
32
38
 
33
39
  def progressed?
34
- @progressed.present?
40
+ @progressed
35
41
  end
36
42
 
37
43
  def route
@@ -41,104 +47,217 @@ module Stealth
41
47
  def flow_controller
42
48
  @flow_controller ||= begin
43
49
  flow_controller = [current_session.flow_string.pluralize, 'controller'].join('_').classify.constantize
44
- flow_controller.new(
45
- service_message: @current_message,
46
- current_flow: current_flow
47
- )
50
+ flow_controller.new(service_message: @current_message, pos: @pos)
48
51
  end
49
52
  end
50
53
 
51
54
  def current_session
52
- @current_session ||= Stealth::Session.new(user_id: current_session_id)
55
+ @current_session ||= Stealth::Session.new(id: current_session_id)
53
56
  end
54
57
 
55
58
  def previous_session
56
- @previous_session ||= Stealth::Session.new(user_id: current_session_id, previous: true)
59
+ @previous_session ||= Stealth::Session.new(
60
+ id: current_session_id,
61
+ type: :previous
62
+ )
57
63
  end
58
64
 
59
65
  def action(action: nil)
60
- @action_name = action
61
- @action_name ||= current_session.state_string
62
-
63
- # Check if the user needs to be redirected
64
- if current_session.flow.current_state.redirects_to.present?
65
- Stealth::Logger.l(
66
- topic: "redirect",
67
- message: "From #{current_session.session} to #{current_session.flow.current_state.redirects_to.session}"
66
+ begin
67
+ # Grab a mutual exclusion lock on the session
68
+ lock_session!(
69
+ session_slug: Session.slugify(
70
+ flow: current_session.flow_string,
71
+ state: current_session.state_string
72
+ )
68
73
  )
69
- step_to(session: current_session.flow.current_state.redirects_to)
70
- return
71
- end
72
74
 
73
- run_callbacks :action do
74
- begin
75
- flow_controller.send(@action_name)
76
- run_catch_all(reason: 'Did not send replies, update session, or step') unless flow_controller.progressed?
77
- rescue StandardError => e
78
- Stealth::Logger.l(topic: "catch_all", message: e.backtrace.join("\n"))
79
- run_catch_all(reason: e.message)
75
+ @action_name = action
76
+ @action_name ||= current_session.state_string
77
+
78
+ # Check if the user needs to be redirected
79
+ if current_session.flow.current_state.redirects_to.present?
80
+ Stealth::Logger.l(
81
+ topic: "redirect",
82
+ message: "From #{current_session.session} to #{current_session.flow.current_state.redirects_to.session}"
83
+ )
84
+ step_to(session: current_session.flow.current_state.redirects_to, pos: @pos)
85
+ return
86
+ end
87
+
88
+ run_callbacks :action do
89
+ begin
90
+ flow_controller.send(@action_name)
91
+ unless flow_controller.progressed?
92
+ run_catch_all(reason: 'Did not send replies, update session, or step')
93
+ end
94
+ rescue StandardError => e
95
+ if e.class == Stealth::Errors::UnrecognizedMessage
96
+ run_unrecognized_message(err: e)
97
+ else
98
+ run_catch_all(err: e)
99
+ end
100
+ end
80
101
  end
102
+ ensure
103
+ # Release mutual exclusion lock on the session
104
+ release_lock!
81
105
  end
82
106
  end
83
107
 
84
- def step_to_in(delay, session: nil, flow: nil, state: nil)
85
- flow, state = get_flow_and_state(session: session, flow: flow, state: state)
108
+ def step_to_in(delay, session: nil, flow: nil, state: nil, slug: nil)
109
+ if interrupt_detected?
110
+ run_interrupt_action
111
+ return :interrupted
112
+ end
113
+
114
+ flow, state = get_flow_and_state(
115
+ session: session,
116
+ flow: flow,
117
+ state: state,
118
+ slug: slug
119
+ )
86
120
 
87
121
  unless delay.is_a?(ActiveSupport::Duration)
88
122
  raise ArgumentError, "Please specify your step_to_in `delay` parameter using ActiveSupport::Duration, e.g. `1.day` or `5.hours`"
89
123
  end
90
124
 
91
- Stealth::ScheduledReplyJob.perform_in(delay, current_service, current_session_id, flow, state)
125
+ Stealth::ScheduledReplyJob.perform_in(delay, current_service, current_session_id, flow, state, current_message.target_id)
92
126
  Stealth::Logger.l(topic: "session", message: "User #{current_session_id}: scheduled session step to #{flow}->#{state} in #{delay} seconds")
93
127
  end
94
128
 
95
- def step_to_at(timestamp, session: nil, flow: nil, state: nil)
96
- flow, state = get_flow_and_state(session: session, flow: flow, state: state)
129
+ def step_to_at(timestamp, session: nil, flow: nil, state: nil, slug: nil)
130
+ if interrupt_detected?
131
+ run_interrupt_action
132
+ return :interrupted
133
+ end
134
+
135
+ flow, state = get_flow_and_state(
136
+ session: session,
137
+ flow: flow,
138
+ state: state,
139
+ slug: slug
140
+ )
97
141
 
98
142
  unless timestamp.is_a?(DateTime)
99
143
  raise ArgumentError, "Please specify your step_to_at `timestamp` parameter as a DateTime"
100
144
  end
101
145
 
102
- Stealth::ScheduledReplyJob.perform_at(timestamp, current_service, current_session_id, flow, state)
146
+ Stealth::ScheduledReplyJob.perform_at(timestamp, current_service, current_session_id, flow, state, current_message.target_id)
103
147
  Stealth::Logger.l(topic: "session", message: "User #{current_session_id}: scheduled session step to #{flow}->#{state} at #{timestamp.iso8601}")
104
148
  end
105
149
 
106
- def step_to(session: nil, flow: nil, state: nil)
107
- flow, state = get_flow_and_state(session: session, flow: flow, state: state)
108
- step(flow: flow, state: state)
150
+ def step_to(session: nil, flow: nil, state: nil, slug: nil, pos: nil)
151
+ if interrupt_detected?
152
+ run_interrupt_action
153
+ return :interrupted
154
+ end
155
+
156
+ flow, state = get_flow_and_state(
157
+ session: session,
158
+ flow: flow,
159
+ state: state,
160
+ slug: slug
161
+ )
162
+
163
+ step(flow: flow, state: state, pos: pos)
109
164
  end
110
165
 
111
- def update_session_to(session: nil, flow: nil, state: nil)
112
- flow, state = get_flow_and_state(session: session, flow: flow, state: state)
166
+ def update_session_to(session: nil, flow: nil, state: nil, slug: nil)
167
+ if interrupt_detected?
168
+ run_interrupt_action
169
+ return :interrupted
170
+ end
171
+
172
+ flow, state = get_flow_and_state(
173
+ session: session,
174
+ flow: flow,
175
+ state: state,
176
+ slug: slug
177
+ )
178
+
113
179
  update_session(flow: flow, state: state)
114
180
  end
115
181
 
182
+ def set_back_to(session: nil, flow: nil, state: nil, slug: nil)
183
+ if interrupt_detected?
184
+ run_interrupt_action
185
+ return :interrupted
186
+ end
187
+
188
+ flow, state = get_flow_and_state(
189
+ session: session,
190
+ flow: flow,
191
+ state: state,
192
+ slug: slug
193
+ )
194
+
195
+ store_back_to_session(flow: flow, state: state)
196
+ end
197
+
198
+ def step_back
199
+ back_to_session = Stealth::Session.new(
200
+ id: current_session_id,
201
+ type: :back_to
202
+ )
203
+
204
+ if back_to_session.blank?
205
+ raise(
206
+ Stealth::Errors::InvalidStateTransition,
207
+ 'back_to_session not found; make sure set_back_to was called first'
208
+ )
209
+ end
210
+
211
+ step_to(session: back_to_session)
212
+ end
213
+
214
+ def do_nothing
215
+ @progressed = :do_nothing
216
+ end
217
+
116
218
  private
117
219
 
118
220
  def update_session(flow:, state:)
119
- @current_session = Stealth::Session.new(user_id: current_session_id)
120
221
  @progressed = :updated_session
121
- @current_session.set(flow: flow, state: state)
222
+ @current_session = Session.new(id: current_session_id)
223
+
224
+ unless current_session.flow_string == flow.to_s && current_session.state_string == state.to_s
225
+ @current_session.set_session(new_flow: flow, new_state: state)
226
+ end
227
+ end
228
+
229
+ def store_back_to_session(flow:, state:)
230
+ back_to_session = Session.new(
231
+ id: current_session_id,
232
+ type: :back_to
233
+ )
234
+ back_to_session.set_session(new_flow: flow, new_state: state)
122
235
  end
123
236
 
124
- def step(flow:, state:)
237
+ def step(flow:, state:, pos: nil)
125
238
  update_session(flow: flow, state: state)
126
239
  @progressed = :stepped
127
240
  @flow_controller = nil
128
241
  @current_flow = current_session.flow
242
+ @pos = pos
129
243
 
130
- action(action: state)
244
+ flow_controller.action(action: state)
131
245
  end
132
246
 
133
- def get_flow_and_state(session: nil, flow: nil, state: nil)
134
- if session.nil? && flow.nil? && state.nil?
135
- raise(ArgumentError, "A session, flow, or state must be specified")
247
+ def get_flow_and_state(session: nil, flow: nil, state: nil, slug: nil)
248
+ if session.nil? && flow.nil? && state.nil? && slug.nil?
249
+ raise(ArgumentError, "A session, flow, state, or slug must be specified")
136
250
  end
137
251
 
138
252
  if session.present?
139
253
  return session.flow_string, session.state_string
140
254
  end
141
255
 
256
+ if slug.present?
257
+ flow_state = Session.flow_and_state_from_session_slug(slug: slug)
258
+ return flow_state[:flow], flow_state[:state]
259
+ end
260
+
142
261
  if flow.present?
143
262
  if state.blank?
144
263
  state = FlowMap.flow_spec[flow.to_sym].states.keys.first.to_s
@@ -0,0 +1,41 @@
1
+ # coding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module Stealth
5
+ class Controller
6
+ module DevJumps
7
+
8
+ DEV_JUMP_REGEX = /\A\/(.*)\/(.*)\z|\A\/\/(.*)\z|\A\/(.*)\z/
9
+
10
+ extend ActiveSupport::Concern
11
+
12
+ included do
13
+ private
14
+
15
+ def dev_jump_detected?
16
+ if Stealth.env.development?
17
+ if current_message.message&.match(DEV_JUMP_REGEX)
18
+ handle_dev_jump
19
+ return true
20
+ end
21
+ end
22
+
23
+ false
24
+ end
25
+
26
+ def handle_dev_jump
27
+ _, flow, state = current_message.message.split('/')
28
+ flow = nil if flow.blank?
29
+
30
+ Stealth::Logger.l(
31
+ topic: 'dev_jump',
32
+ message: "Dev Jump detected: Flow: #{flow.inspect} State: #{state.inspect}"
33
+ )
34
+
35
+ step_to flow: flow, state: state
36
+ end
37
+ end
38
+
39
+ end
40
+ end
41
+ end
@@ -12,17 +12,15 @@ module Stealth
12
12
  LONG_DELAY = 7.0
13
13
 
14
14
  included do
15
- def dynamic_delay(service_replies:, position:)
16
- if position <= 0
17
- calculate_delay(previous_reply: {})
18
- else
19
- calculate_delay(previous_reply: service_replies[position - 1])
20
- end
15
+ def dynamic_delay(previous_reply:)
16
+ calculate_delay(previous_reply: previous_reply)
21
17
  end
22
18
 
23
19
  private
24
20
 
25
21
  def calculate_delay(previous_reply:)
22
+ return SHORT_DELAY if previous_reply.blank?
23
+
26
24
  case previous_reply['reply_type']
27
25
  when 'text'
28
26
  calculate_delay_from_text(previous_reply['text'])