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
@@ -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,10 +30,10 @@ 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
- if request.env['CONTENT_TYPE'].match(/application\/json/i)
36
+ if request.env['CONTENT_TYPE']&.match(/application\/json/i)
37
37
  json_params = MultiJson.load(request.body.read)
38
38
  params.merge!(json_params)
39
39
  end
@@ -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,32 +142,61 @@ module Stealth
118
142
  [flow, state].join(SLUG_SEPARATOR)
119
143
  end
120
144
 
121
- private
122
-
123
- def previous_session_key(user_id:)
124
- [user_id, 'previous'].join('-')
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
125
153
  end
154
+ end
126
155
 
127
- def store_current_to_previous(flow:, state:)
128
- new_session = self.class.canonical_session_slug(flow: flow, state: state)
156
+ def primary_session?
157
+ type == :primary
158
+ end
129
159
 
130
- # 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")
133
- 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)
136
- end
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
+
172
+ private
173
+
174
+ def previous_session_key
175
+ [id, 'previous'].join('-')
137
176
  end
138
177
 
139
- def sessions_expire?
140
- Stealth.config.session_ttl > 0
178
+ def back_to_key
179
+ [id, 'back_to'].join('-')
141
180
  end
142
181
 
143
- def getex(key)
144
- $redis.multi do
145
- $redis.expire(key, Stealth.config.session_ttl)
146
- $redis.get(key)
182
+ def store_current_to_previous(existing_session:)
183
+ # Prevent previous_session from becoming 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
+ )
190
+ else
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
+ )
147
200
  end
148
201
  end
149
202