├── .gitignore
├── .travis.yml
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── alexa_rubykit.gemspec
├── fixtures
├── LaunchRequest.json
├── card-min.json
├── response-min.json
├── response-sessionAtt.json
├── response-sessionAudio.json
├── sample-IntentRequest.json
├── sample-SessionEndedRequest.json
├── sample-card.json
└── sessionAttributes-min.json
├── lib
├── alexa_rubykit.rb
└── alexa_rubykit
│ ├── intent_request.rb
│ ├── launch_request.rb
│ ├── request.rb
│ ├── response.rb
│ ├── response
│ └── dialog.rb
│ ├── session.rb
│ ├── session_ended_request.rb
│ └── version.rb
├── samples
├── IntentSchema.json
└── SampleUtterances.baf
└── spec
├── request_spec.rb
└── response_spec.rb
/.gitignore:
--------------------------------------------------------------------------------
1 | /.bundle/
2 | /.yardoc
3 | /Gemfile.lock
4 | /_yardoc/
5 | /coverage/
6 | /doc/
7 | /pkg/
8 | /spec/reports/
9 | /tmp/
10 | *.bundle
11 | *.so
12 | *.o
13 | *.a
14 | mkmf.log
15 | .idea/
16 | *.gem
17 |
18 | # Elastic Beanstalk Files
19 | .elasticbeanstalk/*
20 | !.elasticbeanstalk/*.cfg.yml
21 | !.elasticbeanstalk/*.global.yml
22 | .DS_
23 | sync-upstream.sh
24 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: ruby
2 | rvm:
3 | - 2.1.6
4 | - 2.2.0
5 | script: bundle exec rspec --color --format=documentation
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | # Specify your gem's dependencies in alexa_rubykit.gemspec
4 | gemspec
5 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2015 Damian Finol
2 |
3 | MIT License
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining
6 | a copy of this software and associated documentation files (the
7 | "Software"), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # AlexaRubykit
2 | [](http://badge.fury.io/rb/alexa_rubykit)[](https://travis-ci.org/damianFC/alexa-rubykit)[](https://travis-ci.org/damianFC/alexa-rubykit)[](http://inch-ci.org/github/damianFC/alexa-rubykit)
3 |
4 | This gem implements a quick back-end service for deploying applications for Amazon's Echo (Alexa).
5 |
6 | ## Installation
7 |
8 | ### Sample Application
9 |
10 | For a sample application video tutorial, check
11 |
12 |
14 |
15 | Samples are provided by the alexa_rubyengine project: https://github.com/damianFC/alexa_rubyengine
16 |
17 | ### For Ruby Projects:
18 |
19 | Add this line to your application's Gemfile:
20 |
21 | ```ruby
22 | gem 'alexa_rubykit'
23 | ```
24 |
25 | And then execute:
26 |
27 | $ bundle
28 |
29 | Or install it yourself as:
30 |
31 | $ gem install alexa_rubykit
32 |
33 | ## Usage
34 |
35 | This Gem provides methods to create and handle request and response objects to be used in your container of choice.
36 |
37 | Sample usage:
38 |
39 | ```ruby
40 | require 'alexa_rubykit'
41 | response = AlexaRubykit::Response.new
42 | response.add_speech('Ruby is running ready!')
43 | response.build_response
44 | ```
45 |
46 | Will generate a valid outputspeech response in JSON format:
47 |
48 | ```JSON
49 | {
50 | "version": "1.0",
51 | "response": {
52 | "outputSpeech": {
53 | "type": "PlainText",
54 | "text": "Ruby is running ready!"
55 | },
56 | "shouldEndSession": true
57 | }
58 | }
59 | ```
60 |
61 | ## Troubleshooting
62 |
63 | There are two sources of troubleshooting information: the Amazon Echo app/website and the EBS logs that you can get from
64 | the management console.
65 | - "Error in SSL handshake" : Make sure your used the FQDN when you generated the SSL and it's also the active SSL in EBS.
66 | - "Error communicating with the application" : Query the EBS logs from the management console and create an issue on GitHub.
67 |
68 | ## Testing
69 |
70 | Run the tests using
71 |
72 | ```bash
73 | bundle exec rake
74 | ```
75 |
76 | ## Contributing
77 |
78 | 1. Decide to work on the "dev" (unstable) branch or "master" (stable)
79 | 1. Fork it ( https://github.com/[my-github-username]/alexa_rubykit/fork )
80 | 2. Create your feature branch (`git checkout -b my-new-feature`)
81 | 3. Commit your changes (`git commit -am 'Add some feature'`)
82 | 4. Push to the branch (`git push origin my-new-feature`)
83 | 5. Create a new Pull Request
84 |
85 | All development is done in the "dev" branch before being merged to master. Applications can use the developer
86 | environment by adding the following line to their Gemfile:
87 |
88 | ```ruby
89 | gem 'alexa_rubykit', :git => 'https://github.com/damianFC/alexa-rubykit.git', :branch => 'dev'
90 | ```
91 |
92 | To use the stable/master branch, rename 'dev' to 'master' or remove :branch all together.
93 |
94 |
95 |
96 | # Team Members
97 | * "Damian Finol"
98 | * "Dave Shapiro"
99 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require "bundler/gem_tasks"
2 | require 'rspec/core/rake_task'
3 |
4 | RSpec::Core::RakeTask.new
5 |
6 | task :default => [:spec]
7 |
--------------------------------------------------------------------------------
/alexa_rubykit.gemspec:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | lib = File.expand_path('../lib', __FILE__)
3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4 | require 'alexa_rubykit/version'
5 |
6 | Gem::Specification.new do |spec|
7 | spec.name = "alexa_rubykit"
8 | spec.version = AlexaRubykit::VERSION
9 | spec.authors = ["Damian Finol"]
10 | spec.email = ["damian.finol@gmail.com"]
11 | spec.summary = %q{Alexa Ruby Kit}
12 | spec.description = %q{Alexa Ruby Kit with examples}
13 | spec.homepage = 'https://github.com/damianFC/alexa-rubykit'
14 | spec.license = "MIT"
15 | spec.files = Dir['[A-Z]*'] + Dir['lib/**/*'] + Dir['tests/**'] + Dir['bin/**']
16 | spec.files.reject! { |fn| fn.include? ".gem" }
17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19 | spec.require_paths = ["lib"]
20 | spec.add_runtime_dependency 'bundler', '~> 1.7'
21 | spec.add_runtime_dependency 'rake'
22 | spec.add_development_dependency 'rspec', '~> 3.2', '>= 3.2.0'
23 | spec.add_development_dependency 'rspec-mocks', '~> 3.2', '>= 3.2.0'
24 | end
25 |
--------------------------------------------------------------------------------
/fixtures/LaunchRequest.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "1.0",
3 | "session": {
4 | "new": true,
5 | "sessionId": "amzn1.echo-api.session.abeee1a7-aee0-41e6-8192-e6faaed9f5ef",
6 | "attributes": {},
7 | "user": {
8 | "userId": "amzn1.account.AM3B227HF3FAM1B261HK7FFM3A2"
9 | }
10 | },
11 | "request": {
12 | "type": "LaunchRequest",
13 | "requestId": "amzn1.echo-api.request.9cdaa4db-f20e-4c58-8d01-c75322d6c423"
14 | }
15 | }
--------------------------------------------------------------------------------
/fixtures/card-min.json:
--------------------------------------------------------------------------------
1 | {
2 | "card" : {
3 | "type" : "Simple"
4 | },
5 | "shouldEndSession" : true
6 | }
--------------------------------------------------------------------------------
/fixtures/response-min.json:
--------------------------------------------------------------------------------
1 | {
2 | "version" : "1.0",
3 | "response" :
4 | {
5 | "shouldEndSession" : true
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/fixtures/response-sessionAtt.json:
--------------------------------------------------------------------------------
1 | {
2 | "version" : "1.0",
3 | "sessionAttributes" :
4 | {
5 | "new" : false,
6 | "sessionId" : "amzn-xxx-yyy-zzz"
7 | },
8 | "response" :
9 | {
10 | "shouldEndSession": true
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/fixtures/response-sessionAudio.json:
--------------------------------------------------------------------------------
1 | {
2 | "version" : "1.0",
3 | "response" :
4 | {
5 | "directives": [
6 | {
7 | "type": "AudioPlayer.Play",
8 | "playBehavior": "REPLACE_ALL",
9 | "audioItem": {
10 | "stream": {
11 | "token": "token",
12 | "url": "http://test/url.mp3",
13 | "offsetInMilliseconds": 100
14 | }
15 | }
16 | }
17 | ],
18 | "shouldEndSession": true
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/fixtures/sample-IntentRequest.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "1.0",
3 | "session": {
4 | "new": false,
5 | "sessionId": "amzn1.echo-api.session.abeee1a7-aee0-41e6-8192-e6faaed9f5ef",
6 | "attributes": {
7 | "supportedHoroscopePeriods": {
8 | "daily": true,
9 | "weekly": false,
10 | "monthly": false
11 | }
12 | },
13 | "user": {
14 | "userId": "amzn1.account.AM3B227HF3FAM1B261HK7FFM3A2"
15 | }
16 | },
17 | "request": {
18 | "type": "IntentRequest",
19 | "requestId": "amzn1.echo-api.request.6919844a-733e-4e89-893a-fdcb77e2ef0d",
20 | "intent": {
21 | "name": "GetZodiacHoroscopeIntent",
22 | "slots": {
23 | "ZodiacSign": {
24 | "name": "ZodiacSign",
25 | "value": "virgo"
26 | }
27 | }
28 | }
29 | }
30 | }
--------------------------------------------------------------------------------
/fixtures/sample-SessionEndedRequest.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "1.0",
3 | "session": {
4 | "new": false,
5 | "sessionId": "amzn1.echo-api.session.abeee1a7-aee0-41e6-8192-e6faaed9f5ef",
6 | "attributes": {
7 | "supportedHoroscopePeriods": {
8 | "daily": true,
9 | "weekly": false,
10 | "monthly": false
11 | }
12 | },
13 | "user": {
14 | "userId": "amzn1.account.AM3B227HF3FAM1B261HK7FFM3A2"
15 | }
16 | },
17 | "request": {
18 | "type": "SessionEndedRequest",
19 | "requestId": "amzn1.echo-api.request.d8c37cd6-0e1c-458e-8877-5bb4160bf1e1",
20 | "reason": "USER_INITIATED"
21 | }
22 | }
--------------------------------------------------------------------------------
/fixtures/sample-card.json:
--------------------------------------------------------------------------------
1 | {
2 | "card" : {
3 | "title" : "Ruby Run",
4 | "subtitle" : "Ruby Running Ready!",
5 | "type" : "Simple"
6 | },
7 | "shouldEndSession" : true
8 | }
9 |
--------------------------------------------------------------------------------
/fixtures/sessionAttributes-min.json:
--------------------------------------------------------------------------------
1 | {
2 | "sessionAttributes":
3 | {
4 |
5 | }
6 | }
--------------------------------------------------------------------------------
/lib/alexa_rubykit.rb:
--------------------------------------------------------------------------------
1 | require 'alexa_rubykit/request'
2 | require 'alexa_rubykit/version'
3 | require 'alexa_rubykit/response'
4 | require 'alexa_rubykit/intent_request'
5 | require 'alexa_rubykit/launch_request'
6 | require 'alexa_rubykit/session_ended_request'
7 |
8 | module AlexaRubykit
9 | # Prints a JSON object.
10 | def self.print_json(json)
11 | p json
12 | end
13 |
14 | # Prints the Gem version.
15 | def self.print_version
16 | p AlexaRubykit::VERSION
17 | end
18 |
19 | # Returns true if all the Alexa request objects are set.
20 | def self.valid_alexa?(request_json)
21 | !request_json.nil? && !request_json['session'].nil? &&
22 | !request_json['version'].nil? && !request_json['request'].nil?
23 | end
24 | end
--------------------------------------------------------------------------------
/lib/alexa_rubykit/intent_request.rb:
--------------------------------------------------------------------------------
1 | module AlexaRubykit
2 | class IntentRequest < Request
3 | attr_accessor :intent, :name, :slots
4 |
5 | # We still don't know if all of the parameters in the request are required.
6 | # Checking for the presence of intent on an IntentRequest.
7 | def initialize(json_request)
8 | super
9 | @intent = json_request['request']['intent']
10 | raise ArgumentError, 'Intent should exist on an IntentRequest' if @intent.nil?
11 | @type = 'INTENT_REQUEST'
12 | @name = @intent['name']
13 | @slots = @intent['slots']
14 | end
15 |
16 | # Takes a Hash object.
17 | def add_hash_slots(slots)
18 | raise ArgumentError, 'Slots can\'t be empty'
19 | slots.each do |slot|
20 | @slots[:slot[:name]] = Slot.new(slot[:name], slot[:value])
21 | end
22 | @slots
23 | end
24 |
25 | # Takes a JSON Object and symbolizes its keys.
26 | def add_slots(slots)
27 | slot_hash = AlexaRubykit.transform_keys_to_symbols(value)
28 | add_hash_slots(slot_hash)
29 | end
30 |
31 | # Adds a slot from a name and a value.
32 | def add_slot(name, value)
33 | slot = Slot.new(name, value)
34 | @slots[:name] = slot
35 | slot
36 | end
37 |
38 | # Outputs the Intent Name, request Id and slot information.
39 | def to_s
40 | "IntentRequest: #{name} requestID: #{request_id} Slots: #{slots}"
41 | end
42 | end
43 |
44 | # Class that encapsulates each slot.
45 | class Slot
46 | attr_accessor :name, :value
47 |
48 | # Each slot has a name and a value.
49 | def initialize(name, value)
50 | raise ArgumentError, 'Need a name and a value' if name.nil? || value.nil?
51 | @name = name
52 | @value = value
53 | end
54 |
55 | # Outputs Slot name and value.
56 | def to_s
57 | "Slot Name: #{name}, Value: #{value}"
58 | end
59 | end
60 | end
--------------------------------------------------------------------------------
/lib/alexa_rubykit/launch_request.rb:
--------------------------------------------------------------------------------
1 | module AlexaRubykit
2 | class LaunchRequest < Request
3 | # We still don't know if all of the parameters in the request are required.
4 | # Checking for the presence of intent on an IntentRequest.
5 | def initialize(json_request)
6 | super
7 | @type = 'LAUNCH_REQUEST'
8 | end
9 |
10 | # Outputs the launch requestID.
11 | def to_s
12 | "LaunchRequest requestID: #{request_id}"
13 | end
14 | end
15 | end
--------------------------------------------------------------------------------
/lib/alexa_rubykit/request.rb:
--------------------------------------------------------------------------------
1 | module AlexaRubykit
2 | # Echo can send 3 types of requests
3 | # - LaunchRequest: The start of the app.
4 | # - IntentRequest: The intent of the app.
5 | # - SessionEndedRequest: Session has ended.
6 | class Request
7 | require 'json'
8 | require 'alexa_rubykit/session'
9 | attr_accessor :version, :type, :session, :json # global
10 | attr_accessor :request_id, :locale # on request
11 |
12 | def initialize(json_request)
13 | @request_id = json_request['request']['requestId']
14 | raise ArgumentError, 'Request ID should exist on all Requests' if @request_id.nil?
15 | @version = json_request['version']
16 | @locale = json_request['request']['locale']
17 | @json = json_request
18 |
19 | # TODO: We probably need better session handling.
20 | @session = AlexaRubykit::Session.new(json_request['session'])
21 | end
22 | end
23 |
24 | # Builds a new request for Alexa.
25 | def self.build_request(json_request)
26 | raise ArgumentError, 'Invalid Alexa Request.' unless AlexaRubykit.valid_alexa?(json_request)
27 | @request = nil
28 | case json_request['request']['type']
29 | when /Launch/
30 | @request = LaunchRequest.new(json_request)
31 | when /Intent/
32 | @request = IntentRequest.new(json_request)
33 | when /SessionEnded/
34 | @request = SessionEndedRequest.new(json_request)
35 | else
36 | raise ArgumentError, 'Invalid Request Type.'
37 | end
38 | @request
39 | end
40 |
41 | # Take keys of hash and transform those to a symbols
42 | def self.transform_keys_to_symbols(value)
43 | return value if not value.is_a?(Hash)
44 | hash = value.inject({}){|memo,(k,v)| memo[k.to_sym] = self.transform_keys_to_symbols(v); memo}
45 | return hash
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/lib/alexa_rubykit/response.rb:
--------------------------------------------------------------------------------
1 | module AlexaRubykit
2 | class Response
3 | class SlotNotFound < StandardError; end
4 |
5 | require 'json'
6 | require 'alexa_rubykit/response/dialog'
7 |
8 | attr_accessor :version, :session, :response_object, :session_attributes,
9 | :speech, :reprompt, :response, :card, :intents, :request
10 |
11 | # Every response needs a shouldendsession and a version attribute
12 | # We initialize version to 1.0, use add_version to set your own.
13 | def initialize(request=nil, version='1.0')
14 | @session_attributes = Hash.new
15 | @version = version
16 | @request = request
17 | @intents = request.intent if request && request.type == "INTENT_REQUEST"
18 | @directives = []
19 | end
20 |
21 | # Adds a key,value pair to the session object.
22 | def add_session_attribute(key, value)
23 | @session_attributes[key.to_sym] = value
24 | end
25 |
26 | def add_speech(speech_text, ssml = false)
27 | if ssml
28 | @speech = { :type => 'SSML', :ssml => check_ssml(speech_text) }
29 | else
30 | @speech = { :type => 'PlainText', :text => speech_text }
31 | end
32 | @speech
33 | end
34 |
35 | def add_audio_url(url, token='', offset=0)
36 | @directives << {
37 | 'type' => 'AudioPlayer.Play',
38 | 'playBehavior' => 'REPLACE_ALL',
39 | 'audioItem' => {
40 | 'stream' => {
41 | 'token' => token,
42 | 'url' => url,
43 | 'offsetInMilliseconds' => offset
44 | }
45 | }
46 | }
47 | end
48 |
49 | def delegate_dialog_response
50 | @directives = [Dialog.delegate_directive(intents)]
51 | end
52 |
53 | def elicit_dialog_response(slot)
54 | @directives = [Dialog.elicit_slot_directive(slot, intents)]
55 | end
56 |
57 | def confirm_dialog_slot(slot)
58 | @directives = [Dialog.confirm_slot_directive(slot, intents)]
59 | end
60 |
61 | def confirm_dialog_intent
62 | @directives = [Dialog.confirm_intent_directive(intents)]
63 | end
64 |
65 | def modify_slot(name, value, confirmation_status)
66 | raise SlotNotFound if @intents['slots'][name].nil?
67 |
68 | @intents['slots'][name]['value'] = value
69 | @intents['slots'][name]['confirmationStatus'] = confirmation_status
70 | end
71 |
72 | def add_reprompt(speech_text, ssml = false)
73 | if ssml
74 | @reprompt = { "outputSpeech" => { :type => 'SSML', :ssml => check_ssml(speech_text) } }
75 | else
76 | @reprompt = { "outputSpeech" => { :type => 'PlainText', :text => speech_text } }
77 | end
78 | @reprompt
79 | end
80 |
81 | #
82 | #"type": "string",
83 | # "title": "string",
84 | # "subtitle": "string",
85 | # "content": "string"
86 | def add_card(type = nil, title = nil , subtitle = nil, content = nil)
87 | # A Card must have a type which the default is Simple.
88 | @card = Hash.new()
89 | @card[:type] = type || 'Simple'
90 | @card[:title] = title unless title.nil?
91 | @card[:subtitle] = subtitle unless subtitle.nil?
92 | @card[:content] = content unless content.nil?
93 | @card
94 | end
95 |
96 | # The JSON Spec says order shouldn't matter.
97 | def add_hash_card(card)
98 | card[:type] = 'Simple' if card[:type].nil?
99 | @card = card
100 | @card
101 | end
102 |
103 | # Adds a speech to the object, also returns a outputspeech object.
104 | def say_response(speech, end_session = true, ssml = false)
105 | output_speech = add_speech(speech,ssml)
106 | { :outputSpeech => output_speech, :shouldEndSession => end_session }
107 | end
108 |
109 | # Incorporates reprompt in the SDK 2015-05
110 | def say_response_with_reprompt(speech, reprompt_speech, end_session = true, speech_ssml = false, reprompt_ssml = false)
111 | output_speech = add_speech(speech,speech_ssml)
112 | reprompt_speech = add_reprompt(reprompt_speech,reprompt_ssml)
113 | { :outputSpeech => output_speech, :reprompt => reprompt_speech, :shouldEndSession => end_session }
114 | end
115 |
116 | # Creates a session object. We pretty much only use this in testing.
117 | def build_session
118 | # If it's empty assume user doesn't need session attributes.
119 | @session_attributes = Hash.new if @session_attributes.nil?
120 | @session = { :sessionAttributes => @session_attributes }
121 | @session
122 | end
123 |
124 | # The response object (with outputspeech, cards and session end)
125 | # Should rename this, but Amazon picked their names.
126 | # The only mandatory field is end_session which we default to true.
127 | def build_response_object(session_end = true)
128 | @response = Hash.new
129 | @response[:outputSpeech] = @speech unless @speech.nil?
130 | @response[:directives] = @directives unless @directives.empty?
131 | @response[:card] = @card unless @card.nil?
132 | @response[:reprompt] = @reprompt unless session_end && @reprompt.nil?
133 | @response[:shouldEndSession] = session_end
134 | @response
135 | end
136 |
137 | # Builds a response.
138 | # Takes the version, response and should_end_session variables and builds a JSON object.
139 | def build_response(session_end = true)
140 | response_object = build_response_object(session_end)
141 | response = Hash.new
142 | response[:version] = @version
143 | response[:sessionAttributes] = @session_attributes unless @session_attributes.empty?
144 | response[:response] = response_object
145 | response.to_json
146 | end
147 |
148 | # Outputs the version, session object and the response object.
149 | def to_s
150 | "Version => #{@version}, SessionObj => #{@session}, Response => #{@response}"
151 | end
152 |
153 | private
154 |
155 | def check_ssml(ssml_string)
156 | ssml_string = ssml_string.strip[0..6] == "" ? ssml_string : "" + ssml_string
157 | ssml_string.strip[-8..1] == "" ? ssml_string : ssml_string + ""
158 | end
159 | end
160 | end
--------------------------------------------------------------------------------
/lib/alexa_rubykit/response/dialog.rb:
--------------------------------------------------------------------------------
1 | module AlexaRubykit
2 | # Represents the encapsulation of Amazon Alexa Dialog Interface
3 | # https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/dialog-interface-reference
4 | class Dialog
5 | DELEGATE_TYPE = "Dialog.Delegate".freeze
6 | ELICIT_SLOT_TYPE = "Dialog.ElicitSlot".freeze
7 | CONFIRM_SLOT_TYPE = "Dialog.ConfirmSlot".freeze
8 | CONFIRM_INTENT_TYPE = "Dialog.ConfirmIntent".freeze
9 |
10 | class << self
11 | def delegate_directive(updated_intents)
12 | {
13 | 'type' => DELEGATE_TYPE,
14 | 'updatedIntent' => updated_intents
15 | }
16 | end
17 |
18 | def elicit_slot_directive(slot, updated_intents)
19 | {
20 | 'type' => ELICIT_SLOT_TYPE,
21 | 'slotToElicit' => slot,
22 | 'updatedIntent' => updated_intents
23 | }
24 | end
25 |
26 | def confirm_slot_directive(slot, updated_intents)
27 | {
28 | 'type' => CONFIRM_SLOT_TYPE,
29 | 'slotToConfirm' => slot,
30 | 'updatedIntent' => updated_intents
31 | }
32 | end
33 |
34 | def confirm_intent_directive(updated_intents)
35 | {
36 | 'type' => CONFIRM_INTENT_TYPE,
37 | 'updatedIntent' => updated_intents
38 | }
39 | end
40 | end
41 | end
42 | end
--------------------------------------------------------------------------------
/lib/alexa_rubykit/session.rb:
--------------------------------------------------------------------------------
1 | module AlexaRubykit
2 | # Handles the session object in request.
3 | class Session
4 | attr_accessor :new, :session_id, :attributes, :user
5 | def initialize (session)
6 | raise ArgumentError, 'Invalid Session' if session.nil? || session['new'].nil? || session['sessionId'].nil?
7 | @new = session['new']
8 | @session_id = session['sessionId']
9 | session['attributes'].nil? ? @attributes = Hash.new : @attributes = session['attributes']
10 | @user = session['user']
11 | end
12 |
13 | # Returns whether this is a new session or not.
14 | def new?
15 | !!@new
16 | end
17 |
18 | # Returns true if a user is defined.
19 | def user_defined?
20 | !@user.nil? || !@user['userId'].nil?
21 | end
22 |
23 | # Returns the user_id.
24 | def user_id
25 | @user['userId'] if @user
26 | end
27 |
28 | def access_token
29 | @user['accessToken'] if @user
30 | end
31 |
32 | # Check to see if attributes are present.
33 | def has_attributes?
34 | !@attributes.empty?
35 | end
36 | end
37 | end
--------------------------------------------------------------------------------
/lib/alexa_rubykit/session_ended_request.rb:
--------------------------------------------------------------------------------
1 | # Session end request class.
2 | module AlexaRubykit
3 | class SessionEndedRequest < Request
4 | attr_accessor :reason
5 |
6 | # TODO: Validate the reason.
7 | # We still don't know if all of the parameters in the request are required.
8 | # Checking for the presence of intent on an IntentRequest.
9 | def initialize(json_request)
10 | super
11 | @type = 'SESSION_ENDED_REQUEST'
12 | @reason = json_request['request']['reason']
13 | end
14 |
15 | # Ouputs the request_id and the reason why.
16 | def to_s
17 | "Session Ended for requestID: #{request_id} with reason #{reason}"
18 | end
19 | end
20 | end
--------------------------------------------------------------------------------
/lib/alexa_rubykit/version.rb:
--------------------------------------------------------------------------------
1 | module AlexaRubykit
2 | VERSION = '1.3.1'
3 | end
4 |
--------------------------------------------------------------------------------
/samples/IntentSchema.json:
--------------------------------------------------------------------------------
1 | {
2 | "intents": [
3 | {
4 | "intent": "GetHelper",
5 | "slots": [
6 | {
7 | "name": "helper",
8 | "type": "LITERAL"
9 | }
10 | ]
11 | }
12 | ]
13 | }
--------------------------------------------------------------------------------
/samples/SampleUtterances.baf:
--------------------------------------------------------------------------------
1 | GetHelper tell me the {twitter|helper}
--------------------------------------------------------------------------------
/spec/request_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rspec'
2 | require 'alexa_rubykit'
3 |
4 | describe 'Request handling' do
5 |
6 | it 'should accept a proper alexa launch request object' do
7 | sample_request = JSON.parse(File.read('fixtures/LaunchRequest.json'))
8 | request = AlexaRubykit::build_request(sample_request)
9 | expect(request.type).to eq('LAUNCH_REQUEST')
10 | end
11 |
12 | it 'should correctly identify valid AWS requests' do
13 | sample_bad_request = { foo: 'bar' }
14 | expect(AlexaRubykit::valid_alexa?(sample_bad_request)).to be false
15 |
16 | sample_good_request = JSON.parse(File.read('fixtures/sample-IntentRequest.json'))
17 | expect(AlexaRubykit::valid_alexa?(sample_good_request)).to be true
18 | end
19 |
20 | it 'should raise an exception when an invalid request is sent' do
21 | sample_request = 'invalid object!'
22 | expect { AlexaRubykit::build_request(sample_request)}.to raise_error(ArgumentError)
23 | sample_request = nil
24 | expect { AlexaRubykit::build_request(sample_request)}.to raise_error(ArgumentError)
25 | end
26 |
27 | it 'should create valid intent request type' do
28 | sample_request = JSON.parse(File.read('fixtures/sample-IntentRequest.json'))
29 | intent_request = AlexaRubykit::build_request(sample_request)
30 | expect(intent_request.type).to eq('INTENT_REQUEST')
31 | expect(intent_request.request_id).not_to be_empty
32 | expect(intent_request.intent).not_to be_empty
33 | expect(intent_request.name).to eq('GetZodiacHoroscopeIntent')
34 | expect(intent_request.slots).not_to be_empty
35 | end
36 |
37 | it 'should create a valid session end request type' do
38 | sample_request = JSON.parse(File.read('fixtures/sample-SessionEndedRequest.json'))
39 | intent_request = AlexaRubykit::build_request(sample_request)
40 | expect(intent_request.type).to eq('SESSION_ENDED_REQUEST')
41 | expect(intent_request.request_id).not_to be_empty
42 | expect(intent_request.reason).to eq('USER_INITIATED')
43 | end
44 |
45 | it 'should create valid sessions with attributes' do
46 | sample_request = JSON.parse(File.read('fixtures/sample-IntentRequest.json'))
47 | intent_request = AlexaRubykit::build_request(sample_request)
48 | expect(intent_request.session.new?).to be_falsey
49 | expect(intent_request.session.has_attributes?).to be_truthy
50 | expect(intent_request.session.user_defined?).to be_truthy
51 | expect(intent_request.session.attributes).not_to be_empty
52 | end
53 |
54 | it 'transform_keys_to_symbols' do
55 | string_keys_hash = { "test1" => 'value', "test2" => { "test3" => 'value' } }
56 | symbol_keys_hash = { test1: 'value', test2: { test3: 'value' } }
57 | expect(AlexaRubykit.transform_keys_to_symbols(string_keys_hash)).to eq(symbol_keys_hash)
58 | end
59 | end
60 |
--------------------------------------------------------------------------------
/spec/response_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rspec'
2 | require 'alexa_rubykit/response'
3 |
4 | describe 'Builds appropriate response objects' do
5 | let(:response) { AlexaRubykit::Response.new }
6 |
7 |
8 | #TODO: Do a :before with the Response object creation
9 |
10 | it 'should create valid session responses' do
11 | # Pair values.
12 | response.add_session_attribute('new', false)
13 | response.add_session_attribute('sessionId', 'amzn1.echo-api.session.abeee1a7-aee0-41e6-8192-e6faaed9f5ef')
14 | session = response.build_session
15 | expect(session).to include(:sessionAttributes)
16 | expect(session[:sessionAttributes]).to include(:new)
17 | expect(session[:sessionAttributes]).to include(:sessionId)
18 |
19 | # Empty.
20 | response = AlexaRubykit::Response.new
21 | session = response.build_session
22 | expect(session).to include(:sessionAttributes)
23 | expect(session[:sessionAttributes]).to be_empty
24 | end
25 |
26 | # TODO: Add cards.
27 | it 'should create a valid Alexa say response object' do
28 | response.add_speech('Testing Alexa Rubykit!')
29 | response_object = response.build_response_object
30 | expect(response_object).to include(:outputSpeech)
31 | expect(response_object[:outputSpeech][:type]).to include('PlainText')
32 | expect(response_object[:outputSpeech][:text]).to include('Testing Alexa Rubykit!')
33 |
34 | # The say_response command should create the same object.
35 | response_say = AlexaRubykit::Response.new
36 | response_say_object = response_say.say_response('Testing Alexa Rubykit!')
37 | expect(response_say_object).to eq(response_object)
38 |
39 | # End session should be true if we didn't specify it.
40 | expect(response_object[:shouldEndSession]).to eq(true)
41 | response_object = response.build_response_object(false)
42 | # And to be false if we tell it to continue the session
43 | expect(response_object[:shouldEndSession]).to eq(false)
44 | # say_response should now be NOT equal thanks to endsession.
45 | expect(response_say_object).not_to eq(response_object)
46 | end
47 |
48 | it 'should create a valid SSML Alexa say response object' do
49 | response.add_speech('Testing SSML Alexa Rubykit support!',true)
50 | response_object = response.build_response_object
51 | expect(response_object).to include(:outputSpeech)
52 | expect(response_object[:outputSpeech][:type]).to include('SSML')
53 | expect(response_object[:outputSpeech][:ssml]).to include('Testing SSML Alexa Rubykit support!')
54 |
55 | # The say_response command should create the same object.
56 | response_say = AlexaRubykit::Response.new
57 | response_say_object = response_say.say_response('Testing SSML Alexa Rubykit support!',true,true)
58 | expect(response_say_object).to eq(response_object)
59 |
60 | # End session should be true if we didn't specify it.
61 | expect(response_object[:shouldEndSession]).to eq(true)
62 | response_object = response.build_response_object(false)
63 | # And to be false if we tell it to continue the session
64 | expect(response_object[:shouldEndSession]).to eq(false)
65 | # say_response should now be NOT equal thanks to endsession.
66 | expect(response_say_object).not_to eq(response_object)
67 | end
68 |
69 | it 'should create a valid SSML Alexa say response object when ssml lacks speak tags' do
70 | response.add_speech('Testing SSML Alexa Rubykit support!',true)
71 | response_object = response.build_response_object
72 | expect(response_object).to include(:outputSpeech)
73 | expect(response_object[:outputSpeech][:type]).to include('SSML')
74 | expect(response_object[:outputSpeech][:ssml]).to include('')
75 | expect(response_object[:outputSpeech][:ssml]).to include('')
76 | expect(response_object[:outputSpeech][:ssml]).to include('Testing SSML Alexa Rubykit support!')
77 | end
78 |
79 | it 'should create a valid minimum response (body)' do
80 | # Every response needs a version and a "response object", sessionAttributes is optional.
81 | # Response Object needs a endsession at a minimum, which we default to true.
82 | response.build_response_object
83 | response_json = response.build_response
84 | sample_json = JSON.parse(File.read('fixtures/response-min.json')).to_json
85 | expect(response_json).to eq(sample_json)
86 | end
87 |
88 | it 'should create a valid card from a hash' do
89 | response.add_hash_card( { :title => 'Ruby Run', :subtitle => 'Ruby Running Ready!' } )
90 | response_json = response.build_response_object
91 | sample_json = JSON.parse(File.read('fixtures/sample-card.json'))
92 | expect(response_json.to_json).to eq(sample_json.to_json)
93 | end
94 |
95 | it 'should create an empty valid card with a response object.' do
96 | response.add_card
97 | response_json = response.build_response_object
98 | sample_json = JSON.parse(File.read('fixtures/card-min.json'))
99 | expect(response_json.to_json).to eq(sample_json.to_json)
100 | end
101 |
102 | it 'should create a valid response with some attributes' do
103 | response.add_session_attribute('new', false)
104 | response.add_session_attribute('sessionId', 'amzn-xxx-yyy-zzz')
105 | response.build_response_object
106 | response_json = response.build_response
107 | sample_json = JSON.parse(File.read('fixtures/response-sessionAtt.json')).to_json
108 | expect(response_json).to eq(sample_json)
109 | end
110 |
111 | it 'should create a valid response with an audio stream directive' do
112 | response.add_audio_url('http://test/url.mp3','token',100)
113 | response.build_response_object
114 | response_json = response.build_response
115 | sample_json = JSON.parse(File.read('fixtures/response-sessionAudio.json')).to_json
116 | expect(response_json).to eq(sample_json)
117 | end
118 |
119 | describe 'alexa dialog interface' do
120 | it 'should create a valid response when delegating the dialog to Alexa' do
121 | response.delegate_dialog_response
122 | response_object = response.build_response_object
123 | expect(response_object).to include(:directives)
124 | expect(response_object[:directives]).to include({'type' => 'Dialog.Delegate', 'updatedIntent' => nil})
125 | end
126 |
127 | it 'should create a valid response when eliciting dialog slot to Alexa' do
128 | response.elicit_dialog_response('slot')
129 | response_object = response.build_response_object
130 | expect(response_object).to include(:directives)
131 | expect(response_object[:directives]).to include({
132 | 'type' => 'Dialog.ElicitSlot', 'slotToElicit' => 'slot', 'updatedIntent' => nil
133 | })
134 | end
135 |
136 | it 'should create a valid response when confirming slot to Alexa' do
137 | response.confirm_dialog_slot('slot')
138 | response_object = response.build_response_object
139 | expect(response_object).to include(:directives)
140 | expect(response_object[:directives]).to include({
141 | 'type' => 'Dialog.ConfirmSlot', 'slotToConfirm' => 'slot','updatedIntent' => nil
142 | })
143 | end
144 |
145 | it 'should create a valid response when confirming intent to Alexa' do
146 | response.confirm_dialog_intent
147 | response_object = response.build_response_object
148 | expect(response_object).to include(:directives)
149 | expect(response_object[:directives]).to include({'type' => 'Dialog.ConfirmIntent', 'updatedIntent' => nil})
150 | end
151 | end
152 | end
--------------------------------------------------------------------------------