By Sam Morgan, Head of Education at Makers Academy
Editor’s note: This is part five of our Makers Academy series for Ruby developers. Learn more about this free training on the Alexa Skills Kit and read parts one, two, three, and four. Check out the full training course for free online.
We will be using the Ralyxa framework during this module.
We have constructed a series of skills that allow us to interact with, and control, Alexa. However, our skills rely on data held in-memory within our Sinatra applications. All data passed to and from Alexa is wiped whenever we restart our Sinatra application.
During this module, you will construct an intermediate skill called Pizza Buddy. This skill will allow users to:
We will use the session to design a multi-stage ordering process. We will store and retrieve orders using a Postgres relational database. We will use Datamapper as the translation later ('ORM') between our Sinatra application and the Postgres database. Finally, we will post cards to the user, displaying simple information about the orders that have been made.
A completed version of the Pizza Buddy application is available here. You can use the commits to guide your build, or fork and play with the completed application. This walkthrough covers commits 1 - 12.
You can take two routes through this walkthrough:
The Quick Steps map directly onto the Detailed Walkthrough. Each Quick Step has an associated chapter in the Detailed Walkthrough.
StartPizzaOrder
intent in the Alexa Developer Portal, with one Utterance: "StartPizzaOrder new pizza".StartPizzaOrder
intent declaration in the Sinatra application, responding with a prompt to pick a size of pizza.LaunchRequest
intent declaration in the Sinatra application. Respond with a simple 'welcome' message. (Checkpoint 2)Pizza
object to the Sinatra application, which presents the available sizes of pizza.. (Checkpoint 3)ContinuePizzaOrder
intent in the Alexa Developer Portal, with one slot, named size
, of type PIZZA_SIZE
, a custom slot. Define a custom slot, PIZZA_SIZE
, with values depending on the sizes you offer in your response to the StartPizzaOrder
intent.ContinuePizzaOrder
intent declaration which saves the user's choice of pizza size to the session, and prompts for pizza toppings.PenultimatePizzaOrder
intent in the Alexa Developer Portal, with slots for up to five toppings.PenultimatePizzaOrder
, handling between one and five toppings.PenultimatePizzaOrder
intent declaration confirming the size and toppings. (Checkpoint 4)ConfirmPizzaOrder
intent in the Alexa Developer Portal.ConfirmPizzaOrder
intent declaration, which saves the confirmed pizza to the database. (Checkpoint 6)ListOrders
intent, listing Pizza
entities saved in the database. (Checkpoint 7)ListToppings
intent, which lists the available permitted toppings. (Checkpoint 8)
Our first step, as always, is to set up the skill on the Amazon developer portal. We'll want to call this skill Pizza Buddy with an invocation name of 'pizza buddy.' We won't need account linking.
Create a new directory (we'll refer to this as the 'application directory'). Inside this directory, create a Gemfile, containing a single gem: sinatra
. Also, install ngrok. I'll assume you've downloaded ngrok to this directory.
Use bundle install
to install all dependencies (in this case, just Sinatra), and add a server.rb
file to the application directory.
I'd advise using Ruby 2.3.1. If
bundle install
fails for you, you may need to install Bundler. Do this withgem install bundler
.
Inside the server.rb
file, add the following lines to a) require Sinatra, and b) add a single POST
index route. This is the route Alexa will use to contact your application:
# inside server.rb
require 'sinatra'
post '/' do
# We'll fill this out in a minute
end
Here's your application directory at the end of this step:
.
├── Gemfile
├── Gemfile.lock
├── ngrok
└── server.rb
Ralyxa is a Ruby framework for interacting with Alexa. It simplifies a lot of the interactions on the Ruby side. This walkthrough assumes you are using, at a minimum, version 1.2.0 of Ralyxa. If you don't know your Ralyxa version, you're probably fine.
Add the ralyxa
gem to your Gemfile, and bundle install
. Update the POST /
route in server.rb
with the following:
# inside server.rb
require 'sinatra'
require 'ralyxa'
post '/' do
Ralyxa::Skill.handle(request)
end
This will allow Ralyxa to hook in to, and respond to, any requests that come to your application.
Add a subdirectory within your application directory, called intents
. This is where you will define your intent declarations, which are where you tell Ralyxa how to handle Alexa requests.
Here's your application directory at the end of this step:
.
├── Gemfile
├── Gemfile.lock
├── intents
├── ngrok
└── server.rb
If you start the application now, with
ruby server.rb
, you'll see a warning that you haven't defined any intent declarations. This is expected, as we haven't defined any intent declarations yet.
StartPizzaOrder
intent in the Alexa Developer Portal, with one Utterance: "StartPizzaOrder new pizza".You can jump directly to this step by forking from this commit.
In the next few steps, we hit the main bulk of our workflow. The pattern goes like this:
The first custom intent will be the StartPizzaOrder
intent. Alexa will listen for the user to say:
Order a pizza
And respond with:
Great! What pizza would you like? You can pick from large, medium, and small.
First, define the intent in the intent schema, in the Alexa Developer Portal:
{
"intents": [
{
"intent": "StartPizzaOrder"
}
]
}
Next, define the utterance for this intent:
StartPizzaOrder order a pizza
StartPizzaOrder
intent declaration in the Sinatra application, responding with a prompt to pick a size of pizza.Second, define the intent declaration in the Sinatra application. Add a new file, start_pizza_order.rb
, to the intents subdirectory inside your application directory. Inside this, write the intent declaration as follows:
intent "StartPizzaOrder" do
ask("Great! What pizza would you like? You can pick from large, medium, and small.")
end
It doesn't actually matter what you call this Ruby file, so long as you keep the file extension as
.rb
.
Here's how this works:
intent
says to Ralyxa "handle anything you hear from Alexa with an intent name StartPizzaOrder
"ask
says to Ralyxa "construct a JSON response for Alexa that makes Alexa say 'Great! What pizza would you like...'"Test that your Alexa skill can send a StartPizzaOrder
intent to your Sinatra application by doing the following:
ruby server.rb
.ngrok http 4567
, and copy the HTTPS endpoint ending in .ngrok.io
to the clipboard.LaunchRequest
by saying "Alexa, launch Pizza Buddy"If everything is correctly configured, you'll hear or see a response from your application.
Here's your application directory at the end of this step:
.
├── Gemfile
├── Gemfile.lock
├── intents
│ └── start_pizza_order.rb
├── ngrok
└── server.rb
LaunchRequest
intent declaration in the Sinatra application. Respond with a simple 'welcome' message.Sometimes, a user will want to launch a skill without specifying any particular action to take. This is called a LaunchRequest
. For example:
Alexa, launch Pizza Buddy
Should cause Alexa to respond with:
Welcome to Pizza Buddy. Would you like a new pizza, or to list orders?
A LaunchRequest
is a built-in intent, so we don't need to define it in our intent schema. We can jump straight to implementing an intent declaration.
Add a new file, launch_request.rb
, to the intents subdirectory inside your application directory. Inside this, write an intent declaration as follows:
intent "LaunchRequest" do
ask("Welcome to Pizza Buddy. Would you like a new pizza, or to list orders?")
end
Test that your Alexa skill can send a LaunchRequest
to your Sinatra application by doing the following:
LaunchRequest
by saying "Alexa, launch Pizza Buddy"
{
"session": {
"sessionId": "REDACTED",
"application": {
"applicationId": "REDACTED"
},
"attributes": {},
"user": {
"userId": "REDACTED"
},
"new": true
},
"request": {
"type": "LaunchRequest",
"requestId": "REDACTED",
"locale": "en-GB",
"timestamp": "2017-05-09T15:39:26Z"
},
"version": "1.0"
}
You don't have to replace the words
REDACTED
with anything for this to send a successfulLaunchRequest
.
Here's your application directory at the end of this step:
.
├── Gemfile
├── Gemfile.lock
├── intents
│ ├── launch_request.rb
│ └── start_pizza_order.rb
├── ngrok
└── server.rb
Pizza
object to the Sinatra application, which presents the available sizes of pizzaYou can jump directly to this step by forking from this commit.
Our StartPizzaOrder
intent declaration currently reads as follows:
intent "StartPizzaOrder" do
ask("Great! What pizza would you like? You can pick from large, medium, and small.")
end
It would be great if this intent declaration read like this:
intent "StartPizzaOrder" do
ask("Great! What pizza would you like? You can pick from #{ Pizza::SIZES.to_sentence }")
end
Let's extract a Pizza
object to hold the available sizes of pizza we offer. We'll follow a Test-Driven Development methodology. First, install RSpec to the project by adding the rspec
gem to your Gemfile, and using bundle install
to install the dependency. Then, initialise RSpec using rspec --init
from the command line.
rspec --init
should generate a couple of files for you, and a spec subdirectory. This is where our test files go.
Write a test for the Pizza
object in a spec file, pizza_spec.rb
, inside the spec subdirectory of your application directory:
# in spec/pizza_spec.rb
require 'pizza'
RSpec.describe Pizza do
describe 'SIZES' do
it 'holds the available pizza sizes' do
expect(described_class::SIZES).to eq [:large, :medium, :small]
end
end
end
Run the test using rspec spec
from the application directory. It will fail: you need to create a lib directory, containing a pizza.rb
file. This file will contain your Pizza
code:
# in lib/pizza.rb
class Pizza
SIZES = [:large, :medium, :small]
end
Run the test again using rspec spec
from the application directory. It should pass. We can now use our Pizza
object in our intent declaration.
Before we can use it as intended, however, you will need to add the activesupport
gem to your Gemfile and bundle install
again. Now, we can express our available Pizza sizes as a string:
# in intents/start_pizza_order.rb
require './lib/pizza'
require 'active_support/core_ext/array/conversions'
intent "StartPizzaOrder" do
ask("Great! What pizza would you like? You can pick from #{ Pizza::SIZES.to_sentence }")
end
We can change our Pizza sizes easily, by altering the Pizza::SIZES
constant and restarting the server.
Here's your application directory at the end of this step:
.
├── Gemfile
├── Gemfile.lock
├── intents
│ ├── launch_request.rb
│ └── start_pizza_order.rb
├── lib
│ └── pizza.rb
├── spec
│ ├── pizza_spec.rb
│ └── spec_helper.rb
├── ngrok
└── server.rb
ContinuePizzaOrder
intent in the Alexa Developer Portal, with one custom slot: the available sizes of pizzaYou can jump directly to this step by forking from this commit.
Remember our workflow?
Firstly, we need an Intent to handle the user's choice of pizza size, and an accompanying Utterance.
Define a ContinuePizzaOrder
intent in the Alexa Developer Portal. This Intent needs one slot: the requested pizza size. Here's the complete intent schema for this, including previous steps:
{
"intents": [
{
"intent": "StartPizzaOrder"
},
{
"intent": "ContinuePizzaOrder",
"slots": [
{
"name": "size",
"type": "PIZZA_SIZE"
}
]
}
]
}
PIZZA_SIZE
is not a built-in slot type. Now define a custom slot type, named PIZZA_SIZE
, with the possible pizza sizes (small, medium, large) as guiding values. These will help Alexa to recognise the kind of pizza the user is asking for. Now, define utterances to invoke this intent. Here's the complete utterances after this step:
StartPizzaOrder order a pizza
ContinuePizzaOrder {size}
ContinuePizzaOrder a {size} pizza
I've added two alternative utterances. This is because it's quite likely our user will interact with the application in different ways. It's reasonable to expect the user to reply to "what size of pizza?" with "small/medium/large", or "a small/medium/large pizza". There are other possibilities too: you can add as many variants as you wish.
ContinuePizzaOrder
intent declaration which saves the user's choice of pizza size to the session, and prompts for pizza toppingsSecondly, we need an intent declaration in Sinatra to handle the request Alexa will serve our application when the ContinuePizzaOrder
intent is invoked.
Define a declaration that pulls the requested size from the size
slot, and persists it to the session for later use:
# in intents/continue_pizza_order.rb
require './lib/pizza'
intent "ContinuePizzaOrder" do
size = request.slot_value("size")
response_text = ["OK, a #{ size } pizza. What would you like on that pizza? ",
"You can choose up to five items, or ask for a list of ",
"toppings. Or, choose another size: #{ Pizza::SIZES.to_sentence }"].join
ask(response_text, session_attributes: { size: size })
end
The
response_text
was pretty long, so I split it into an array of 'sentence parts'. Then, I'vejoin
ed the array into the final string.
Finally, we need to test that Alexa and Sinatra play together nicely. In the Service Simulator, on-device, or through Echosim.io, try asking Pizza Buddy for "a large pizza". You should be prompted to give your choice of toppings.
PenultimatePizzaOrder
intent in the Alexa Developer Portal, with slots for up to five toppingsYou can jump directly to this step by forking from this commit.
The user can order a pizza with a given size. Now, they're being prompted to respond with a list of toppings. The ideal conversation flow should go something like this:
User: Alexa, ask Pizza Buddy to order a pizza Alexa: Great! What pizza would you like? You can pick from large, medium, and small. User: A small pizza Alexa: OK, a small pizza. What would you like on that pizza? You can choose up to five items, or ask for a list of toppings. Or, choose another size: large, medium, and small. User: Cheese, ham, mushrooms Alexa: "OK, a small pizza with cheese, ham, and mushrooms. Is that right?
Let's start from our workflow:
We need an intent to handle the user's choice of pizza toppings, and an accompanying utterances. This is trickier than offering pizza sizes, as we want our intent to handle anywhere between one and five toppings.
First, define a PenultimatePizzaOrder
intent in the Alexa Developer Portal. This intent needs five slots: the requested pizza toppings. Here's the complete intent schema for this, including previous steps:
{
"intents": [
{
"intent": "StartPizzaOrder"
},
{
"intent": "ContinuePizzaOrder",
"slots": [
{
"name": "size",
"type": "PIZZA_SIZE"
}
]
},
{
"intent": "PenultimatePizzaOrder",
"slots": [
{
"name": "toppingOne",
"type": "PIZZA_TOPPING"
},
{
"name": "toppingTwo",
"type": "PIZZA_TOPPING"
},
{
"name": "toppingThree",
"type": "PIZZA_TOPPING"
},
{
"name": "toppingFour",
"type": "PIZZA_TOPPING"
},
{
"name": "toppingFive",
"type": "PIZZA_TOPPING"
}
]
}
]
}
PIZZA_TOPPING
is not a built-in slot type. Define a custom slot type, named PIZZA_TOPPING
, with some possible pizza toppings as guiding values. I chose: tomato sauce, barbecue sauce, cheese, ham, pineapple, pepperoni, mushrooms, sweetcorn, and olives. These values will help Alexa to recognise the toppings the user is asking for.
These values only guide Alexa's understanding. Users can still order toppings outside of this given range. We'll handle that case in section 13.
PenultimatePizzaOrder
, handling between one and five toppingsNow, define utterances to invoke this intent. Since we want this intent to be invoked with between one and five toppings, we define cascading utterances. Here are the complete utterances after this step:
StartPizzaOrder order a pizza
ContinuePizzaOrder {size}
ContinuePizzaOrder a {size} pizza
PenultimatePizzaOrder {toppingOne}
PenultimatePizzaOrder {toppingOne} {toppingTwo}
PenultimatePizzaOrder {toppingOne} {toppingTwo} {toppingThree}
PenultimatePizzaOrder {toppingOne} {toppingTwo} {toppingThree} {toppingFour}
PenultimatePizzaOrder {toppingOne} {toppingTwo} {toppingThree} {toppingFour} {toppingFive}
The PenultimatePizzaOrder
intent can now be invoked with between one and five toppings. If a user provides, say, four toppings, the final slot (toppingFive
) will be empty.
PenultimatePizzaOrder
intent declaration confirming the size and toppingsYou can jump directly to this step by forking from this commit.
Our second step is to define an intent declaration in Sinatra to handle the request Alexa will serve our application when the PenultimatePizzaOrder
intent is invoked.
Define a declaration that pulls the toppings from the toppingOne
to toppingFive
slots (if they contain data), presents them to the user, and asks them to continue. Also, persist the size and toppings to the session for later use:
# inside intents/penultimate_pizza_order
require 'active_support/core_ext/array/conversions'
intent "PenultimatePizzaOrder" do
size = request.session_attribute('size')
toppings = ['One', 'Two', 'Three', 'Four', 'Five'].inject([]) do |toppings, topping_number|
topping = request.slot_value("topping#{ topping_number }")
topping ? toppings + [topping] : toppings
end
ask("OK, a #{ size } pizza with #{ toppings.to_sentence }. Is that right?", session_attributes: { size: size, toppings: toppings })
end
The
inject
function in the middle of this intent declaration iterates through available toppings and compiles an array of requested toppings (accounting for null values if the user ordered fewer than five toppings).
We need a mechanism to stop the user from ordering non-existent toppings. An ideal interaction (from the start) would go like this:
User: Alexa, ask Pizza Buddy to order a pizza Alexa: Great! What pizza would you like? You can pick from large, medium, and small. User: A small pizza Alexa: OK, a small pizza. What would you like on that pizza?... User: Cheese, ham, forgiveness Alexa: I'm afraid we don't have forgiveness. Please choose your toppings again, or ask for a list of available toppings.
Alexa should reply in the negative ("we don't have { unavailable topping }"), and prompt for the user to choose their toppings again.
Additionally, Alexa can offer the user the chance to hear a list of all toppings. We'll implement handling this later.
For easy changing, let's implement a method on Pizza
that filters a given list of toppings, and returns disallowed ones. Start with a test:
# in spec/pizza_spec.rb
require 'pizza'
RSpec.describe Pizza do
describe 'SIZES' do
it 'holds the available pizza sizes' do
expect(described_class::SIZES).to eq [:small, :medium, :large]
end
end
describe '.disallowed_toppings' do
it 'filters a list of toppings, returning the disallowed ones' do
expect(described_class.disallowed_toppings(['mushrooms', 'tomato sauce'])).to be_empty
expect(described_class.disallowed_toppings(['gold', 'forgiveness'])).to eq ['gold', 'forgiveness']
expect(described_class.disallowed_toppings(['mushrooms', 'forgiveness'])).to eq ['forgiveness']
end
end
end
And an implementation:
# in lib/pizza.rb
class Pizza
SIZES = [:small, :medium, :large]
TOPPINGS = [
:tomato_sauce,
:barbecue_sauce,
:cheese,
:ham,
:pineapple,
:pepperoni,
:mushrooms,
:sweetcorn,
:olives
]
def self.disallowed_toppings(toppings)
toppings.reject { |topping| allowed_topping?(topping) }
end
private
def self.allowed_topping?(topping)
TOPPINGS.include? topping.gsub(" ", "_").to_sym
end
end
I've opted to make the list of possible toppings available as the
Pizza::TOPPINGS
constant. I chose to do this because it neatly mirrors the Custom Values we picked in the Alexa Developer Console, which makes updating these options easier in future. This way, we are always anticipating change in certain areas of the program.
We can now use this Pizza.disallowed_toppings(toppings)
method in our intent declaration, to compile and return a list of disallowed toppings if the user gave us any. If the disallowed toppings are empty – the user gave us only allowed toppings – we can proceed as normal.
# inside intents/penultimate_pizza_order.rb
require './lib/pizza'
require 'active_support/core_ext/array/conversions'
intent "PenultimatePizzaOrder" do
size = request.session_attribute('size')
toppings = ['One', 'Two', 'Three', 'Four', 'Five'].inject([]) do |toppings, topping_number|
topping = request.slot_value("topping#{ topping_number }")
topping ? toppings + [topping] : toppings
end
disallowed_toppings = Pizza.disallowed_toppings(toppings)
if disallowed_toppings.empty?
ask("OK, a #{ size } pizza with #{ toppings.to_sentence }. Is that right?", session_attributes: { size: size, toppings: toppings })
else
response_text = "I'm afraid we don't have #{ disallowed_toppings.to_sentence }. Please choose your toppings again, or ask for a list of available toppings."
ask(response_text, session_attributes: { size: size })
end
end
ConfirmPizzaOrder
intent in the Alexa Developer PortalYou can jump directly to this step by forking from this commit.
If the user has requested available toppings, and no disallowed toppings, they are prompted to confirm their order. Here's how this conversation could go:
User: Alexa, ask Pizza Buddy to order a pizza Alexa: Great! What pizza would you like? You can pick from large, medium, and small. User: A small pizza Alexa: OK, a small pizza. What would you like on that pizza? You can choose up to five items, or ask for a list of toppings. Or, choose another size: large, medium, and small. User: Cheese, ham, mushrooms Alexa: "OK, a small pizza with cheese, ham, and mushrooms. To confirm, say 'confirm my order'. User: Confirm my order Alexa: Thanks! Your small pizza with cheese, ham, and mushrooms is on its way to you. Your order ID is #1. Thank you for using Pizza Buddy!
Define an intent to handle this confirmation:
{
"intents": [
{
"intent": "StartPizzaOrder"
},
{
"intent": "ContinuePizzaOrder",
"slots": [
{
"name": "size",
"type": "PIZZA_SIZE"
}
]
},
{
"intent": "PenultimatePizzaOrder",
"slots": [
{
"name": "toppingOne",
"type": "PIZZA_TOPPING"
},
{
"name": "toppingTwo",
"type": "PIZZA_TOPPING"
},
{
"name": "toppingThree",
"type": "PIZZA_TOPPING"
},
{
"name": "toppingFour",
"type": "PIZZA_TOPPING"
},
{
"name": "toppingFive",
"type": "PIZZA_TOPPING"
}
]
},
{
"intent": "ConfirmPizzaOrder"
}
]
}
Define utterances for the ConfirmPizzaOrder
intent:
StartPizzaOrder order a pizza
ContinuePizzaOrder {size}
ContinuePizzaOrder a {size} pizza
PenultimatePizzaOrder {toppingOne}
PenultimatePizzaOrder {toppingOne} {toppingTwo}
PenultimatePizzaOrder {toppingOne} {toppingTwo} {toppingThree}
PenultimatePizzaOrder {toppingOne} {toppingTwo} {toppingThree} {toppingFour}
PenultimatePizzaOrder {toppingOne} {toppingTwo} {toppingThree} {toppingFour} {toppingFive}
ConfirmPizzaOrder confirm my order
ConfirmPizzaOrder
intent declaration, which saves the confirmed pizza to the databaseWe want to persist the user's order to a database, for later retrieval. To do this, we need to set up a Postgres database and an ORM to communicate between Sinatra and Postgres. I'm using Datamapper, which is a well-supported Ruby ORM. You can use any selection of technology you prefer!
First of all, update the Gemfile to include the following gems:
data_mapper
, which gives us an ORM, Datamapperdm-postgres-adapter
, which allows Datamapper to talk with our database, Postgresdm-postgres-types
, which will allow us to use the Array
Postgres type to store multiple toppingsHere is the complete Gemfile at this point:
# in Gemfile
source "https://rubygems.org"
ruby '2.3.1'
gem 'sinatra'
gem 'rspec'
gem 'ralyxa'
gem 'activesupport'
gem 'data_mapper'
gem 'dm-postgres-adapter'
gem 'dm-postgres-types'
Run bundle install
to install dependencies to this project. To set up a Postgres database, follow the PostgreSQL Sinatra recipe. Make two databases: one for development, and one for testing. I named mine pizzabuddydevelopment
and pizzabuddytest
, all with a user named pizzabuddy
.
Next, configure Sinatra to connect with Postgres on startup. Create a database configuration file, database.rb
:
# inside database.rb
require './lib/pizza'
configure :development do
DataMapper.setup(:default, 'postgres://pizzabuddy@localhost/pizzabuddydevelopment')
end
DataMapper.finalize
Pizza.auto_upgrade!
Include and immediately execute this file inside the server.rb
:
# in server.rb
require 'sinatra'
require 'ralyxa'
load './database.rb'
post '/' do
Ralyxa::Skill.handle(request)
end
Configure the tests to use the test database, and add tests for saving the Pizza
object:
# in spec/pizza_spec.rb, with some omissions for brevity
require 'pizza'
RSpec.describe Pizza do
before do
DataMapper.setup(:default, 'postgres://pizzabuddy@localhost/pizzabuddytest')
DataMapper.finalize
Pizza.auto_migrate!
end
describe 'Saving to a database' do
it 'starts out unpersisted' do
pizza = Pizza.new(size: 'small', toppings: ['cheese', 'ham'])
expect(pizza.id).to be_nil
end
it 'can be persisted' do
pizza = Pizza.new(size: 'small', toppings: ['cheese', 'ham'])
pizza.save
expect(pizza.id).not_to be_nil
end
end
end
Implement the desired saving functionality on Pizza
:
# in lib/pizza.rb
require 'data_mapper'
require 'dm-postgres-types'
class Pizza
include DataMapper::Resource
SIZES = [:small, :medium, :large]
TOPPINGS = [
:tomato_sauce,
:barbecue_sauce,
:cheese,
:ham,
:pineapple,
:pepperoni,
:mushrooms,
:sweetcorn,
:olives
]
property :id, Serial
property :size, String
property :toppings, PgArray
def self.disallowed_toppings(toppings)
toppings.reject { |topping| allowed_topping?(topping) }
end
private
def self.allowed_topping?(topping)
TOPPINGS.include? topping.gsub(" ", "_").to_sym
end
end
Note that we are using the special Postgres Array type,
PgArray
, to save multiple toppings in a single database column. If you are using another SQL database, you may need to store them in individual columns, or relationally.
Run the tests using rspec
: our Pizza should correctly save to the database. Now, we can use our upgraded Pizza
to give the user a real ID in response to order confirmation:
require './lib/pizza'
require 'active_support/core_ext/array/conversions'
intent "ConfirmPizzaOrder" do
pizza = Pizza.new(size: request.session_attribute('size'), toppings: request.session_attribute('toppings'))
pizza.save
response_text = ["Thanks! Your #{ pizza.size } pizza with #{ pizza.toppings.to_sentence } is on ",
"its way to you. Your order ID is #{ pizza.id }. Thank you for using Pizza Buddy!"].join
tell(response_text)
end
Note that I am using a
tell
to give the user their confirmation. This will close the session (and clear any session attributes). It's like 'ending' the conversation with the user.
ListOrders
intent, listing Pizza
entities saved in the databaseYou can jump directly to this step by forking from this commit.
Users should be able to list previously-placed orders (regardless of who ordered them, for now). An ideal conversation would go:
User: Alexa, ask Pizza Buddy to list orders Alexa: There are 14 orders. Here are the first four: a small pizza with cheese and ham...you can ask to list orders again, or order a pizza.
First, define the intent in the intent schema:
{
"intents": [
{
"intent": "StartPizzaOrder"
},
{
"intent": "ContinuePizzaOrder",
"slots": [
{
"name": "size",
"type": "PIZZA_SIZE"
}
]
},
{
"intent": "PenultimatePizzaOrder",
"slots": [
{
"name": "toppingOne",
"type": "PIZZA_TOPPING"
},
{
"name": "toppingTwo",
"type": "PIZZA_TOPPING"
},
{
"name": "toppingThree",
"type": "PIZZA_TOPPING"
},
{
"name": "toppingFour",
"type": "PIZZA_TOPPING"
},
{
"name": "toppingFive",
"type": "PIZZA_TOPPING"
}
]
},
{
"intent": "ConfirmPizzaOrder"
},
{
"intent": "ListOrders"
}
]
}
Add an utterance:
StartPizzaOrder order a pizza
ContinuePizzaOrder {size}
ContinuePizzaOrder a {size} pizza
PenultimatePizzaOrder {toppingOne}
PenultimatePizzaOrder {toppingOne} {toppingTwo}
PenultimatePizzaOrder {toppingOne} {toppingTwo} {toppingThree}
PenultimatePizzaOrder {toppingOne} {toppingTwo} {toppingThree} {toppingFour}
PenultimatePizzaOrder {toppingOne} {toppingTwo} {toppingThree} {toppingFour} {toppingFive}
ConfirmPizzaOrder confirm my order
ListOrders list orders
And an intent declaration:
# in intents/list_orders.rb
require './lib/pizza'
require 'active_support/core_ext/array/conversions'
intent "ListOrders" do
orders = Pizza.first(4).map { |order| "a #{ order.size } pizza with #{ order.toppings.to_sentence }" }
response_text = ["There are #{ Pizza.count } orders. ",
"Here are the first four: #{ orders.to_sentence }.",
"You can ask to list orders again, or order a pizza."].join
ask(response_text)
end
Remember to test the implementation of this intent using the Service Simulator, Echosim.io, or your Alexa device.
ListToppings
intent, which lists the available permitted toppingsYou can jump directly to this step by forking from this commit.
Users should also be able to list available toppings. An ideal conversation would go:
User: Alexa, ask Pizza Buddy to list toppings Alexa: We have the following available toppings: tomato sauce, barbecue sauce, cheese.... You can order a pizza, or list previous orders.
First, define the intent in the intent schema:
{
"intents": [
{
"intent": "StartPizzaOrder"
},
{
"intent": "ContinuePizzaOrder",
"slots": [
{
"name": "size",
"type": "PIZZA_SIZE"
}
]
},
{
"intent": "PenultimatePizzaOrder",
"slots": [
{
"name": "toppingOne",
"type": "PIZZA_TOPPING"
},
{
"name": "toppingTwo",
"type": "PIZZA_TOPPING"
},
{
"name": "toppingThree",
"type": "PIZZA_TOPPING"
},
{
"name": "toppingFour",
"type": "PIZZA_TOPPING"
},
{
"name": "toppingFive",
"type": "PIZZA_TOPPING"
}
]
},
{
"intent": "ConfirmPizzaOrder"
},
{
"intent": "ListOrders"
},
{
"intent": "ListToppings"
}
]
}
Add an utterance:
StartPizzaOrder order a pizza
ContinuePizzaOrder {size}
ContinuePizzaOrder a {size} pizza
PenultimatePizzaOrder {toppingOne}
PenultimatePizzaOrder {toppingOne} {toppingTwo}
PenultimatePizzaOrder {toppingOne} {toppingTwo} {toppingThree}
PenultimatePizzaOrder {toppingOne} {toppingTwo} {toppingThree} {toppingFour}
PenultimatePizzaOrder {toppingOne} {toppingTwo} {toppingThree} {toppingFour} {toppingFive}
ConfirmPizzaOrder confirm my order
ListOrders list orders
ListToppings list toppings
And an intent declaration:
require './lib/pizza'
require 'active_support/core_ext/array/conversions'
intent "ListToppings" do
response_text = [
"We have the following available toppings: ",
"#{ Pizza::TOPPINGS.map { |topping| topping.to_s.gsub("_", " ") }.to_sentence }. ",
"You can order a pizza, or list previous orders."
].join
ask(response_text)
end
Again, remember to test that your integration works!
You can jump directly to this step by forking from this commit.
Finally, we're going to add a Standard Card to the user's order, with a picture of a pizza and some details about their meal.
Alexa cards come in three types:
Using Ralyxa's Card API, let's add a card to the ConfirmPizzaOrder
intent declaration response:
# in intents/confirm_pizza_order.rb
require './lib/pizza'
require 'active_support/core_ext/array/conversions'
intent "ConfirmPizzaOrder" do
pizza = Pizza.new(size: request.session_attribute('size'), toppings: request.session_attribute('toppings'))
pizza.save
response_text = ["Thanks! Your #{ pizza.size } pizza with #{ pizza.toppings.to_sentence } is on ",
"its way to you. Your order ID is #{ pizza.id }. Thank you for using Pizza Buddy!"].join
# Here are all the Standard Card 'bits'
card_title = "Your Pizza Order ##{ pizza.id }"
card_body = "You ordered a #{ pizza.size } pizza with #{ pizza.toppings.to_sentence }!"
card_image = "https://image.ibb.co/jeRZLv/alexa_pizza.png"
# Here we call the `card` method, which constructs the card JSON
pizza_card = card(card_title, card_body, card_image)
# Here we add the constructed card to the `tell` response
tell(response_text, card: pizza_card)
end
If you're feeling adventurous, why not customise the card picture depending on the ordered pizza? Your picture must be under 2MB, and accessible via an SSL-enabled (HTTPS) endpoint.
Congratulations! You now know how to build an intermediate Alexa skill with sessions, persisted data, and cards in Ruby. This concludes module 5. You can fork the completed application at the end of this module from this commit.
The Alexa Skills Kit (ASK) enables developers to build capabilities, called skills, for Alexa. ASK is a collection of self-service APIs, documentation, tools, and code samples that make it fast and easy for anyone to add skills to Alexa.
Developers have built more than 12,000 skills with ASK. Explore the stories behind some of these innovations, then start building your own skill. Once you publish your skill, mark the occasion with a free, limited-edition Alexa dev shirt. Quantities are limited.