stealth 1.1.3 → 2.0.0.beta2

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 +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