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
@@ -13,7 +13,8 @@ module Stealth
13
13
  magenta: 35,
14
14
  cyan: 36,
15
15
  gray: 37,
16
- light_cyan: 96
16
+ light_cyan: 96,
17
+ white: 97
17
18
  ].freeze
18
19
 
19
20
  def self.color_code(code)
@@ -26,29 +27,37 @@ module Stealth
26
27
 
27
28
  def self.log(topic:, message:)
28
29
  unless ENV['STEALTH_ENV'] == 'test'
29
- puts "#{print_topic(topic)} #{message}"
30
+ puts "TID-#{Stealth.tid} #{print_topic(topic)} #{message}"
30
31
  end
31
32
  end
32
33
 
33
34
  def self.print_topic(topic)
34
35
  topic_string = "[#{topic}]"
35
36
 
36
- case topic.to_sym
37
- when :session
38
- colorize(topic_string, color: :green)
39
- when :previous_session
40
- colorize(topic_string, color: :yellow)
41
- when :facebook, :twilio
42
- colorize(topic_string, color: :blue)
43
- when :smooch
44
- colorize(topic_string, color: :magenta)
45
- when :alexa
46
- colorize(topic_string, color: :light_cyan)
47
- when :catch_all
48
- colorize(topic_string, color: :red)
49
- else
50
- colorize(topic_string, color: :gray)
51
- end
37
+ color = case topic.to_sym
38
+ when :primary_session
39
+ :green
40
+ when :previous_session, :back_to_session
41
+ :yellow
42
+ when :interrupt
43
+ :magenta
44
+ when :facebook, :twilio, :bandwidth
45
+ :blue
46
+ when :smooch
47
+ :magenta
48
+ when :alexa, :unrecognized_message
49
+ :light_cyan
50
+ when :nlp
51
+ :cyan
52
+ when :catch_all, :err
53
+ :red
54
+ when :user
55
+ :white
56
+ else
57
+ :gray
58
+ end
59
+
60
+ colorize(topic_string, color: color)
52
61
  end
53
62
 
54
63
  class << self
@@ -0,0 +1,22 @@
1
+ # coding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module Stealth
5
+ module Nlp
6
+ class Client
7
+
8
+ def client
9
+ nil
10
+ end
11
+
12
+ def understand(query:)
13
+ nil
14
+ end
15
+
16
+ def understand_speech(audio_file:, audio_config: nil)
17
+ nil
18
+ end
19
+ end
20
+
21
+ end
22
+ end
@@ -0,0 +1,57 @@
1
+ # coding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module Stealth
5
+ module Nlp
6
+ class Result
7
+
8
+ ENTITY_TYPES = %i(number currency email percentage phone age
9
+ url ordinal geo dimension temp datetime duration
10
+ key_phrase name)
11
+
12
+ attr_reader :result
13
+
14
+ def initialize(result:)
15
+ @result = result
16
+ end
17
+
18
+ def parsed_result
19
+ nil
20
+ end
21
+
22
+ def intent_id
23
+ nil
24
+ end
25
+
26
+ def intent
27
+ nil
28
+ end
29
+
30
+ def intent_score
31
+ nil
32
+ end
33
+
34
+ def raw_entities
35
+ {}
36
+ end
37
+
38
+ def entities
39
+ {}
40
+ end
41
+
42
+ # :postive, :negative, :neutral
43
+ def sentiment
44
+ nil
45
+ end
46
+
47
+ def sentiment_score
48
+ nil
49
+ end
50
+
51
+ def present?
52
+ parsed_result.present?
53
+ end
54
+
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,90 @@
1
+ # coding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ require 'zeitwerk'
5
+
6
+ module Stealth
7
+ class Reloader
8
+
9
+ def initialize
10
+ @reloader = Class.new(ActiveSupport::Reloader)
11
+ @loader = Zeitwerk::Loader.new
12
+ # @loader.logger = method(:puts)
13
+ @loader
14
+ end
15
+
16
+ def load_bot!
17
+ load_autoload_paths!
18
+ enable_reloading!
19
+ enable_eager_load!
20
+ @loader.setup
21
+ end
22
+
23
+ def load_autoload_paths!
24
+ if Stealth.config.autoload_paths.present?
25
+ Stealth.config.autoload_paths.each do |autoload_path|
26
+ @loader.push_dir(autoload_path)
27
+ end
28
+
29
+ # Bot-specific ignores
30
+ Stealth.config.autoload_ignore_paths.each do |autoload_ignore_path|
31
+ @loader.ignore(autoload_ignore_path)
32
+ end
33
+
34
+ # Ignore setup files
35
+ @loader.ignore(File.join(Stealth.root, 'config', 'initializers'))
36
+ @loader.ignore(File.join(Stealth.root, 'config', 'boot.rb'))
37
+ @loader.ignore(File.join(Stealth.root, 'config', 'environment.rb'))
38
+ @loader.ignore(File.join(Stealth.root, 'config', 'puma.rb'))
39
+ end
40
+ end
41
+
42
+ def enable_eager_load!
43
+ if Stealth.config.eager_load
44
+ @loader.eager_load
45
+ Stealth::Logger.l(topic: 'stealth', message: 'Eager loading enabled.')
46
+ end
47
+ end
48
+
49
+ def enable_reloading!
50
+ if Stealth.config.hot_reload
51
+ @checker = ActiveSupport::EventedFileUpdateChecker.new([], files_to_watch) do
52
+ reload!
53
+ end
54
+
55
+ @loader.enable_reloading
56
+ Stealth::Logger.l(topic: 'stealth', message: 'Hot reloading enabled.')
57
+ end
58
+ end
59
+
60
+ # Only reloads if a change has been detected in one of the autoload files`
61
+ def reload
62
+ if Stealth.config.hot_reload
63
+ @checker.execute_if_updated
64
+ end
65
+ end
66
+
67
+ # Force reloads reglardless of filesystem changes
68
+ def reload!
69
+ if Stealth.config.hot_reload
70
+ @loader.reload
71
+ end
72
+ end
73
+
74
+ def call
75
+ @reloader.wrap do
76
+ reload
77
+ yield
78
+ end
79
+ end
80
+
81
+ private
82
+
83
+ def files_to_watch
84
+ Stealth.config.autoload_paths.map do |path|
85
+ [path, 'rb']
86
+ end.to_h
87
+ end
88
+
89
+ end
90
+ end
@@ -15,5 +15,22 @@ module Stealth
15
15
  @reply[key]
16
16
  end
17
17
 
18
+ def []=(key, value)
19
+ @reply[key] = value
20
+ end
21
+
22
+ def delay?
23
+ reply_type == 'delay'
24
+ end
25
+
26
+ def self.dynamic_delay
27
+ self.new(
28
+ unstructured_reply: {
29
+ 'reply_type' => 'delay',
30
+ 'duration' => 'dynamic'
31
+ }
32
+ )
33
+ end
34
+
18
35
  end
19
36
  end
@@ -6,12 +6,12 @@ module Stealth
6
6
  class ScheduledReplyJob < Stealth::Jobs
7
7
  sidekiq_options queue: :stealth_replies, retry: false
8
8
 
9
- def perform(service, user_id, flow, state)
9
+ def perform(service, user_id, flow, state, target_id=nil)
10
10
  service_message = ServiceMessage.new(service: service)
11
11
  service_message.sender_id = user_id
12
+ service_message.target_id = target_id
12
13
  controller = BotController.new(service_message: service_message)
13
- controller.update_session_to(flow: flow, state: state)
14
- controller.route
14
+ controller.step_to(flow: flow, state: state)
15
15
  end
16
16
  end
17
17
 
@@ -30,7 +30,7 @@ module Stealth
30
30
  end
31
31
 
32
32
  get_or_post '/incoming/:service' do
33
- Stealth::Logger.l(topic: "incoming", message: "Received webhook from #{params[:service]}")
33
+ Stealth::Logger.l(topic: params[:service], message: 'Received webhook.')
34
34
 
35
35
  # JSON params need to be parsed and added to the params
36
36
  if request.env['CONTENT_TYPE']&.match(/application\/json/i)
@@ -50,8 +50,8 @@ module Stealth
50
50
  private
51
51
 
52
52
  def get_helpers_from_request(request)
53
- request.env.reject do |header, value|
54
- header.match(/rack\.|puma\.|sinatra\./)
53
+ request.env.select do |header, value|
54
+ %w[HTTP_HOST].include?(header)
55
55
  end
56
56
  end
57
57
 
@@ -4,8 +4,9 @@
4
4
  module Stealth
5
5
  class ServiceMessage
6
6
 
7
- attr_accessor :sender_id, :timestamp, :service, :message, :location,
8
- :attachments, :payload, :referral
7
+ attr_accessor :sender_id, :target_id, :timestamp, :service, :message,
8
+ :location, :attachments, :payload, :referral, :nlp_result,
9
+ :catch_all_reason
9
10
 
10
11
  def initialize(service:)
11
12
  @service = service
@@ -18,7 +18,11 @@ module Stealth
18
18
  @yaml_reply
19
19
  end
20
20
 
21
- @replies = load_replies(YAML.load(processed_reply))
21
+ if yaml_reply.is_a?(Array)
22
+ @replies = load_replies(@yaml_reply)
23
+ else
24
+ @replies = load_replies(YAML.load(processed_reply))
25
+ end
22
26
  end
23
27
 
24
28
  private
@@ -8,8 +8,8 @@ module Stealth
8
8
  attr_reader :recipient_id, :reply
9
9
 
10
10
  def initialize(recipient_id:, reply:)
11
- @client = client
12
- @options = options
11
+ @recipient_id = recipient_id
12
+ @reply = reply
13
13
  end
14
14
 
15
15
  def text
@@ -4,21 +4,30 @@
4
4
  module Stealth
5
5
  class Session
6
6
 
7
+ include Stealth::Redis
8
+
7
9
  SLUG_SEPARATOR = '->'
8
10
 
9
- attr_reader :flow, :state, :user_id, :previous
11
+ attr_reader :flow, :state, :id, :type
10
12
  attr_accessor :session
11
13
 
12
- def initialize(user_id: nil, previous: false)
13
- @user_id = user_id
14
- @previous = previous
14
+ # Session types:
15
+ # - :primary
16
+ # - :previous
17
+ # - :back_to
18
+ def initialize(id: nil, type: :primary)
19
+ @id = id
20
+ @type = type
15
21
 
16
- if user_id.present?
22
+ if id.present?
17
23
  unless defined?($redis) && $redis.present?
18
- raise(Stealth::Errors::RedisNotConfigured, "Please make sure REDIS_URL is configured before using sessions")
24
+ raise(
25
+ Stealth::Errors::RedisNotConfigured,
26
+ "Please make sure REDIS_URL is configured before using sessions"
27
+ )
19
28
  end
20
29
 
21
- get
30
+ get_session
22
31
  end
23
32
 
24
33
  self
@@ -31,6 +40,14 @@ module Stealth
31
40
  }
32
41
  end
33
42
 
43
+ def self.slugify(flow:, state:)
44
+ unless flow.present? && state.present?
45
+ raise(ArgumentError, 'A flow and state must be specified.')
46
+ end
47
+
48
+ [flow, state].join(SLUG_SEPARATOR)
49
+ end
50
+
34
51
  def flow
35
52
  return nil if flow_string.blank?
36
53
 
@@ -49,31 +66,32 @@ module Stealth
49
66
  session&.split(SLUG_SEPARATOR)&.last
50
67
  end
51
68
 
52
- def get
53
- prev_key = previous_session_key(user_id: user_id)
54
-
55
- @session ||= begin
56
- if sessions_expire?
57
- previous? ? getex(prev_key) : getex(user_id)
58
- else
59
- previous? ? $redis.get(prev_key) : $redis.get(user_id)
60
- end
61
- end
69
+ def get_session
70
+ @session ||= get_key(session_key)
62
71
  end
63
72
 
64
- def set(flow:, state:)
65
- store_current_to_previous(flow: flow, state: state)
66
-
67
- @flow = nil
68
- @session = self.class.canonical_session_slug(flow: flow, state: state)
73
+ def set_session(new_flow:, new_state:)
74
+ @flow = nil # override @flow's memoization
75
+ existing_session = session # tmp backup for previous_session storage
76
+ @session = self.class.canonical_session_slug(
77
+ flow: new_flow,
78
+ state: new_state
79
+ )
80
+
81
+ Stealth::Logger.l(
82
+ topic: [type, 'session'].join('_'),
83
+ message: "User #{id}: setting session to #{new_flow}->#{new_state}"
84
+ )
85
+
86
+ if primary_session?
87
+ store_current_to_previous(existing_session: existing_session)
88
+ end
69
89
 
70
- Stealth::Logger.l(topic: "session", message: "User #{user_id}: setting session to #{flow}->#{state}")
90
+ persist_key(key: session_key, value: session)
91
+ end
71
92
 
72
- if sessions_expire?
73
- $redis.setex(user_id, Stealth.config.session_ttl, session)
74
- else
75
- $redis.set(user_id, session)
76
- end
93
+ def clear_session
94
+ $redis.del(session_key)
77
95
  end
78
96
 
79
97
  def present?
@@ -84,17 +102,16 @@ module Stealth
84
102
  !present?
85
103
  end
86
104
 
87
- def previous?
88
- @previous
89
- end
90
-
91
105
  def +(steps)
92
106
  return nil if flow.blank?
93
107
  return self if steps.zero?
94
108
 
95
109
  new_state = self.state + steps
96
- new_session = Stealth::Session.new(user_id: self.user_id)
97
- new_session.session = self.class.canonical_session_slug(flow: self.flow_string, state: new_state)
110
+ new_session = Stealth::Session.new(id: self.id)
111
+ new_session.session = self.class.canonical_session_slug(
112
+ flow: self.flow_string,
113
+ state: new_state
114
+ )
98
115
 
99
116
  new_session
100
117
  end
@@ -109,6 +126,13 @@ module Stealth
109
126
  end
110
127
  end
111
128
 
129
+ def ==(other_session)
130
+ self.flow_string == other_session.flow_string &&
131
+ self.state_string == other_session.state_string &&
132
+ self.type == other_session.type &&
133
+ self.id == other_session.id
134
+ end
135
+
112
136
  def self.is_a_session_string?(string)
113
137
  session_regex = /(.+)(#{SLUG_SEPARATOR})(.+)/
114
138
  !!string.match(session_regex)
@@ -118,34 +142,63 @@ module Stealth
118
142
  [flow, state].join(SLUG_SEPARATOR)
119
143
  end
120
144
 
145
+ def session_key
146
+ case type
147
+ when :primary
148
+ id
149
+ when :previous
150
+ previous_session_key
151
+ when :back_to
152
+ back_to_key
153
+ end
154
+ end
155
+
156
+ def primary_session?
157
+ type == :primary
158
+ end
159
+
160
+ def previous_session?
161
+ type == :previous
162
+ end
163
+
164
+ def back_to_session?
165
+ type == :back_to
166
+ end
167
+
168
+ def to_s
169
+ [flow_string, state_string].join(SLUG_SEPARATOR)
170
+ end
171
+
121
172
  private
122
173
 
123
- def previous_session_key(user_id:)
124
- [user_id, 'previous'].join('-')
174
+ def previous_session_key
175
+ [id, 'previous'].join('-')
125
176
  end
126
177
 
127
- def store_current_to_previous(flow:, state:)
128
- new_session = self.class.canonical_session_slug(flow: flow, state: state)
178
+ def back_to_key
179
+ [id, 'back_to'].join('-')
180
+ end
129
181
 
182
+ def store_current_to_previous(existing_session:)
130
183
  # Prevent previous_session from becoming current_session
131
- if new_session == session
132
- Stealth::Logger.l(topic: "previous_session", message: "User #{user_id}: skipping setting to #{session} because it is the same as current_session")
184
+ if session == existing_session
185
+ Stealth::Logger.l(
186
+ topic: "previous_session",
187
+ message: "User #{id}: skipping setting to #{session}"\
188
+ ' because it is the same as current_session'
189
+ )
133
190
  else
134
- Stealth::Logger.l(topic: "previous_session", message: "User #{user_id}: setting to #{session}")
135
- $redis.set(previous_session_key(user_id: user_id), session)
191
+ Stealth::Logger.l(
192
+ topic: "previous_session",
193
+ message: "User #{id}: setting to #{existing_session}"
194
+ )
195
+
196
+ persist_key(
197
+ key: previous_session_key,
198
+ value: existing_session
199
+ )
136
200
  end
137
201
  end
138
202
 
139
- def sessions_expire?
140
- Stealth.config.session_ttl > 0
141
- end
142
-
143
- def getex(key)
144
- $redis.multi do
145
- $redis.expire(key, Stealth.config.session_ttl)
146
- $redis.get(key)
147
- end.last
148
- end
149
-
150
203
  end
151
204
  end