alexa-rails 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +165 -0
  4. data/Rakefile +36 -0
  5. data/app/assets/config/alexa_manifest.js +2 -0
  6. data/app/assets/javascripts/alexa/application.js +13 -0
  7. data/app/assets/stylesheets/alexa/application.css +15 -0
  8. data/app/controllers/alexa/application_controller.rb +6 -0
  9. data/app/controllers/alexa/intent_handlers_controller.rb +49 -0
  10. data/app/helpers/alexa/application_helper.rb +4 -0
  11. data/app/helpers/alexa/context_helper.rb +23 -0
  12. data/app/helpers/alexa/render_helper.rb +24 -0
  13. data/app/jobs/alexa/application_job.rb +4 -0
  14. data/app/mailers/alexa/application_mailer.rb +6 -0
  15. data/app/models/alexa/application_record.rb +5 -0
  16. data/app/models/alexa/usage.rb +11 -0
  17. data/app/models/alexa/user.rb +17 -0
  18. data/app/views/alexa/_response.json.erb +18 -0
  19. data/app/views/alexa/output/_card.json.erb +13 -0
  20. data/app/views/alexa/output/_ssml.json.erb +7 -0
  21. data/app/views/alexa/output/cards/_ask_for_permissions_consent.json.erb +6 -0
  22. data/app/views/alexa/output/cards/_simple.json.erb +5 -0
  23. data/app/views/alexa/permission_consents/device_address.text.erb +7 -0
  24. data/app/views/layouts/alexa/application.html.erb +14 -0
  25. data/config/routes.rb +3 -0
  26. data/lib/alexa/context.rb +38 -0
  27. data/lib/alexa/device.rb +61 -0
  28. data/lib/alexa/engine.rb +5 -0
  29. data/lib/alexa/intent_handlers/base.rb +158 -0
  30. data/lib/alexa/request.rb +97 -0
  31. data/lib/alexa/response.rb +85 -0
  32. data/lib/alexa/responses/bye.rb +26 -0
  33. data/lib/alexa/responses/delegate.rb +18 -0
  34. data/lib/alexa/responses/permission_consents/device_address.rb +28 -0
  35. data/lib/alexa/session.rb +35 -0
  36. data/lib/alexa/slot.rb +50 -0
  37. data/lib/alexa/version.rb +3 -0
  38. data/lib/alexa-rails.rb +19 -0
  39. data/lib/generators/alexa/migrations_generator.rb +22 -0
  40. data/lib/generators/alexa/templates/create_alexa_usages.rb +13 -0
  41. data/lib/generators/alexa/templates/create_alexa_users.rb +13 -0
  42. data/lib/tasks/alexa_tasks.rake +4 -0
  43. metadata +115 -0
@@ -0,0 +1,158 @@
1
+ module Alexa
2
+ module IntentHandlers
3
+ class Base
4
+ class << self
5
+ def inherited(subclass)
6
+ subclass.instance_variable_set("@_required_slot_names", _required_slot_names.clone)
7
+ end
8
+
9
+ # Lets you set +required_slot_names+ per subclass
10
+ #
11
+ # class IntentHandlers::NewIntent < IntentHandlers::Base
12
+ # required_slot_names :Function, :CareerLevel
13
+ # end
14
+ #
15
+ # handler = IntentHandlers::NewIntent.new
16
+ # handler.required_slot_names # => [:Function, :CareerLevel]
17
+
18
+ def _required_slot_names
19
+ @_required_slot_names ||= []
20
+ end
21
+
22
+ def required_slot_names(*names)
23
+ @_required_slot_names = names.map(&:to_s)
24
+ end
25
+ end
26
+
27
+ attr_accessor :context
28
+
29
+ def initialize(alexa_context)
30
+ @context = alexa_context
31
+ end
32
+
33
+ def request
34
+ context.request
35
+ end
36
+
37
+ def handle
38
+ raise "Override .handle"
39
+ end
40
+
41
+ def session
42
+ request.session
43
+ end
44
+
45
+ def response
46
+ @_response ||= Alexa::Response.new(intent: self)
47
+ end
48
+
49
+ def say_welcome?
50
+ @say_welcome == true
51
+ end
52
+
53
+ def slots
54
+ request.slots
55
+ end
56
+
57
+ def intent_usage_count
58
+ @_usage_count ||= context.user.usage_count_for(intent_name: request.intent_name)
59
+ end
60
+
61
+ def show_device_address_permission_consent_card?
62
+ @_show_device_address_permission_consent_card == true
63
+ end
64
+
65
+
66
+ protected
67
+
68
+ def has_all_slots?
69
+ empty_slots.empty?
70
+ end
71
+
72
+ def has_required_slots?
73
+ non_empty_slot_names = non_empty_slots.map { |_, s| s.name }
74
+ (required_slot_names - non_empty_slot_names).empty?
75
+ end
76
+
77
+ def empty_slots
78
+ @_empty_slots ||= slots.select { |name, slot| slot.value.nil? }
79
+ end
80
+
81
+ def non_empty_slots
82
+ @_non_empty_slots ||= slots.select { |name, slot| !slot.value.nil? }
83
+ end
84
+
85
+ def unmatched_slots
86
+ @_unmatched_slots ||= slots.select { |_, slot| slot.bad_match? }
87
+ end
88
+
89
+ def required_slot_names
90
+ self.class._required_slot_names
91
+ end
92
+
93
+ def intent_confirmed?
94
+ # TODO: Move this to Alexa::Request
95
+ request.params["request"]["intent"]["confirmationStatus"] == "CONFIRMED"
96
+ end
97
+
98
+ def intent_denied?
99
+ # TODO: Move this to Alexa::Request
100
+ request.params["request"]["intent"]["confirmationStatus"] == "DENIED"
101
+ end
102
+
103
+ def dialog_complete?
104
+ dialog_state == "COMPLETED"
105
+ end
106
+
107
+ def has_unmatched_slots?
108
+ unmatched_slots.any?
109
+ end
110
+
111
+ def dialog_state
112
+ request.dialog_state
113
+ end
114
+
115
+ def sorry_response
116
+ {
117
+ "version": "1.0",
118
+ sessionAttributes: session,
119
+ "response": {
120
+ "outputSpeech": {
121
+ "type": "PlainText",
122
+ "ssml": "Sorry! Looks like I couldn't process your request, may be try something else?"
123
+ },
124
+ "shouldEndSession": false
125
+ }
126
+ }
127
+ end
128
+
129
+ def show_device_address_permission_consent_card!
130
+ @_show_device_address_permission_consent_card = true
131
+ end
132
+
133
+ def device_address_permission_consent_response
134
+ @_device_address_permission_consent_response ||= Alexa::Responses::PermissionConsents::DeviceAddress.new(intent: self)
135
+ end
136
+
137
+ def delegate_response
138
+ @_delegate_response ||= Alexa::Responses::Delegate.new
139
+ end
140
+
141
+ def bye_response
142
+ @_bye_response ||= Alexa::Responses::Bye.new(intent: self)
143
+ end
144
+
145
+ def say_welcome!
146
+ @say_welcome = true
147
+ end
148
+
149
+ def video_supported?
150
+ context.device.video_supported?
151
+ end
152
+
153
+ def audio_supported?
154
+ context.device.audio_supported?
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,97 @@
1
+ module Alexa
2
+ class Request
3
+ attr_accessor :body, :params
4
+
5
+ def initialize(request)
6
+ @req = request
7
+ @body = request.body
8
+ @params = if request.body.size > 0
9
+ request.body.rewind
10
+ JSON.parse(request.body.read).with_indifferent_access
11
+ else
12
+ {}
13
+ end
14
+ end
15
+
16
+ def application_id
17
+ session.application_id
18
+ end
19
+
20
+ def user_id
21
+ session.user_id
22
+ end
23
+
24
+ def type
25
+ params["request"]["type"]
26
+ end
27
+
28
+ def intent_request?
29
+ type == "IntentRequest"
30
+ end
31
+
32
+ def launch_request?
33
+ type == "LaunchRequest"
34
+ end
35
+
36
+ def session_ended_request?
37
+ type == "SessionEndedRequest"
38
+ end
39
+
40
+ def help_request?
41
+ intent_request? && intent_name == "AMAZON.HelpIntent"
42
+ end
43
+
44
+ def cancel_request?
45
+ intent_request? && intent_name == "AMAZON.CancelIntent"
46
+ end
47
+
48
+ def session
49
+ @_session ||= Alexa::Session.new(params["session"].dup)
50
+ end
51
+
52
+ def slots
53
+ @_slots ||= begin
54
+ if intent_request?
55
+ return [] if help_request?
56
+ return [] if cancel_request?
57
+
58
+ params["request"]["intent"]["slots"]
59
+ .inject(HashWithIndifferentAccess.new) do |hash, slot|
60
+ name = slot[0]
61
+ data = slot[1]
62
+ hash[name] = Alexa::Slot.new(data)
63
+ hash
64
+ end
65
+ else
66
+ []
67
+ end
68
+ end
69
+ end
70
+
71
+ def dialog_state
72
+ params["request"]["dialogState"]
73
+ end
74
+
75
+ def valid?
76
+ Rails.configuration.x.alexa.skill_ids.include?(application_id)
77
+ end
78
+
79
+ def intent_name
80
+ return nil if !intent_request?
81
+ params["request"]["intent"]["name"]
82
+ end
83
+
84
+ def locale
85
+ request_locale = params["request"]["locale"]
86
+ case params["request"]["locale"]
87
+ when "en-US", "en-GB", "en-IN", 'en-CA'
88
+ # this is to match pjpp locale codes
89
+ params["request"]["locale"].gsub("-", "_").downcase
90
+ when "de-DE"
91
+ "de"
92
+ else
93
+ "en_gb"
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,85 @@
1
+ module Alexa
2
+ class Response
3
+ attr_accessor :intent, :directives
4
+
5
+ def initialize(intent:, directives: [])
6
+ @intent = intent
7
+ @directives = directives
8
+ @slots_to_not_render_elicitation = []
9
+ end
10
+
11
+ # Marks a slot for elicitation.
12
+ #
13
+ # Options:
14
+ # - skip_render: Lets you skip the rendering of the elicited slot's view.
15
+ # Helpful when you have the elication text already in the
16
+ # response and don't wanna override it.
17
+ def elicit_slot!(slot_to_elicit, skip_render: false)
18
+ directives << {
19
+ type: "Dialog.ElicitSlot",
20
+ slotToElicit: slot_to_elicit
21
+ }
22
+
23
+ if skip_render
24
+ @slots_to_not_render_elicitation << slot_to_elicit
25
+ end
26
+ end
27
+
28
+ def partial_path(format: :ssml, filename: nil)
29
+ if elicit_directives.any?
30
+ slot_to_elicit = elicit_directives.first[:slotToElicit]
31
+ end
32
+
33
+ template_path = "alexa/#{intent.context.locale}/intent_handlers/"\
34
+ "#{intent.class.name.demodulize.underscore}"
35
+
36
+ if filename.present?
37
+ if format == :ssml
38
+ "#{template_path}/#{filename}.ssml.erb"
39
+ else
40
+ "#{template_path}/#{filename}.text.erb"
41
+ end
42
+ else
43
+ if intent.show_device_address_permission_consent_card? && format == :text
44
+ return Alexa::Responses::PermissionConsents::DeviceAddress.new(
45
+ intent: intent
46
+ ).partial_path(format: :text)
47
+ end
48
+
49
+ if slot_to_elicit.present? && !@slots_to_not_render_elicitation.include?(slot_to_elicit)
50
+ if format == :ssml
51
+ "#{template_path}/elicitations/#{slot_to_elicit.underscore}.ssml.erb"
52
+ else
53
+ "#{template_path}/elicitations/#{slot_to_elicit.underscore}.text.erb"
54
+ end
55
+ else
56
+ if format == :ssml
57
+ "#{template_path}/speech.ssml.erb"
58
+ else
59
+ "#{template_path}/display.text.erb"
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ def elicit_directives
66
+ return [] if directives.empty?
67
+ directives.select { |directive| directive[:type] == "Dialog.ElicitSlot" }
68
+ end
69
+
70
+ def keep_listening!
71
+ @keep_listening = true
72
+ self
73
+ end
74
+
75
+ def keep_listening?
76
+ @keep_listening == true
77
+ end
78
+
79
+ def end_session?
80
+ return false if keep_listening?
81
+ return false if elicit_directives.any?
82
+ return true
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,26 @@
1
+ module Alexa
2
+ module Responses
3
+ class Bye < Alexa::Response
4
+ attr_accessor :intent, :directives
5
+
6
+ def initialize(intent:, directives: [])
7
+ @intent = intent
8
+ end
9
+
10
+ def partial_path(format: :ssml)
11
+ template_path = "alexa/#{intent.context.locale}/intent_handlers/"\
12
+ "#{intent.class.name.demodulize.underscore}"
13
+
14
+ if format == :ssml
15
+ "#{template_path}/bye.ssml.erb"
16
+ else
17
+ "#{template_path}/bye.text.erb"
18
+ end
19
+ end
20
+
21
+ def end_session?
22
+ true
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,18 @@
1
+ module Alexa
2
+ module Responses
3
+ class Delegate
4
+ def as_json(options={})
5
+ {
6
+ "version": "1.0",
7
+ "response": {
8
+ "directives": [
9
+ {
10
+ "type": "Dialog.Delegate"
11
+ }
12
+ ]
13
+ }
14
+ }
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,28 @@
1
+ module Alexa
2
+ module Responses
3
+ module PermissionConsents
4
+ class DeviceAddress < Alexa::Response
5
+ attr_accessor :intent, :directives
6
+
7
+ def initialize(intent:, directives: [])
8
+ @intent = intent
9
+ end
10
+
11
+ def partial_path(format: :ssml)
12
+ if format == :ssml
13
+ "alexa/#{intent.context.locale}/intent_handlers/"\
14
+ "#{intent.class.name.demodulize.underscore}"\
15
+ "/permission_consents/"\
16
+ "device_address.ssml.erb"
17
+ else
18
+ "alexa/permission_consents/device_address.text.erb"
19
+ end
20
+ end
21
+
22
+ def end_session?
23
+ true
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,35 @@
1
+ module Alexa
2
+ class Session < HashWithIndifferentAccess
3
+ def initialize(session={})
4
+ @variables = session
5
+ super(session["attributes"])
6
+ end
7
+
8
+ def elicitation_count_for(slot_name)
9
+ self["elicitations"] ||= {}
10
+ if self["elicitations"][slot_name].present?
11
+ return self["elicitations"][slot_name]["count"]
12
+ end
13
+ return 0
14
+ end
15
+
16
+ def increment_elicitation_count!(slot_name)
17
+ count = elicitation_count_for(slot_name)
18
+ self["elicitations"].merge!(
19
+ "#{slot_name}": { count: count + 1 }
20
+ )
21
+ end
22
+
23
+ def new?
24
+ @variables["new"] == true
25
+ end
26
+
27
+ def application_id
28
+ @variables["application"]["applicationId"]
29
+ end
30
+
31
+ def user_id
32
+ @variables["user"]["userId"]
33
+ end
34
+ end
35
+ end
data/lib/alexa/slot.rb ADDED
@@ -0,0 +1,50 @@
1
+ module Alexa
2
+ class Slot
3
+ attr_accessor :name, :value
4
+
5
+ def initialize(attributes={})
6
+ @name = attributes['name']
7
+ @value = attributes['value']
8
+ @resolutions = attributes['resolutions']
9
+ end
10
+
11
+ def matched_value
12
+ if matched?
13
+ resolution["values"].first["value"]["name"]
14
+ end
15
+ end
16
+
17
+ def matched_id
18
+ if matched?
19
+ resolution["values"].first["value"]["id"]
20
+ end
21
+ end
22
+
23
+ def matched?
24
+ return false if !has_resolutions?
25
+ resolution["status"]["code"] == "ER_SUCCESS_MATCH"
26
+ end
27
+
28
+ def bad_match?
29
+ return false if !has_resolutions?
30
+ resolution["status"]["code"] == "ER_SUCCESS_NO_MATCH"
31
+ end
32
+
33
+ def has_resolutions?
34
+ @resolutions.present?
35
+ end
36
+
37
+ def resolution
38
+ return nil if !has_resolutions?
39
+ @resolutions["resolutionsPerAuthority"].first
40
+ end
41
+
42
+ def as_json(options={})
43
+ h = { name: name }
44
+ # add other attributes only if they are present
45
+ h.merge!(value: value) if value.present?
46
+ h.merge!(resolutions: @resolutions) if @resolutions.present?
47
+ h
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,3 @@
1
+ module Alexa
2
+ VERSION = '0.1.1'
3
+ end
@@ -0,0 +1,19 @@
1
+ require "alexa/engine"
2
+ require 'alexa/context'
3
+ require 'alexa/device'
4
+ require 'alexa/request'
5
+ require 'alexa/response'
6
+ require 'alexa/session'
7
+ require 'alexa/slot'
8
+ require 'alexa/version'
9
+ require 'alexa/responses/bye'
10
+ require 'alexa/responses/delegate'
11
+ require 'alexa/responses/permission_consents/device_address'
12
+ require 'alexa/intent_handlers/base'
13
+ require_relative '../app/models/alexa/user'
14
+ require_relative '../app/models/alexa/usage'
15
+
16
+
17
+ module Alexa
18
+ # Your code goes here...
19
+ end
@@ -0,0 +1,22 @@
1
+ require 'rails/generators'
2
+ require 'rails/generators/migration'
3
+
4
+ module Alexa
5
+ module Generators
6
+ class MigrationsGenerator < ::Rails::Generators::Base
7
+ include Rails::Generators::Migration
8
+ source_root File.expand_path('../templates', __FILE__)
9
+ desc "Add the migrations for Alexa"
10
+
11
+ def self.next_migration_number(path)
12
+ next_migration_number = current_migration_number(path) + 1
13
+ ActiveRecord::Migration.next_migration_number(next_migration_number)
14
+ end
15
+
16
+ def copy_migrations
17
+ migration_template "create_alexa_users.rb", "db/migrate/create_alexa_users.rb"
18
+ migration_template "create_alexa_usages.rb", "db/migrate/create_alexa_usages.rb"
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,13 @@
1
+ class CreateAlexaUsages < ActiveRecord::Migration[4.2]
2
+ def up
3
+ create_table :alexa_usages do |t|
4
+ t.references :alexa_user, foreign_key: true
5
+ t.string :intent_name
6
+ t.integer :count, default: 0
7
+ end
8
+ end
9
+
10
+ def down
11
+ drop_table :alexa_usages
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ class CreateAlexaUsers < ActiveRecord::Migration[4.2]
2
+ def up
3
+ create_table :alexa_users do |t|
4
+ t.string :amazon_id
5
+ end
6
+ add_index :alexa_users, :amazon_id
7
+ end
8
+
9
+ def down
10
+ remove_index :alexa_users, :amazon_id
11
+ drop_table :alexa_users
12
+ end
13
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :alexa do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,115 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: alexa-rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Sri Vishnu Totakura
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-03-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 5.1.4
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 5.1.4
27
+ - !ruby/object:Gem::Dependency
28
+ name: pg
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description: The gem adds additional capabilities to your rails app to serve as a
42
+ backend for your Alexa skill by providing routing, controllers, views and structure
43
+ to easily develop and maintain skills' backend
44
+ email:
45
+ - srivishnu@totakura.in
46
+ executables: []
47
+ extensions: []
48
+ extra_rdoc_files: []
49
+ files:
50
+ - MIT-LICENSE
51
+ - README.md
52
+ - Rakefile
53
+ - app/assets/config/alexa_manifest.js
54
+ - app/assets/javascripts/alexa/application.js
55
+ - app/assets/stylesheets/alexa/application.css
56
+ - app/controllers/alexa/application_controller.rb
57
+ - app/controllers/alexa/intent_handlers_controller.rb
58
+ - app/helpers/alexa/application_helper.rb
59
+ - app/helpers/alexa/context_helper.rb
60
+ - app/helpers/alexa/render_helper.rb
61
+ - app/jobs/alexa/application_job.rb
62
+ - app/mailers/alexa/application_mailer.rb
63
+ - app/models/alexa/application_record.rb
64
+ - app/models/alexa/usage.rb
65
+ - app/models/alexa/user.rb
66
+ - app/views/alexa/_response.json.erb
67
+ - app/views/alexa/output/_card.json.erb
68
+ - app/views/alexa/output/_ssml.json.erb
69
+ - app/views/alexa/output/cards/_ask_for_permissions_consent.json.erb
70
+ - app/views/alexa/output/cards/_simple.json.erb
71
+ - app/views/alexa/permission_consents/device_address.text.erb
72
+ - app/views/layouts/alexa/application.html.erb
73
+ - config/routes.rb
74
+ - lib/alexa-rails.rb
75
+ - lib/alexa/context.rb
76
+ - lib/alexa/device.rb
77
+ - lib/alexa/engine.rb
78
+ - lib/alexa/intent_handlers/base.rb
79
+ - lib/alexa/request.rb
80
+ - lib/alexa/response.rb
81
+ - lib/alexa/responses/bye.rb
82
+ - lib/alexa/responses/delegate.rb
83
+ - lib/alexa/responses/permission_consents/device_address.rb
84
+ - lib/alexa/session.rb
85
+ - lib/alexa/slot.rb
86
+ - lib/alexa/version.rb
87
+ - lib/generators/alexa/migrations_generator.rb
88
+ - lib/generators/alexa/templates/create_alexa_usages.rb
89
+ - lib/generators/alexa/templates/create_alexa_users.rb
90
+ - lib/tasks/alexa_tasks.rake
91
+ homepage: https://github.com/tsrivishnu/alexa-rails
92
+ licenses:
93
+ - MIT
94
+ metadata: {}
95
+ post_install_message:
96
+ rdoc_options: []
97
+ require_paths:
98
+ - lib
99
+ required_ruby_version: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ required_rubygems_version: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ version: '0'
109
+ requirements: []
110
+ rubyforge_project:
111
+ rubygems_version: 2.5.2.1
112
+ signing_key:
113
+ specification_version: 4
114
+ summary: Serve Alexa skills with your rails app as backend
115
+ test_files: []