stealth 1.1.3 → 2.0.0.beta2

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 +93 -0
  4. data/Gemfile +1 -1
  5. data/Gemfile.lock +53 -48
  6. data/LICENSE +4 -17
  7. data/README.md +9 -17
  8. data/VERSION +1 -1
  9. data/lib/stealth/base.rb +72 -19
  10. data/lib/stealth/cli.rb +1 -2
  11. data/lib/stealth/commands/console.rb +1 -1
  12. data/lib/stealth/configuration.rb +6 -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 +183 -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 +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 +10 -2
  54. data/lib/stealth/session.rb +106 -53
  55. data/spec/configuration_spec.rb +42 -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
@@ -19,7 +19,7 @@ module Stealth
19
19
 
20
20
  private
21
21
 
22
- def state(name, fails_to: nil, redirects_to: nil)
22
+ def state(name, fails_to: nil, redirects_to: nil, **opts)
23
23
  fail_state = get_fail_or_redirect_state(fails_to)
24
24
  redirect_state = get_fail_or_redirect_state(redirects_to)
25
25
 
@@ -27,7 +27,8 @@ module Stealth
27
27
  name: name,
28
28
  spec: self,
29
29
  fails_to: fail_state,
30
- redirects_to: redirect_state
30
+ redirects_to: redirect_state,
31
+ opts: opts
31
32
  )
32
33
 
33
34
  @initial_state = new_state if @states.empty?
@@ -8,9 +8,9 @@ module Stealth
8
8
  include Comparable
9
9
 
10
10
  attr_accessor :name
11
- attr_reader :spec, :fails_to, :redirects_to
11
+ attr_reader :spec, :fails_to, :redirects_to, :opts
12
12
 
13
- def initialize(name:, spec:, fails_to: nil, redirects_to: nil)
13
+ def initialize(name:, spec:, fails_to: nil, redirects_to: nil, opts:)
14
14
  if fails_to.present? && !fails_to.is_a?(Stealth::Session)
15
15
  raise(ArgumentError, 'fails_to state should be a Stealth::Session')
16
16
  end
@@ -20,7 +20,7 @@ module Stealth
20
20
  end
21
21
 
22
22
  @name, @spec = name, spec
23
- @fails_to, @redirects_to = fails_to, redirects_to
23
+ @fails_to, @redirects_to, @opts = fails_to, redirects_to, opts
24
24
  end
25
25
 
26
26
  def <=>(other_state)
@@ -1,9 +1,9 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- gem 'stealth', '~> 1.0'
3
+ gem 'stealth', '~> 2.0'
4
4
 
5
- gem 'railties', '~> 5.2'
6
- gem 'activerecord', '~> 5.2'
5
+ gem 'railties', '~> 6.0'
6
+ gem 'activerecord', '~> 6.0'
7
7
  # Use sqlite3 as the database for Active Record
8
8
  gem 'sqlite3'
9
9
 
@@ -15,4 +15,5 @@ gem 'sqlite3'
15
15
 
16
16
  group :development do
17
17
  gem 'foreman'
18
+ gem 'listen', '~> 3.3'
18
19
  end
@@ -1,8 +1,24 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class BotController < Stealth::Controller
2
4
 
3
5
  helper :all
4
6
 
5
7
  def route
8
+ if current_message.payload.present?
9
+ handle_payloads
10
+ # Clear out the payload to prevent duplicate handling
11
+ current_message.payload = nil
12
+ return
13
+ end
14
+
15
+ # Allow devs to jump around flows and states by typing:
16
+ # /flow_name/state_name or
17
+ # /flow_name (jumps to first state) or
18
+ # //state_name (jumps to state in current flow)
19
+ # (only works for bots in development)
20
+ return if dev_jump_detected?
21
+
6
22
  if current_session.present?
7
23
  step_to session: current_session
8
24
  else
@@ -10,4 +26,30 @@ class BotController < Stealth::Controller
10
26
  end
11
27
  end
12
28
 
29
+ private
30
+
31
+ # Handle payloads globally since payload buttons remain in the chat
32
+ # and we cannot guess in which states they will be tapped.
33
+ def handle_payloads
34
+ case current_message.payload
35
+ when 'developer_restart', 'new_user'
36
+ step_to flow: 'hello', state: 'say_hello'
37
+ when 'goodbye'
38
+ step_to flow: 'goodbye'
39
+ end
40
+ end
41
+
42
+ # Automatically called when clients receive an opt-out error from
43
+ # the platform. You can write your own steps for handling.
44
+ def handle_opt_out
45
+ do_nothing
46
+ end
47
+
48
+ # Automatically called when clients receive an invalid session_id error from
49
+ # the platform. For example, attempting to text a landline.
50
+ # You can write your own steps for handling.
51
+ def handle_invalid_session_id
52
+ do_nothing
53
+ end
54
+
13
55
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class CatchAllsController < BotController
2
4
 
3
5
  def level1
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class GoodbyesController < BotController
2
4
 
3
5
  def say_goodbye
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class HellosController < BotController
2
4
 
3
5
  def say_hello
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class InterruptsController < BotController
4
+
5
+ def say_interrupted
6
+ do_nothing
7
+ end
8
+
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UnrecognizedMessagesController < BotController
4
+
5
+ def handle_unrecognized_message
6
+ do_nothing
7
+ end
8
+
9
+ end
@@ -10,6 +10,14 @@ class FlowMap
10
10
  state :say_goodbye
11
11
  end
12
12
 
13
+ flow :interrupt do
14
+ state :say_interrupted
15
+ end
16
+
17
+ flow :unrecognized_message do
18
+ state :handle_unrecognized_message
19
+ end
20
+
13
21
  flow :catch_all do
14
22
  state :level1
15
23
  end
@@ -0,0 +1,8 @@
1
+ # Add additional directories below for hot-reloading during development.
2
+ # You'll want to include any custom directories you create for your code.
3
+ # To stop certain files or directories from autoloading, use autoload_ignore_paths
4
+
5
+ # Stealth.config.autoload_paths << File.join(Stealth.root, 'bot', 'services')
6
+ # Stealth.config.autoload_paths << File.join(Stealth.root, 'bot', 'jobs')
7
+
8
+ # Stealth.config.autoload_ignore_paths << File.join(Stealth.root, 'bot', 'overrides')
@@ -0,0 +1,16 @@
1
+ # Be sure to restart your server when you modify this file.
2
+
3
+ # Add new inflection rules using the following format. Inflections
4
+ # are locale specific, and you may define rules for as many different
5
+ # locales as you wish. All of these examples are active by default:
6
+ # ActiveSupport::Inflector.inflections(:en) do |inflect|
7
+ # inflect.plural /^(ox)$/i, '\1en'
8
+ # inflect.singular /^(ox)en/i, '\1'
9
+ # inflect.irregular 'person', 'people'
10
+ # inflect.uncountable %w( fish sheep )
11
+ # end
12
+
13
+ # These inflection rules are supported but not enabled by default:
14
+ # ActiveSupport::Inflector.inflections(:en) do |inflect|
15
+ # inflect.acronym 'RESTful'
16
+ # end
@@ -5,6 +5,21 @@ threads threads_count, threads_count
5
5
  #
6
6
  port ENV.fetch("PORT") { 3000 }
7
7
 
8
+ # Specifies the number of `workers` to boot in clustered mode.
9
+ # Workers are forked webserver processes. If using threads and workers together
10
+ # the concurrency of the application would be max `threads` * `workers`.
11
+ # Workers do not work on JRuby or Windows (both of which do not support
12
+ # processes).
13
+ #
14
+ # workers ENV.fetch("WEB_CONCURRENCY") { 2 }
15
+
16
+ # Use the `preload_app!` method when specifying a `workers` number.
17
+ # This directive tells Puma to first boot the application and load code
18
+ # before forking the application. This takes advantage of Copy On Write
19
+ # process behavior so workers use less memory.
20
+ #
21
+ # preload_app!
22
+
8
23
  # Specifies the `environment` that Puma will run in.
9
24
  #
10
25
  environment ENV.fetch("STEALTH_ENV") { "development" }
@@ -0,0 +1,40 @@
1
+ # coding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module Stealth
5
+ module Redis
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ private
10
+
11
+ def get_key(key, expiration: Stealth.config.session_ttl)
12
+ if expiration > 0
13
+ getex(key, expiration)
14
+ else
15
+ $redis.get(key)
16
+ end
17
+ end
18
+
19
+ def delete_key(key)
20
+ $redis.del(key)
21
+ end
22
+
23
+ def getex(key, expiration=Stealth.config.session_ttl)
24
+ $redis.multi do
25
+ $redis.expire(key, expiration)
26
+ $redis.get(key)
27
+ end.last
28
+ end
29
+
30
+ def persist_key(key:, value:, expiration: Stealth.config.session_ttl)
31
+ if expiration > 0
32
+ $redis.setex(key, expiration, value)
33
+ else
34
+ $redis.set(key, value)
35
+ end
36
+ end
37
+
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,83 @@
1
+ # coding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module Stealth
5
+ class Lock
6
+
7
+ include Stealth::Redis
8
+
9
+ attr_accessor :session_id, :session_slug, :position, :tid
10
+
11
+ def initialize(session_id:, session_slug: nil, position: nil)
12
+ @session_id = session_id
13
+ @session_slug = session_slug
14
+ @position = position
15
+ @tid = Stealth.tid
16
+ end
17
+
18
+ def self.find_lock(session_id:)
19
+ lock = Lock.new(session_id: session_id)
20
+ lock_slug = lock.slug # fetch lock from Redis
21
+
22
+ return if lock_slug.nil?
23
+
24
+ # parse the lock slug
25
+ tid_and_session_slug, position = lock_slug.split(':')
26
+ tid, session_slug = tid_and_session_slug.split('#')
27
+
28
+ # set the values from the slug to the lock object
29
+ lock.session_slug = session_slug
30
+ lock.position = position&.to_i
31
+ lock.tid = tid
32
+ lock
33
+ end
34
+
35
+ def create
36
+ if session_slug.blank?
37
+ raise(
38
+ ArgumentError,
39
+ 'A session_slug must be specified before a lock can be created.'
40
+ )
41
+ end
42
+
43
+ # Expire locks after 30 seconds to prevent zombie locks from blocking
44
+ # other threads to interact with a session.
45
+ persist_key(
46
+ key: lock_key,
47
+ value: generate_lock,
48
+ expiration: Stealth.config.lock_autorelease
49
+ )
50
+ end
51
+
52
+ def release
53
+ delete_key(lock_key)
54
+ end
55
+
56
+ def slug
57
+ # We don't want to extend the expiration time that would result if
58
+ # we specified one here.
59
+ get_key(lock_key, expiration: 0)
60
+ end
61
+
62
+ # Returns a hash:
63
+ # { flow: 'flow_name', state: 'state_name' }
64
+ def flow_and_state
65
+ Session.flow_and_state_from_session_slug(slug: session_slug)
66
+ end
67
+
68
+ private
69
+
70
+ def lock_key
71
+ [@session_id, 'lock'].join('-')
72
+ end
73
+
74
+ def generate_lock
75
+ if @position.present?
76
+ @session_slug = [@session_slug, @position].join(':')
77
+ end
78
+
79
+ [@tid, @session_slug].join('#')
80
+ end
81
+
82
+ end
83
+ end
@@ -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, :voice, :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