├── .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 | [![Gem Version](https://badge.fury.io/rb/alexa_rubykit.svg)](http://badge.fury.io/rb/alexa_rubykit)[![Build Status](https://travis-ci.org/damianFC/alexa-rubykit.svg?branch=master)](https://travis-ci.org/damianFC/alexa-rubykit)[![Build Status](https://travis-ci.org/damianFC/alexa-rubykit.svg?branch=dev)](https://travis-ci.org/damianFC/alexa-rubykit)[![Inline docs](http://inch-ci.org/github/damianFC/alexa-rubykit.svg?branch=master)](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 | Running a sample Rubykit Demo 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 --------------------------------------------------------------------------------