stealth 1.1.6 → 2.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (103) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +15 -54
  3. data/CHANGELOG.md +72 -0
  4. data/Gemfile +1 -1
  5. data/Gemfile.lock +49 -44
  6. data/LICENSE +4 -17
  7. data/README.md +9 -17
  8. data/VERSION +1 -1
  9. data/lib/stealth/base.rb +62 -15
  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 +178 -40
  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 +3 -3
  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 -38
  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'])