Lab 4: Add a Multimodal Response to Your Skill
Welcome to lab 4 of your skill. In this lab, you'll learn how to add multimodality to your skill by using APL.
Time required: 30 minutes
What you'll learn
- How to add a multimodal response
- How to use multimodal response in your dialogs
- How to make changes in your skill backend to support APL
- (Optional) How to use APL templates on welcome and out of domain responses
Introduction
In lab 3, you built an Alexa Conversations skill that supports voice-only responses. When the requested flight is found, the skill will prompt the voice-only response which contains the details.
In this lab, you will learn how to extend the response by adding visuals. We will add an APL template to the flight search result to enable multimodality in APL supported devices, such as echo show.
Step 1: Enabling APL support in skill manifest
First, we need to enable APL support in the skill manifest so that our skill can support multimodality. To do so, we need to make changes in "skill.json" file under "skill-package" directory.
We need to add "interfaces" under "manifest.apis.custom":
...
"apis": {
"custom": {
"dialogManagement": {
"sessionStartDelegationStrategy": {
"target": "AMAZON.Conversations"
},
"dialogManagers": [
{
"type": "AMAZON.Conversations"
}
]
},
"interfaces": [
{
"supportedViewports": [
{
"maxHeight": 540,
"maxWidth": 960,
"minHeight": 540,
"minWidth": 960,
"mode": "TV",
"shape": "RECTANGLE"
},
{
"maxHeight": 599,
"maxWidth": 599,
"minHeight": 100,
"minWidth": 100,
"mode": "HUB",
"shape": "ROUND"
},
{
"maxHeight": 959,
"maxWidth": 1279,
"minHeight": 600,
"minWidth": 960,
"mode": "HUB",
"shape": "RECTANGLE"
},
{
"maxHeight": 1279,
"maxWidth": 1920,
"minHeight": 600,
"minWidth": 1280,
"mode": "HUB",
"shape": "RECTANGLE"
},
{
"maxHeight": 599,
"maxWidth": 1279,
"minHeight": 100,
"minWidth": 960,
"mode": "HUB",
"shape": "RECTANGLE"
},
{
"maxHeight": 1279,
"maxWidth": 2560,
"minHeight": 960,
"minWidth": 1920,
"mode": "HUB",
"shape": "RECTANGLE"
},
{
"maxHeight": 2560,
"maxWidth": 1279,
"minHeight": 1920,
"minWidth": 960,
"mode": "HUB",
"shape": "RECTANGLE"
},
{
"maxHeight": 1920,
"maxWidth": 959,
"minHeight": 320,
"minWidth": 600,
"mode": "MOBILE",
"shape": "RECTANGLE"
},
{
"maxHeight": 1920,
"maxWidth": 1279,
"minHeight": 320,
"minWidth": 960,
"mode": "MOBILE",
"shape": "RECTANGLE"
},
{
"maxHeight": 1920,
"maxWidth": 1920,
"minHeight": 320,
"minWidth": 1280,
"mode": "MOBILE",
"shape": "RECTANGLE"
}
],
"type": "ALEXA_PRESENTATION_APL"
}
],
"endpoint": {
...
Your skill.json file will look as indicated below.
{
"manifest": {
"publishingInformation": {
"locales": {
"en-US": {
"summary": "Sample introductory skill to ACDL",
"examplePhrases": [
"Alexa open flight search",
"I want to travel from Seattle to San Francisco on Sunday",
"help"
],
"name": "Flight Search",
"description": "This skill is an example only and is not intended to refer to actual airlines or airline data"
}
},
"isAvailableWorldwide": true,
"testingInstructions": "Sample Testing Instructions.",
"category": "KNOWLEDGE_AND_TRIVIA",
"distributionCountries": []
},
"apis": {
"custom": {
"dialogManagement": {
"sessionStartDelegationStrategy": {
"target": "AMAZON.Conversations"
},
"dialogManagers": [
{
"type": "AMAZON.Conversations"
}
]
},
"interfaces": [
{
"supportedViewports": [
{
"maxHeight": 540,
"maxWidth": 960,
"minHeight": 540,
"minWidth": 960,
"mode": "TV",
"shape": "RECTANGLE"
},
{
"maxHeight": 599,
"maxWidth": 599,
"minHeight": 100,
"minWidth": 100,
"mode": "HUB",
"shape": "ROUND"
},
{
"maxHeight": 959,
"maxWidth": 1279,
"minHeight": 600,
"minWidth": 960,
"mode": "HUB",
"shape": "RECTANGLE"
},
{
"maxHeight": 1279,
"maxWidth": 1920,
"minHeight": 600,
"minWidth": 1280,
"mode": "HUB",
"shape": "RECTANGLE"
},
{
"maxHeight": 599,
"maxWidth": 1279,
"minHeight": 100,
"minWidth": 960,
"mode": "HUB",
"shape": "RECTANGLE"
},
{
"maxHeight": 1279,
"maxWidth": 2560,
"minHeight": 960,
"minWidth": 1920,
"mode": "HUB",
"shape": "RECTANGLE"
},
{
"maxHeight": 2560,
"maxWidth": 1279,
"minHeight": 1920,
"minWidth": 960,
"mode": "HUB",
"shape": "RECTANGLE"
},
{
"maxHeight": 1920,
"maxWidth": 959,
"minHeight": 320,
"minWidth": 600,
"mode": "MOBILE",
"shape": "RECTANGLE"
},
{
"maxHeight": 1920,
"maxWidth": 1279,
"minHeight": 320,
"minWidth": 960,
"mode": "MOBILE",
"shape": "RECTANGLE"
},
{
"maxHeight": 1920,
"maxWidth": 1920,
"minHeight": 320,
"minWidth": 1280,
"mode": "MOBILE",
"shape": "RECTANGLE"
}
],
"type": "ALEXA_PRESENTATION_APL"
}
],
"endpoint": {
"uri": "{your lambda endpoint}"
}
}
},
"manifestVersion": "1.0"
}
}
Step 2: Add an APL template
Similar to the APL-A templates (voice responses) we created in the previous lab, we will add an APL template to support the visual responses.
- First, create a new folder named "display" under the "skill-package/response" directory. This folder will contain the APL files that your skill uses.
- In order to create an APL file to support flight search results, we will create a new folder under "display" called "FlightSearchResponseVisual".
- Next, create a "document.json" file under newly created "FlightSearchResponseVisual" folder and copy the below code:
Copied to clipboard.
{
"type": "APL",
"version": "1.8",
"license": "Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.\nSPDX-License-Identifier: LicenseRef-.amazon.com.-AmznSL-1.0\nLicensed under the Amazon Software License http://aws.amazon.com/asl/",
"theme": "dark",
"import": [
{
"name": "alexa-layouts",
"version": "1.5.0"
}
],
"resources": [
{
"description": "Public resource definitions Simple Text",
"colors": {
"colorText": "@colorText"
},
"dimensions": {
"headerHeight": "${@headerAttributionIconMaxHeight + (2 * @spacingLarge)}",
"simpleTextScrollHeightWithFooter": "${viewport.height - @headerHeight - @footerPaddingTop - @footerPaddingBottom}",
"simpleTextScrollHeightNoFooter": "100%",
"simpleTextImageHorizontalSpacing": "@spacingLarge",
"simpleTextImageVerticalSpacing": "@spacingMedium",
"simpleTextPaddingBottom": "@spacingLarge",
"simpleLineHeight": "1.5"
}
},
{
"when": "${@viewportProfileCategory == @hubRound}",
"dimensions": {
"simpleTextPaddingBottom": "@spacing3XLarge"
}
},
{
"when": "${@viewportProfile == @hubLandscapeSmall}",
"dimensions": {
"simpleTextScrollHeightWithFooter": "100%"
}
},
{
"when": "${viewport.theme == 'light'}",
"colors": {
"colorText": "@colorTextReversed"
}
}
],
"layouts": {
"SimpleText": {
"parameters": [
{
"name": "backgroundImageSource",
"description": "URL for the background image source.",
"type": "string"
},
{
"name": "footerHintText",
"description": "Hint text to display in the footer.",
"type": "string"
},
{
"name": "foregroundImageLocation",
"description": "Location of the forground image. Options are top, bottom, left, and right. Default is top.",
"type": "string",
"default": "top"
},
{
"name": "foregroundImageSource",
"description": "URL for the foreground image source. If blank, the template will be full text layout.",
"type": "string"
},
{
"name": "headerAttributionImage",
"description": "URL for attribution image or logo source (PNG/vector).",
"type": "string"
},
{
"name": "headerTitle",
"description": "Title text to render in the header.",
"type": "string"
},
{
"name": "headerSubtitle",
"description": "Subtitle Text to render in the header.",
"type": "string"
},
{
"name": "primaryText",
"description": "Text for to render below to tile text in the body.",
"type": "string"
},
{
"name": "secondaryText",
"description": "Text for to render below to primary text in the body.",
"type": "string"
},
{
"name": "titleText",
"description": "Title text to render in the body.",
"type": "string"
},
{
"name": "textAlignment",
"description": "Alignment of text content. Options are start, and center. Default is start.",
"type": "string",
"default": "start"
}
],
"item": {
"type": "Container",
"height": "100vh",
"width": "100vw",
"bind": [
{
"name": "imageCenterAlign",
"type": "boolean",
"value": "${@viewportProfileCategory == @hubRound || foregroundImageLocation == 'top' || foregroundImageLocation == 'bottom'}"
},
{
"name": "hasFooter",
"type": "boolean",
"value": "${@viewportProfileCategory != @hubRound && @viewportProfile != @hubLandscapeSmall && footerHintText}"
}
],
"items": [
{
"type": "AlexaBackground",
"backgroundColor": "${backgroundColor}",
"backgroundImageSource": "${backgroundImageSource}",
"colorOverlay": true
},
{
"when": "${@viewportProfileCategory != @hubRound}",
"type": "AlexaHeader",
"layoutDirection": "${environment.layoutDirection}",
"headerAttributionImage": "${headerAttributionImage}",
"headerTitle": "${headerTitle}",
"headerSubtitle": "${headerSubtitle}",
"headerAttributionPrimacy": true,
"width": "100%"
},
{
"description": "Footer Hint Text - not displaying on small hubs",
"when": "${@viewportProfileCategory != @hubRound && @viewportProfile != @hubLandscapeSmall && footerHintText}",
"type": "AlexaFooter",
"hintText": "${footerHintText}",
"theme": "${viewport.theme}",
"width": "100%",
"position": "absolute",
"bottom": "0"
},
{
"type": "ScrollView",
"height": "${hasFooter ? @simpleTextScrollHeightWithFooter : @simpleTextScrollHeightNoFooter}",
"width": "100vw",
"shrink": 1,
"items": [
{
"type": "Container",
"width": "100%",
"padding": [
"@marginHorizontal",
0
],
"paddingBottom": "@simpleTextPaddingBottom",
"justifyContent": "center",
"alignItems": "center",
"items": [
{
"when": "${@viewportProfileCategory == @hubRound}",
"type": "AlexaHeader",
"layoutDirection": "${environment.layoutDirection}",
"headerAttributionImage": "${headerAttributionImage}",
"headerAttributionPrimacy": true,
"width": "100%"
},
{
"description": "Image and text content block",
"type": "Container",
"width": "100%",
"alignItems": "${imageCenterAlign ? 'center' : 'start'}",
"direction": "${foregroundImageLocation == 'left' ? 'row' : (foregroundImageLocation == 'right' ? 'rowReverse' : (foregroundImageLocation == 'bottom' ? 'columnReverse' : 'column'))}",
"shrink": 1,
"items": [
{
"shrink": 1,
"items": [
{
"description": "Title Text",
"when": "${titleText}",
"type": "Text",
"width": "100%",
"style": "textStyleDisplay3",
"text": "${titleText}",
"textAlign": "${@viewportProfileCategory == @hubRound ? 'center' : textAlignment}"
},
{
"description": "Primary Text",
"when": "${primaryText}",
"type": "Text",
"spacing": "@spacing2XSmall",
"style": "textStyleBody2",
"fontWeight": "@fontWeightLight",
"text": "${primaryText}",
"textAlign": "${@viewportProfileCategory == @hubRound ? 'center' : textAlignment}"
},
{
"fontWeight": "@fontWeightLight",
"text": "${secondaryText}",
"textAlign": "${@viewportProfileCategory == @hubRound ? 'center' : textAlignment}",
"lineHeight": "@simpleLineHeight",
"description": "Secondary Text",
"when": "${secondaryText}",
"type": "Text",
"style": "textStyleBody",
"paddingTop": "@spacingLarge",
"spacing": "@spacing2XSmall"
}
],
"alignItems": "start",
"description": "Primary Text and Title Text block",
"when": "${primaryText || titleText}",
"type": "Container",
"width": "100%",
"paddingTop": "${foregroundImageLocation == 'top' ? @simpleTextImageVerticalSpacing : '0dp'}",
"paddingBottom": "${foregroundImageLocation == 'bottom' ? @simpleTextImageVerticalSpacing : '0dp'}",
"paddingStart": "${foregroundImageLocation == 'left' && foregroundImageSource ? @simpleTextImageHorizontalSpacing : '0dp'}",
"paddingEnd": "${foregroundImageLocation == 'right' && foregroundImageSource ? @simpleTextImageHorizontalSpacing : '0dp'}"
}
]
}
]
}
]
}
]
}
}
},
"mainTemplate": {
"parameters": [
"payload"
],
"item": [
{
"type": "SimpleText",
"backgroundImageSource": "https://m.media-amazon.com/images/G/01/ask/SkillSamples/skies._CB635120026_.jpeg",
"footerHintText": "${payload.flightResponse.display.hintText}",
"foregroundImageLocation": "${payload.flightResponse.display.foregroundImageLocation}",
"foregroundImageSource": "${payload.flightResponse.display.foregroundImageSource}",
"headerAttributionImage": "https://m.media-amazon.com/images/G/01/ask/SkillSamples/friendly_skies._CB1198675309_.png",
"headerTitle": "${payload.flightResponse.display.headerTitle}",
"headerSubtitle": "${payload.flightResponse.display.headerSubtitle}",
"primaryText": "${payload.flightResponse.display.primaryText}",
"secondaryText": "${payload.flightResponse.display.secondaryText}",
"textAlignment": "${payload.flightResponse.display.textAlignment}",
"titleText": "${payload.flightResponse.display.titleText}"
}
]
}
}
Step 3: Use APL template in FlightSearch dialog
First, we need to import the APL to our skill. To do that, you need to add the following code:
Copied to clipboard.
import displays.*
This will import all APL templates under the display folder.
We want to return Flight Details to the APL template we created. To do that, we need to extend our payload with new type:
...
// FlightDetails object we will return from our API
type FlightDetails {
DATE date
NUMBER cost
US_CITY arrivalCity
US_CITY departureCity
TIME time
Airline airline
Display display
}
// Display object we will return from our API
type Display {
String headerTitle
String headerSubtitle
String primaryText
String secondaryText
String textAlignment
String titleText
}
...
As you see above, we used String primitive type for the Display type we created. We also need to import String type into our ACDL file. To do so, we need use the following import statement:
Copied to clipboard.
import com.amazon.alexa.schema.String
Now, we need to make the change in our dialog. In our last response we have:
...
response(
response = MultiModalResponse {
apla = FlightSearchResponsePrompt
},
act = Notify {
success = true,
actionName = FlightFinder
},
payload = FlightDetailsPayload {
flightResponse = flightResult
},
)
We will need to add our APL template to the MultiModalResponse. After adding the APL, your response will look like:
...
response(
response = MultiModalResponse {
apla = FlightSearchResponsePrompt,
apl = FlightSearchResponseVisual
},
act = Notify {
success = true,
actionName = FlightFinder
},
payload = FlightDetailsPayload {
flightResponse = flightResult
},
)
Here is the snapshot of your ACDL file:
Copied to clipboard.
namespace com.flightsearch
import com.amazon.alexa.ask.conversations.*
import com.amazon.ask.types.builtins.AMAZON.US_CITY
import com.amazon.ask.types.builtins.AMAZON.DATE
import com.amazon.ask.types.builtins.AMAZON.NUMBER
import com.amazon.ask.types.builtins.AMAZON.TIME
import slotTypes.Airline
// ACDL primitive types
import com.amazon.alexa.schema.String
import com.amazon.alexa.schema.Nothing
// APLA documents import
import prompts.*
// APL documents import
import displays.*
// Declare a type to represent the information that
// will be extracted from FlightSearchUtterances event
type FlightSearchDetails {
optional DATE date
optional US_CITY arrivalCity
optional US_CITY departureCity
}
// FlightDetails object we will return from our API
type FlightDetails {
DATE date
NUMBER cost
US_CITY arrivalCity
US_CITY departureCity
TIME time
Airline airline
Display display
}
// Display object we will return from our API
type Display {
String headerTitle
String headerSubtitle
String primaryText
String secondaryText
String textAlignment
String titleText
}
// Payload declaration
type FlightDetailsPayload {
FlightDetails flightResponse
}
// API definition
action FlightDetails FlightFinder(DATE date, US_CITY arrivalCity, US_CITY departureCity)
// Variations of how a user might request the flight search
// user can provide any of the optional slots: date, arrivalCity and departureCity
@locale(
Locale.en_US
)
FlightSearchUtterances = utterances<FlightSearchDetails>(
[
"help me to find the cheapest flight",
"I want to find the cheapest flight",
"Find me the cheapest flight",
"Search for a cheap flight",
"Find me a flight",
"I would like to search for a cheap flight",
"I want to travel from {departureCity} to {arrivalCity} on {date}",
"I want to fly from {departureCity} to {arrivalCity} on {date}",
"I want to go from {departureCity} to {arrivalCity} on {date}",
"I want to travel to {arrivalCity} from {departureCity} on {date}",
"I want to fly to {arrivalCity} from {departureCity} on {date}",
"I want to go to {arrivalCity} from {departureCity} on {date}",
"I want to travel from {departureCity} to {arrivalCity} {date}",
"I want to fly from {departureCity} to {arrivalCity} {date}",
"I want to go from {departureCity} to {arrivalCity} {date}",
"I want to travel to {arrivalCity} from {departureCity} {date}",
"I want to fly to {arrivalCity} from {departureCity} {date}",
"I want to go to {arrivalCity} from {departureCity} {date}",
"I want to fly to {arrivalCity} from {departureCity}",
"I want to go to {arrivalCity} from {departureCity}",
"I want to travel to {arrivalCity} from {departureCity}",
"from {departureCity} to {arrivalCity}",
"I want to travel from {departureCity} on {date}",
"I want to fly from {departureCity} on {date}",
"I want to go from {departureCity} on {date}",
"I want to travel from {departureCity} {date}",
"I want to fly from {departureCity} {date}",
"I want to go from {departureCity} {date}",
"I want to go to {arrivalCity} on {date}",
"I want to fly to {arrivalCity} on {date}",
"I would like go to {arrivalCity} on {date}",
"I want to go to {arrivalCity} {date}",
"I want to fly to {arrivalCity} {date}",
"I would like go to {arrivalCity} {date}",
"from {departureCity}",
"to {arrivalCity}",
"I am traveling from {departureCity}",
"I want to fly from {departureCity}",
"I want to travel from {departureCity}",
"I want to go to {arrivalCity}",
"I want to fly to {arrivalCity}",
"I would like go to {arrivalCity}",
"on {date}",
"{date}"
]
)
// Variations of how a user might confirm their request
@locale(
Locale.en_US
)
AffirmUtterances = utterances<Nothing>(
[
"correct",
"OK",
"Yeap",
"Yep",
"Yes"
]
)
// Dialog between user and Alexa
@locale(
Locale.en_US
)
dialog Nothing FlightSearch {
sample {
// Declare the expectation that user will request for Flight search
findCheapFlights = expect (Invoke, FlightSearchUtterances)
// Ensures that all optional slots are provided by the user
// Provides a response expression for each of the arguments
ensure(
RequestArguments {arguments = [FlightFinder.arguments.departureCity], response = RequestDepartureCityPrompt},
RequestArguments {arguments = [FlightFinder.arguments.arrivalCity], response = RequestArrivalCityPrompt},
RequestArguments {arguments = [FlightFinder.arguments.date], response = RequestDatePrompt}
)
// Skill response to the user
// Confirmation response act with payload to pass FlightSearchConfirmPrompt APLA document
response(
response = MultiModalResponse {
apla = FlightSearchConfirmPrompt
},
act = ConfirmAction {
actionName = FlightFinder
},
payload = FlightSearchDetails {
date = findCheapFlights.date,
arrivalCity = findCheapFlights.arrivalCity,
departureCity = findCheapFlights.departureCity
}
)
// Declare the expectation that user confirms the request before calling the API
expect (Affirm, AffirmUtterances)
// Using explicit arguments
flightResult = FlightFinder(
date = findCheapFlights.date,
arrivalCity = findCheapFlights.arrivalCity,
departureCity = findCheapFlights.departureCity
)
// Skill response to the user
// Notify response act indicates that, this response is a notification to the user and FlightFinder API is successfully invoked
// FlightDetailsPayload is passed to the MultiModalResponse which contains both APL and APLA documents
response(
response = MultiModalResponse {
apla = FlightSearchResponsePrompt,
apl = FlightSearchResponseVisual
},
act = Notify {
success = true,
actionName = FlightFinder
},
payload = FlightDetailsPayload {
flightResponse = flightResult
}
)
}
}
Step 4: Update your skill backend to support APL response
We added our response to our dialog, but this response will require some inputs such as header title, primary and secondary text, title text and text alignment. We defined the payload data in the APL file as follows:
...
"mainTemplate": {
"parameters": [
"payload"
],
"item": [
{
"type": "SimpleText",
"backgroundImageSource": "https://m.media-amazon.com/images/G/01/ask/SkillSamples/skies._CB635120026_.jpeg",
"footerHintText": "${payload.flightResponse.display.hintText}",
"foregroundImageLocation": "${payload.flightResponse.display.foregroundImageLocation}",
"foregroundImageSource": "${payload.flightResponse.display.foregroundImageSource}",
"headerAttributionImage": "https://m.media-amazon.com/images/G/01/ask/SkillSamples/friendly_skies._CB1198675309_.png",
"headerTitle": "${payload.flightResponse.display.headerTitle}",
"headerSubtitle": "${payload.flightResponse.display.headerSubtitle}",
"primaryText": "${payload.flightResponse.display.primaryText}",
"secondaryText": "${payload.flightResponse.display.secondaryText}",
"textAlignment": "${payload.flightResponse.display.textAlignment}",
"titleText": "${payload.flightResponse.display.titleText}"
}
]
}
We need to send the payload from our API to the APL template for the data it requires.
- Let's start with expanding our database(flight-data.json) to support airport names, arrival and departure times for the locations. We will show these data on the screen.
Copied to clipboard.
{
"new york": {
"los angeles":
{
"cost": "450",
"time": "12 pm",
"airline": "Hippogriff Air Lines",
"departureAirport": "JFK",
"arrivalAirport": "LAX",
"departureTime": "12:00 PM EST",
"arrivalTime": "14:55 PM PST"
},
"las vegas":
{
"cost": "340",
"time": "2 pm",
"airline": "Lightning Airways",
"departureAirport": "LGA",
"arrivalAirport": "LAS",
"departureTime": "02:00 PM EST",
"arrivalTime": "04:35 PM PST"
},
"seattle":
{
"cost": "470",
"time": "11 am",
"airline": "Harpy Intercontinental",
"departureAirport": "EWR",
"arrivalAirport": "SEA",
"departureTime": "11:00 AM EST",
"arrivalTime": "02:15 PM PST"
},
"boston":
{
"cost": "150",
"time": "7 pm",
"airline": "Lightning Airways",
"departureAirport": "LGA",
"arrivalAirport": "BOS",
"departureTime": "07:00 PM EST",
"arrivalTime": "08:25 PM EST"
},
"chicago":
{
"cost": "250",
"time": "3 pm",
"airline": "Hippogriff Air Lines",
"departureAirport": "EWR",
"arrivalAirport": "ORD",
"departureTime": "02:00 PM EST",
"arrivalTime": "04:35 PM CST"
},
"san francisco":
{
"cost": "520",
"time": "8 am",
"airline": "Griffin Air",
"departureAirport": "JFK",
"arrivalAirport": "SFO",
"departureTime": "08:00 AM EST",
"arrivalTime": "11:20 AM PST"
}
},
"los angeles": {
"new york":
{
"cost": "450",
"time": "12 pm",
"airline": "Hippogriff Air Lines",
"departureAirport": "LAX",
"arrivalAirport": "EWR",
"departureTime": "12:00 PM PST",
"arrivalTime": "08:35 PM EST"
},
"las vegas":
{
"cost": "140",
"time": "2 pm",
"airline": "Lightning Airways",
"departureAirport": "LAX",
"arrivalAirport": "LAS",
"departureTime": "12:00 PM PST",
"arrivalTime": "1:15 PM PST"
},
"seattle":
{
"cost": "180",
"time": "11 am",
"airline": "Harpy Intercontinental",
"departureAirport": "LAX",
"arrivalAirport": "SEA",
"departureTime": "11:00 AM PST",
"arrivalTime": "01:45 PM PST"
},
"boston":
{
"cost": "470",
"time": "4 pm",
"airline": "Lightning Airways",
"departureAirport": "LAX",
"arrivalAirport": "BOS",
"departureTime": "07:00 PM PST",
"arrivalTime": "01:35 AM EST"
},
"chicago":
{
"cost": "250",
"time": "3 pm",
"airline": "Hippogriff Air Lines",
"departureAirport": "LAX",
"arrivalAirport": "ORD",
"departureTime": "03:00 PM PST",
"arrivalTime": "09:10 PM CST"
},
"san Francisco":
{
"cost": "220",
"time": "8 am",
"airline": "Griffin Air",
"departureAirport": "LAX",
"arrivalAirport": "SFO",
"departureTime": "12:00 PM PST",
"arrivalTime": "01:25 PM PST"
}
},
"las vegas": {
"new york":
{
"cost": "450",
"time": "12 pm",
"airline": "Hippogriff Air Lines",
"departureAirport": "LAS",
"arrivalAirport": "EWR",
"departureTime": "12:00 PM PST",
"arrivalTime": "08:00 PM EST"
},
"los angeles":
{
"cost": "150",
"time": "2 pm",
"airline": "Lightning Airways",
"departureAirport": "LAS",
"arrivalAirport": "LAX",
"departureTime": "02:00 PM PST",
"arrivalTime": "03:15 PM PST"
},
"seattle":
{
"cost": "270",
"time": "11 am",
"airline": "Harpy Intercontinental",
"departureAirport": "LAS",
"arrivalAirport": "SEA",
"departureTime": "11:00 AM PST",
"arrivalTime": "01:10 PM PST"
},
"boston":
{
"cost": "350",
"time": "4 pm",
"airline": "Lightning Airways",
"departureAirport": "LAS",
"arrivalAirport": "BOS",
"departureTime": "04:00 PM PST",
"arrivalTime": "00:10 AM EST"
},
"chicago":
{
"cost": "250",
"time": "3 pm",
"airline": "Hippogriff Air Lines",
"departureAirport": "LAS",
"arrivalAirport": "ORD",
"departureTime": "03:00 PM PST",
"arrivalTime": "08:45 PM CST"
},
"san francisco":
{
"cost": "520",
"time": "8 am",
"airline": "Griffin Air",
"departureAirport": "LAS",
"arrivalAirport": "SFO",
"departureTime": "08:00 AM PST",
"arrivalTime": "09:35 PM PST"
}
},
"seattle": {
"new york":
{
"cost": "450",
"time": "12 pm",
"airline": "Hippogriff Air Lines",
"departureAirport": "SEA",
"arrivalAirport": "EWR",
"departureTime": "12:00 PM PST",
"arrivalTime": "08:20 PM EST"
},
"los angeles":
{
"cost": "340",
"time": "2 pm",
"airline": "Lightning Airways",
"departureAirport": "SEA",
"arrivalAirport": "LAX",
"departureTime": "02:00 PM PST",
"arrivalTime": "04:50 PM PST"
},
"las vegas":
{
"cost": "150",
"time": "11 am",
"airline": "Harpy Intercontinental",
"departureAirport": "SEA",
"arrivalAirport": "LAS",
"departureTime": "11:00 AM PST",
"arrivalTime": "01:35 PM PST"
},
"boston":
{
"cost": "270",
"time": "5 pm",
"airline": "Lightning Airways",
"departureAirport": "SEA",
"arrivalAirport": "BOS",
"departureTime": "12:00 PM PST",
"arrivalTime": "01:20 AM EST"
},
"chicago":
{
"cost": "250",
"time": "3 pm",
"airline": "Hippogriff Air Lines",
"departureAirport": "SEA",
"arrivalAirport": "ORD",
"departureTime": "03:00 PM PST",
"arrivalTime": "09:00 PM CST"
},
"san francisco":
{
"cost": "220",
"time": "8 am",
"airline": "Griffin Air",
"departureAirport": "SEA",
"arrivalAirport": "SFO",
"departureTime": "08:00 AM PST",
"arrivalTime": "10:15 AM EST"
}
},
"boston": {
"new york":
{
"cost": "450",
"time": "12 pm",
"airline": "Hippogriff Air Lines",
"departureAirport": "BOS",
"arrivalAirport": "EWR",
"departureTime": "12:00 PM EST",
"arrivalTime": "01:25 PM EST"
},
"los angeles":
{
"cost": "340",
"time": "2 pm",
"airline": "Lightning Airways",
"departureAirport": "BOS",
"arrivalAirport": "LAX",
"departureTime": "02:00 PM EST",
"arrivalTime": "05:10 PM PST"
},
"las vegas":
{
"cost": "470",
"time": "11 am",
"airline": "Harpy Intercontinental",
"departureAirport": "BOS",
"arrivalAirport": "LAS",
"departureTime": "11:00 AM EST",
"arrivalTime": "01:45 PM PST"
},
"seattle":
{
"cost": "150",
"time": "7 pm",
"airline": "Lightning Airways",
"departureAirport": "BOS",
"arrivalAirport": "SEA",
"departureTime": "07:00 PM EST",
"arrivalTime": "10:20 PM PST"
},
"chicago":
{
"cost": "250",
"time": "3 pm",
"airline": "Hippogriff Air Lines",
"departureAirport": "BOS",
"arrivalAirport": "ORD",
"departureTime": "03:00 PM EST",
"arrivalTime": "04:50 PM CST"
},
"san francisco":
{
"cost": "520",
"time": "8 am",
"airline": "Griffin Air",
"departureAirport": "BOS",
"arrivalAirport": "SFO",
"departureTime": "08:00 AM EST",
"arrivalTime": "11:35 AM PST"
}
},
"chicago": {
"new york":
{
"cost": "450",
"time": "12 pm",
"airline": "Lightning Airways",
"departureAirport": "ORD",
"arrivalAirport": "LGA",
"departureTime": "12:00 PM CST",
"arrivalTime": "03:10 PM EST"
},
"los angeles":
{
"cost": "340",
"time": "2 pm",
"airline": "Lightning Airways",
"departureAirport": "ORD",
"arrivalAirport": "LAX",
"departureTime": "02:00 PM CST",
"arrivalTime": "04:20 PM PST"
},
"las vegas":
{
"cost": "470",
"time": "11 am",
"airline": "Harpy Intercontinental",
"departureAirport": "ORD",
"arrivalAirport": "LAS",
"departureTime": "11:00 AM CST",
"arrivalTime": "12:50 PM PST"
},
"seattle":
{
"cost": "150",
"time": "7 pm",
"airline": "Lightning Airways",
"departureAirport": "ORD",
"arrivalAirport": "SEA",
"departureTime": "07:00 PM CST",
"arrivalTime": "09:35 PM PST"
},
"boston":
{
"cost": "250",
"time": "3 pm",
"airline": "Hippogriff Air Lines",
"departureAirport": "ORD",
"arrivalAirport": "BOS",
"departureTime": "03:00 PM CST",
"arrivalTime": "06:20 PM EST"
},
"san francisco":
{
"cost": "520",
"time": "8 am",
"airline": "Griffin Air",
"departureAirport": "ORD",
"arrivalAirport": "SFO",
"departureTime": "08:00 AM CST",
"arrivalTime": "10:35 AM PST"
}
},
"san francisco": {
"new york":
{
"cost": "450",
"time": "12 pm",
"airline": "Lightning Airways",
"departureAirport": "SFO",
"arrivalAirport": "JFK",
"departureTime": "12:00 PM PST",
"arrivalTime": "08:40 PM EST"
},
"los angeles":
{
"cost": "340",
"time": "2 pm",
"airline": "Hippogriff Air Lines",
"departureAirport": "SFO",
"arrivalAirport": "LAX",
"departureTime": "02:00 PM PST",
"arrivalTime": "03:35 PM PST"
},
"las vegas":
{
"cost": "470",
"time": "11 am",
"airline": "Harpy Intercontinental",
"departureAirport": "SFO",
"arrivalAirport": "LAS",
"departureTime": "11:00 AM PST",
"arrivalTime": "12:35 PM PST"
},
"seattle":
{
"cost": "150",
"time": "7 pm",
"airline": "Lightning Airways",
"departureAirport": "SFO",
"arrivalAirport": "SEA",
"departureTime": "07:00 PM PST",
"arrivalTime": "09:10 PM PST"
},
"boston":
{
"cost": "250",
"time": "3 pm",
"airline": "Harpy Intercontinental",
"departureAirport": "SFO",
"arrivalAirport": "BOS",
"departureTime": "03:00 PM PST",
"arrivalTime": "11:50 PM EST"
},
"chicago":
{
"cost": "520",
"time": "8 am",
"airline": "Griffin Air",
"departureAirport": "SFO",
"arrivalAirport": "ORD",
"departureTime": "08:00 AM PST",
"arrivalTime": "14:20 PM CST"
}
}
}
- Now, we can extend our response for the APL template we created. Let's open the index.js file and create an object in flightresponse called "display" and set the parameters.
...
const headerTitle = "Flight Search";
const textAlignment = "start";
let response = "";
let primaryText = "";
let secondaryText = "";
let titleText = "";
if (flightData.cost == "") {
primaryText = `Sorry, I couldn't find any flights from ${util.capitalizeFirstLetter(departure)} to ${util.capitalizeFirstLetter(arrival)}.`;
}
else {
primaryText = flightData.airline;
secondaryText = `<b>Passengers: </b>1 Adult<br><b>Seat: </b> Main Cabin<br><b>Departure Time: </b> ${flightData.departureTime} <br><b>Arrival Time: </b> ${flightData.arrivalTime} <br> <b>Total Cost: </b>$${flightData.cost}`;
titleText = `${flightData.departureAirport} to ${flightData.arrivalAirport}`;
}
...
response = {
arrivalCity: arrival,
departureCity: departure,
date: date,
time: flightData.time,
cost: flightData.cost,
airline: flightData.airline,
display: {
headerTitle: headerTitle,
headerSubtitle: "",
primaryText: primaryText,
secondaryText: secondaryText,
textAlignment: textAlignment,
titleText: titleText
}
};
...
Our API handler will look like as follows:
Copied to clipboard.
/* *
* FlightFinderHandler searches for the flight for given departure and arrival city and returns the response to the skill.
* This handler will be triggered when three slots are collected: arrivalCity, departureCity and date
* Response contains the json which maps FlightDetails type in ACDL and display for APL template
* */
const FlightFinderHandler = {
canHandle(handlerInput) {
return util.isApiRequest(handlerInput, 'com.flightsearch.FlightFinder'); //this needs to be your namespace and api name
},
handle(handlerInput) {
console.log(`flight finder handler: ~~~ ${JSON.stringify(handlerInput)}`);
const departure = util.getApiSlotBestValue(handlerInput, "departureCity"); //name of the U.S. city given in the api for departureCity slot (API definition)
const arrival = util.getApiSlotBestValue(handlerInput, "arrivalCity"); //name of the U.S. city given in the api for arrivalCity slot (API definition)
const date = util.getApiSlotBestValue(handlerInput, "date"); //date in the api for date slot (API definition)
const flightData = flightSearch.getFlightData(departure, arrival);
const headerTitle = "Flight Search";
const textAlignment = "start";
let response = "";
let primaryText = "";
let secondaryText = "";
let titleText = "";
// requested flight is not found, set only primary text for the visual display
if (flightData.cost == "") {
primaryText = `Sorry, I couldn't find any flights from ${util.capitalizeFirstLetter(departure)} to ${util.capitalizeFirstLetter(arrival)}.`;
}
else {
primaryText = flightData.airline;
secondaryText = `<b>Passengers: </b>1 Adult<br><b>Seat: </b> Main Cabin<br><b>Departure Time: </b> ${flightData.departureTime} <br><b>Arrival Time: </b> ${flightData.arrivalTime} <br> <b>Total Cost: </b>$${flightData.cost}`;
titleText = `${flightData.departureAirport} to ${flightData.arrivalAirport}`;
}
// response maps to FlightDetails type in ACDL
// arrivalCity, departureCity, date, time, cost and airline
// display is used in APL template
response = {
arrivalCity: arrival,
departureCity: departure,
date: date,
time: flightData.time,
cost: flightData.cost,
airline: flightData.airline,
display: {
headerTitle: headerTitle,
headerSubtitle: "",
primaryText: primaryText,
secondaryText: secondaryText,
textAlignment: textAlignment,
titleText: titleText
}
};
console.log("response: ", response);
return handlerInput.responseBuilder
.withApiResponse(response)
.withShouldEndSession(false) // Setting this to false keeps the mic on after Alexa responds
.getResponse();
}
};
Here is the snapshot of the index.js:
Copied to clipboard.
/* *
* This sample demonstrates handling intents from an Alexa skill using the Alexa Skills Kit SDK (v2).
* Please visit https://alexa.design/cookbook for additional examples on implementing slots, dialog management,
* session persistence, api calls, and more.
* */
const Alexa = require('ask-sdk-core');
const util = require('./util');
const flightSearch = require('./flightSearch')
/* *
* FlightFinderHandler searches for the flight for given departure and arrival city and returns the response to the skill.
* This handler will be triggered when three slots are collected: arrivalCity, departureCity and date
* Response contains the json which maps FlightDetails type in ACDL and display for APL template
* */
const FlightFinderHandler = {
canHandle(handlerInput) {
return util.isApiRequest(handlerInput, 'com.flightsearch.FlightFinder'); //this needs to be your namespace and api name
},
handle(handlerInput) {
console.log(`flight finder handler: ~~~ ${JSON.stringify(handlerInput)}`);
const departure = util.getApiSlotBestValue(handlerInput, "departureCity"); //name of the U.S. city given in the api for departureCity slot (API definition)
const arrival = util.getApiSlotBestValue(handlerInput, "arrivalCity"); //name of the U.S. city given in the api for arrivalCity slot (API definition)
const date = util.getApiSlotBestValue(handlerInput, "date"); //date in the api for date slot (API definition)
const flightData = flightSearch.getFlightData(departure, arrival);
const headerTitle = "Flight Search";
const textAlignment = "start";
let response = "";
let primaryText = "";
let secondaryText = "";
let titleText = "";
// requested flight is not found, set only primary text for the visual display
if (flightData.cost == "") {
primaryText = `Sorry, I couldn't find any flights from ${util.capitalizeFirstLetter(departure)} to ${util.capitalizeFirstLetter(arrival)}.`;
}
else {
primaryText = flightData.airline;
secondaryText = `<b>Passengers: </b>1 Adult<br><b>Seat: </b> Main Cabin<br><b>Departure Time: </b> ${flightData.departureTime} <br><b>Arrival Time: </b> ${flightData.arrivalTime} <br> <b>Total Cost: </b>$${flightData.cost}`;
titleText = `${flightData.departureAirport} to ${flightData.arrivalAirport}`;
}
// response maps to FlightDetails type in ACDL
// arrivalCity, departureCity, date, time, cost and airline
// display is used in APL template
response = {
arrivalCity: arrival,
departureCity: departure,
date: date,
time: flightData.time,
cost: flightData.cost,
airline: flightData.airline,
display: {
headerTitle: headerTitle,
headerSubtitle: "",
primaryText: primaryText,
secondaryText: secondaryText,
textAlignment: textAlignment,
titleText: titleText
}
};
console.log("response: ", response);
return handlerInput.responseBuilder
.withApiResponse(response)
.withShouldEndSession(false) // Setting this to false keeps the mic on after Alexa responds
.getResponse();
}
};
/* *
* SessionEndedRequest notifies that a session was ended. This handler will be triggered when a currently open
* session is closed for one of the following reasons: 1) The user says "exit" or "quit". 2) The user does not
* respond or says something that does not match an intent defined in your voice model. 3) An error occurs
* */
const SessionEndedRequestHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'SessionEndedRequest';
},
handle(handlerInput) {
console.log(`~~~~ Session ended: ${JSON.stringify(handlerInput.requestEnvelope)}`);
// Any cleanup logic goes here.
return handlerInput.responseBuilder.getResponse(); // notice we send an empty response
}
};
/**
* Generic error handling to capture any syntax or routing errors. If you receive an error
* stating the request handler chain is not found, you have not implemented a handler for
* the intent being invoked or included it in the skill builder below
* */
const ErrorHandler = {
canHandle() {
return true;
},
handle(handlerInput, error) {
const speakOutput = 'Sorry, I had trouble doing what you asked. Please try again.';
console.log(`~~~~ Error handled: ${JSON.stringify(error)}`);
return handlerInput.responseBuilder
.speak(speakOutput)
.reprompt(speakOutput)
.getResponse();
}
};
/**
* This handler acts as the entry point for your skill, routing all request and response
* payloads to the handlers above. Make sure any new handlers or interceptors you've
* defined are included below. The order matters - they're processed top to bottom
* */
exports.handler = Alexa.SkillBuilders.custom()
.addRequestHandlers(
FlightFinderHandler,
SessionEndedRequestHandler
)
.addErrorHandlers(
ErrorHandler)
.withCustomUserAgent('sample/hello-world/v1.2')
.lambda();
Step 5: Deploy your changes and test
We are ready to deploy our changes!
- Open the terminal and navigate to the main directory of the Flight Search skill.
- Compile the code.
Copied to clipboard.
askx compile
- Deploy the code.
Copied to clipboard.
askx deploy
- Once the deployment is completed, you can start testing.
(Optional) How to use APL templates on "welcome" and "out-of-domain" responses
We added the first multimodal response to our skill and you were able to test it. This section is optional if you want to learn more about how to add multimodal responses to the skill level responses such as welcome prompt.
-
We need to create APL template for "welcome" and "out-of-domain" responses. To do that, let's create two new folders under skill-packages/response/display called "WelcomeResponseVisual" and "OutOfDomainResponseVisual"
-
Now, we should create "document.json" file under each folder we created.
- We will create a "welcome" prompt as below:
![Welcome prompt APL](https://m.media-amazon.com/images/G/01/mobile-apps/dex/ask-conversations/welcome_prompt._TTH_.png)
WelcomeResponseVisual/document.json APL code will be as follows:
Copied to clipboard.
{
"type": "APL",
"version": "1.8",
"license": "Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.\nSPDX-License-Identifier: LicenseRef-.amazon.com.-AmznSL-1.0\nLicensed under the Amazon Software License http://aws.amazon.com/asl/",
"theme": "dark",
"import": [
{
"name": "alexa-layouts",
"version": "1.5.0"
}
],
"resources": [
{
"description": "Public resource definitions Simple Text",
"colors": {
"colorText": "@colorText"
},
"dimensions": {
"headerHeight": "${@headerAttributionIconMaxHeight + (2 * @spacingLarge)}",
"simpleTextScrollHeightWithFooter": "${viewport.height - @headerHeight - @footerPaddingTop - @footerPaddingBottom}",
"simpleTextScrollHeightNoFooter": "100%",
"simpleTextImageHorizontalSpacing": "@spacingLarge",
"simpleTextImageVerticalSpacing": "@spacingMedium",
"simpleTextPaddingBottom": "@spacingLarge"
}
},
{
"when": "${@viewportProfileCategory == @hubRound}",
"dimensions": {
"simpleTextPaddingBottom": "@spacing3XLarge"
}
},
{
"when": "${@viewportProfile == @hubLandscapeSmall}",
"dimensions": {
"simpleTextScrollHeightWithFooter": "100%"
}
},
{
"when": "${viewport.theme == 'light'}",
"colors": {
"colorText": "@colorTextReversed"
}
}
],
"layouts": {
"SimpleText": {
"parameters": [
{
"name": "backgroundImageSource",
"description": "URL for the background image source.",
"type": "string"
},
{
"name": "footerHintText",
"description": "Hint text to display in the footer.",
"type": "string"
},
{
"name": "foregroundImageLocation",
"description": "Location of the forground image. Options are top, bottom, left, and right. Default is top.",
"type": "string",
"default": "top"
},
{
"name": "foregroundImageSource",
"description": "URL for the foreground image source. If blank, the template will be full text layout.",
"type": "string"
},
{
"name": "headerAttributionImage",
"description": "URL for attribution image or logo source (PNG/vector).",
"type": "string"
},
{
"name": "headerTitle",
"description": "Title text to render in the header.",
"type": "string"
},
{
"name": "headerSubtitle",
"description": "Subtitle Text to render in the header.",
"type": "string"
},
{
"name": "primaryText",
"description": "Text for to render below the title text in the body.",
"type": "string"
},
{
"name": "titleText",
"description": "Title text to render in the body.",
"type": "string"
},
{
"name": "textAlignment",
"description": "Alignment of text content. Options are start, and center. Default is start.",
"type": "string",
"default": "start"
},
{
"name": "feedbackRating",
"description": "Star text to render under the header title",
"type": "number"
}
],
"item": {
"type": "Container",
"height": "100vh",
"width": "100vw",
"bind": [
{
"name": "imageCenterAlign",
"type": "boolean",
"value": "${@viewportProfileCategory == @hubRound || foregroundImageLocation == 'top' || foregroundImageLocation == 'bottom'}"
},
{
"name": "hasFooter",
"type": "boolean",
"value": "${@viewportProfileCategory != @hubRound && @viewportProfile != @hubLandscapeSmall && footerHintText}"
}
],
"items": [
{
"type": "AlexaBackground",
"backgroundColor": "${backgroundColor}",
"backgroundImageSource": "${backgroundImageSource}",
"colorOverlay": true
},
{
"when": "${@viewportProfileCategory != @hubRound}",
"type": "AlexaHeader",
"layoutDirection": "${environment.layoutDirection}",
"headerAttributionImage": "${headerAttributionImage}",
"headerTitle": "${headerTitle}",
"headerSubtitle": "${headerSubtitle}",
"headerAttributionPrimacy": true,
"width": "100%"
},
{
"description": "Footer Hint Text - not displaying on small hubs",
"when": "${@viewportProfileCategory != @hubRound && @viewportProfile != @hubLandscapeSmall && footerHintText}",
"type": "AlexaFooter",
"hintText": "${footerHintText}",
"theme": "${viewport.theme}",
"width": "100%",
"position": "absolute",
"bottom": "0"
},
{
"type": "ScrollView",
"height": "${hasFooter ? @simpleTextScrollHeightWithFooter : @simpleTextScrollHeightNoFooter}",
"width": "100vw",
"shrink": 1,
"items": [
{
"justifyContent": "center",
"alignItems": "center",
"items": [
{
"when": "${@viewportProfileCategory == @hubRound}",
"type": "AlexaHeader",
"layoutDirection": "${environment.layoutDirection}",
"headerAttributionImage": "${headerAttributionImage}",
"headerAttributionPrimacy": true,
"width": "100%"
},
{
"description": "Image and text content block",
"type": "Container",
"width": "100%",
"alignItems": "${imageCenterAlign ? 'center' : 'start'}",
"direction": "${foregroundImageLocation == 'left' ? 'row' : (foregroundImageLocation == 'right' ? 'rowReverse' : (foregroundImageLocation == 'bottom' ? 'columnReverse' : 'column'))}",
"shrink": 1,
"items": [
{
"shrink": 1,
"alignItems": "${imageCenterAlign || textAlignment == 'center' ? 'center' : 'start'}",
"items": [
{
"description": "Title Text",
"when": "${titleText}",
"type": "Text",
"width": "100%",
"style": "textStyleDisplay5",
"text": "${titleText}",
"lineHeight": 1.5,
"textAlign": "${@viewportProfileCategory == @hubRound ? 'center' : textAlignment}"
},
{
"description": "Primary Text",
"when": "${primaryText}",
"type": "Text",
"spacing": "@spacing2XSmall",
"style": "textStyleBody",
"fontWeight": "@fontWeightLight",
"text": "${primaryText}",
"textAlign": "${@viewportProfileCategory == @hubRound ? 'center' : textAlignment}"
}
],
"description": "Primary Text and Title Text block",
"when": "${primaryText || titleText}",
"type": "Container",
"width": "100%",
"paddingBottom": "${foregroundImageLocation == 'bottom' ? @simpleTextImageVerticalSpacing : '0dp'}",
"paddingStart": "${foregroundImageLocation == 'left' && foregroundImageSource ? @simpleTextImageHorizontalSpacing : '0dp'}",
"paddingEnd": "${foregroundImageLocation == 'right' && foregroundImageSource ? @simpleTextImageHorizontalSpacing : '0dp'}"
}
]
}
],
"layoutDirection": "inherit",
"type": "Container",
"width": "100%",
"paddingBottom": "@simpleTextPaddingBottom"
}
]
}
]
}
}
},
"mainTemplate": {
"parameters": [
"payload"
],
"item": [
{
"type": "SimpleText",
"backgroundImageSource": "https://m.media-amazon.com/images/G/01/ask/SkillSamples/skies._CB635120026_.jpeg",
"footerHintText": "Try \"I want to travel to Seattle.\"",
"foregroundImageLocation": "${payload.simpleTextTemplateData.properties.foregroundImageLocation}",
"foregroundImageSource": "${payload.simpleTextTemplateData.properties.foregroundImageSource}",
"headerAttributionImage": "https://m.media-amazon.com/images/G/01/ask/SkillSamples/friendly_skies._CB1198675309_.png",
"headerTitle": "Flight Search",
"headerSubtitle": "${payload.simpleTextTemplateData.properties.headerSubtitle}",
"primaryText": "Currently supports searches for the following cities: <br>Seattle<br>New York<br>Los Angeles<br>San Francisco<br>Las Vegas<br>Chicago<br>Boston",
"textAlignment": "center",
"titleText": "Welcome to the Flight Search"
}
]
}
}
- "Our "out-of-domain" visual response will look as indicated below:
![Out-of-domain prompt APL](https://m.media-amazon.com/images/G/01/mobile-apps/dex/ask-conversations/out_of_domain_prompt._TTH_.png)
APL code for OutOfDomainResponseVisual/document.json will be as below:
Copied to clipboard.
{
"type": "APL",
"version": "1.8",
"license": "Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.\nSPDX-License-Identifier: LicenseRef-.amazon.com.-AmznSL-1.0\nLicensed under the Amazon Software License http://aws.amazon.com/asl/",
"theme": "dark",
"import": [
{
"name": "alexa-layouts",
"version": "1.5.0"
}
],
"resources": [
{
"description": "Public resource definitions Simple Text",
"colors": {
"colorText": "@colorText"
},
"dimensions": {
"headerHeight": "${@headerAttributionIconMaxHeight + (2 * @spacingLarge)}",
"simpleTextScrollHeightWithFooter": "${viewport.height - @headerHeight - @footerPaddingTop - @footerPaddingBottom}",
"simpleTextScrollHeightNoFooter": "100%",
"simpleTextImageHorizontalSpacing": "@spacingLarge",
"simpleTextImageVerticalSpacing": "@spacingMedium",
"simpleTextPaddingBottom": "@spacingLarge"
}
},
{
"when": "${@viewportProfileCategory == @hubRound}",
"dimensions": {
"simpleTextPaddingBottom": "@spacing3XLarge"
}
},
{
"when": "${@viewportProfile == @hubLandscapeSmall}",
"dimensions": {
"simpleTextScrollHeightWithFooter": "100%"
}
},
{
"when": "${viewport.theme == 'light'}",
"colors": {
"colorText": "@colorTextReversed"
}
}
],
"layouts": {
"SimpleText": {
"parameters": [
{
"name": "backgroundImageSource",
"description": "URL for the background image source.",
"type": "string"
},
{
"name": "footerHintText",
"description": "Hint text to display in the footer.",
"type": "string"
},
{
"name": "foregroundImageLocation",
"description": "Location of the forground image. Options are top, bottom, left, and right. Default is top.",
"type": "string",
"default": "top"
},
{
"name": "foregroundImageSource",
"description": "URL for the foreground image source. If blank, the template will be full text layout.",
"type": "string"
},
{
"name": "headerAttributionImage",
"description": "URL for attribution image or logo source (PNG/vector).",
"type": "string"
},
{
"name": "headerTitle",
"description": "Title text to render in the header.",
"type": "string"
},
{
"name": "headerSubtitle",
"description": "Subtitle Text to render in the header.",
"type": "string"
},
{
"name": "primaryText",
"description": "Text for to render below the title text in the body.",
"type": "string"
},
{
"name": "titleText",
"description": "Title text to render in the body.",
"type": "string"
},
{
"name": "textAlignment",
"description": "Alignment of text content. Options are start, and center. Default is start.",
"type": "string",
"default": "start"
},
{
"name": "feedbackRating",
"description": "Star text to render under the header title",
"type": "number"
}
],
"item": {
"type": "Container",
"height": "100vh",
"width": "100vw",
"bind": [
{
"name": "imageCenterAlign",
"type": "boolean",
"value": "${@viewportProfileCategory == @hubRound || foregroundImageLocation == 'top' || foregroundImageLocation == 'bottom'}"
},
{
"name": "hasFooter",
"type": "boolean",
"value": "${@viewportProfileCategory != @hubRound && @viewportProfile != @hubLandscapeSmall && footerHintText}"
}
],
"items": [
{
"type": "AlexaBackground",
"backgroundColor": "${backgroundColor}",
"backgroundImageSource": "${backgroundImageSource}",
"colorOverlay": true
},
{
"when": "${@viewportProfileCategory != @hubRound}",
"type": "AlexaHeader",
"layoutDirection": "${environment.layoutDirection}",
"headerAttributionImage": "${headerAttributionImage}",
"headerTitle": "${headerTitle}",
"headerSubtitle": "${headerSubtitle}",
"headerAttributionPrimacy": true,
"width": "100%"
},
{
"description": "Footer Hint Text - not displaying on small hubs",
"when": "${@viewportProfileCategory != @hubRound && @viewportProfile != @hubLandscapeSmall && footerHintText}",
"type": "AlexaFooter",
"hintText": "${footerHintText}",
"theme": "${viewport.theme}",
"width": "100%",
"position": "absolute",
"bottom": "0"
},
{
"type": "ScrollView",
"height": "${hasFooter ? @simpleTextScrollHeightWithFooter : @simpleTextScrollHeightNoFooter}",
"width": "100vw",
"shrink": 1,
"items": [
{
"justifyContent": "center",
"alignItems": "center",
"items": [
{
"when": "${@viewportProfileCategory == @hubRound}",
"type": "AlexaHeader",
"layoutDirection": "${environment.layoutDirection}",
"headerAttributionImage": "${headerAttributionImage}",
"headerAttributionPrimacy": true,
"width": "100%"
},
{
"description": "Image and text content block",
"type": "Container",
"width": "100%",
"alignItems": "${imageCenterAlign ? 'center' : 'start'}",
"direction": "${foregroundImageLocation == 'left' ? 'row' : (foregroundImageLocation == 'right' ? 'rowReverse' : (foregroundImageLocation == 'bottom' ? 'columnReverse' : 'column'))}",
"shrink": 1,
"items": [
{
"shrink": 1,
"alignItems": "${imageCenterAlign || textAlignment == 'center' ? 'center' : 'start'}",
"items": [
{
"description": "Primary Text",
"when": "${primaryText}",
"type": "Text",
"spacing": "@spacing2XSmall",
"style": "textStyleBody2",
"fontWeight": "@fontWeightLight",
"text": "${primaryText}",
"textAlign": "${@viewportProfileCategory == @hubRound ? 'center' : textAlignment}"
}
],
"description": "Primary Text and Title Text block",
"when": "${primaryText || titleText}",
"type": "Container",
"width": "100%",
"paddingBottom": "${foregroundImageLocation == 'bottom' ? @simpleTextImageVerticalSpacing : '0dp'}",
"paddingStart": "${foregroundImageLocation == 'left' && foregroundImageSource ? @simpleTextImageHorizontalSpacing : '0dp'}",
"paddingEnd": "${foregroundImageLocation == 'right' && foregroundImageSource ? @simpleTextImageHorizontalSpacing : '0dp'}"
}
]
}
],
"layoutDirection": "inherit",
"type": "Container",
"width": "100%",
"paddingTop": "${2*@spacingLarge}",
"paddingBottom": "@simpleTextPaddingBottom"
}
]
}
]
}
}
},
"mainTemplate": {
"parameters": [
"payload"
],
"item": [
{
"type": "SimpleText",
"backgroundImageSource": "https://m.media-amazon.com/images/G/01/ask/SkillSamples/skies._CB635120026_.jpeg",
"footerHintText": "Try \"I want to travel to Seattle\"",
"foregroundImageLocation": "${payload.simpleTextTemplateData.properties.foregroundImageLocation}",
"foregroundImageSource": "${payload.simpleTextTemplateData.properties.foregroundImageSource}",
"headerAttributionImage": "https://m.media-amazon.com/images/G/01/ask/SkillSamples/friendly_skies._CB1198675309_.png",
"headerTitle": "Flight Search",
"headerSubtitle": "${payload.simpleTextTemplateData.properties.headerSubtitle}",
"primaryText": "Sorry I don't understand.",
"textAlignment": "center"
}
]
}
}
- We need to use skill action to set the skill-wide assets. You can learn more about of skill actions here.
...
// Multimodal response for Welcome
multiModalWelcome = MultiModalResponse {
apl = WelcomeResponseVisual,
apla = AlexaConversationsWelcome
}
// Multimodal response for Out of domain
multimodalOutOfDomain = MultiModalResponse {
apl = OutOfDomainResponseVisual,
apla = AlexaConversationsOutOfDomain
}
// Skill action to set the skill-wide assets
mySkill = skill(
locales = [Locale.en_US],
dialogs = [FlightSearch],
skillLevelResponses = SkillLevelResponses
{
welcome = multiModalWelcome,
out_of_domain = multimodalOutOfDomain,
bye = AlexaConversationsBye,
reqmore = AlexaConversationsRequestMore,
provide_help = AlexaConversationsProvideHelp
}
)
...
Your ACDL file will be as following snapshot:
Copied to clipboard.
namespace com.flightsearch
import com.amazon.alexa.ask.conversations.*
import com.amazon.ask.types.builtins.AMAZON.US_CITY
import com.amazon.ask.types.builtins.AMAZON.DATE
import com.amazon.ask.types.builtins.AMAZON.NUMBER
import com.amazon.ask.types.builtins.AMAZON.TIME
import slotTypes.Airline
// ACDL primitive types
import com.amazon.alexa.schema.String
import com.amazon.alexa.schema.Nothing
// APLA documents import
import prompts.*
// APL documents import
import displays.*
// Multimodal response for Welcome
multiModalWelcome = MultiModalResponse {
apl = WelcomeResponseVisual,
apla = AlexaConversationsWelcome
}
// Multimodal response for Out of domain
multimodalOutOfDomain = MultiModalResponse {
apl = OutOfDomainResponseVisual,
apla = AlexaConversationsOutOfDomain
}
// Skill action to set the skill-wide assets
mySkill = skill(
locales = [Locale.en_US],
dialogs = [FlightSearch],
skillLevelResponses = SkillLevelResponses
{
welcome = multiModalWelcome,
out_of_domain = multimodalOutOfDomain,
bye = AlexaConversationsBye,
reqmore = AlexaConversationsRequestMore,
provide_help = AlexaConversationsProvideHelp
}
)
// Declare a type to represent the information that
// will be extracted from FlightSearchUtterances event
type FlightSearchDetails {
optional DATE date
optional US_CITY arrivalCity
optional US_CITY departureCity
}
// FlightDetails object we will return from our API
type FlightDetails {
DATE date
NUMBER cost
US_CITY arrivalCity
US_CITY departureCity
TIME time
Airline airline
Display display
}
// Display object we will return from our API
type Display {
String headerTitle
String headerSubtitle
String primaryText
String secondaryText
String textAlignment
String titleText
}
// Payload declaration
type FlightDetailsPayload {
FlightDetails flightResponse
}
// API definition
action FlightDetails FlightFinder(DATE date, US_CITY arrivalCity, US_CITY departureCity)
// Variations of how a user might request the flight search
// user can provide any of the optional slots: date, arrivalCity and departureCity
@locale(
Locale.en_US
)
FlightSearchUtterances = utterances<FlightSearchDetails>(
[
"help me to find the cheapest flight",
"I want to find the cheapest flight",
"Find me the cheapest flight",
"Search for a cheap flight",
"Find me a flight",
"I would like to search for a cheap flight",
"I want to travel from {departureCity} to {arrivalCity} on {date}",
"I want to fly from {departureCity} to {arrivalCity} on {date}",
"I want to go from {departureCity} to {arrivalCity} on {date}",
"I want to travel to {arrivalCity} from {departureCity} on {date}",
"I want to fly to {arrivalCity} from {departureCity} on {date}",
"I want to go to {arrivalCity} from {departureCity} on {date}",
"I want to travel from {departureCity} to {arrivalCity} {date}",
"I want to fly from {departureCity} to {arrivalCity} {date}",
"I want to go from {departureCity} to {arrivalCity} {date}",
"I want to travel to {arrivalCity} from {departureCity} {date}",
"I want to fly to {arrivalCity} from {departureCity} {date}",
"I want to go to {arrivalCity} from {departureCity} {date}",
"I want to fly to {arrivalCity} from {departureCity}",
"I want to go to {arrivalCity} from {departureCity}",
"I want to travel to {arrivalCity} from {departureCity}",
"from {departureCity} to {arrivalCity}",
"I want to travel from {departureCity} on {date}",
"I want to fly from {departureCity} on {date}",
"I want to go from {departureCity} on {date}",
"I want to travel from {departureCity} {date}",
"I want to fly from {departureCity} {date}",
"I want to go from {departureCity} {date}",
"I want to go to {arrivalCity} on {date}",
"I want to fly to {arrivalCity} on {date}",
"I would like go to {arrivalCity} on {date}",
"I want to go to {arrivalCity} {date}",
"I want to fly to {arrivalCity} {date}",
"I would like go to {arrivalCity} {date}",
"from {departureCity}",
"to {arrivalCity}",
"I am traveling from {departureCity}",
"I want to fly from {departureCity}",
"I want to travel from {departureCity}",
"I want to go to {arrivalCity}",
"I want to fly to {arrivalCity}",
"I would like go to {arrivalCity}",
"on {date}",
"{date}"
]
)
// Variations of how a user might confirm their request
@locale(
Locale.en_US
)
AffirmUtterances = utterances<Nothing>(
[
"correct",
"OK",
"Yeap",
"Yep",
"Yes"
]
)
// Dialog between user and Alexa
@locale(
Locale.en_US
)
dialog Nothing FlightSearch {
sample {
// Declare the expectation that user will request for Flight search
findCheapFlights = expect (Invoke, FlightSearchUtterances)
// Ensures that all optional slots are provided by the user
// Provides a response expression for each of the arguments
ensure(
RequestArguments {arguments = [FlightFinder.arguments.departureCity], response = RequestDepartureCityPrompt},
RequestArguments {arguments = [FlightFinder.arguments.arrivalCity], response = RequestArrivalCityPrompt},
RequestArguments {arguments = [FlightFinder.arguments.date], response = RequestDatePrompt}
)
// Skill response to the user
// Confirmation response act with payload to pass FlightSearchConfirmPrompt APLA document
response(
response = MultiModalResponse {
apla = FlightSearchConfirmPrompt
},
act = ConfirmAction {
actionName = FlightFinder
},
payload = FlightSearchDetails {
date = findCheapFlights.date,
arrivalCity = findCheapFlights.arrivalCity,
departureCity = findCheapFlights.departureCity
}
)
// Declare the expectation that user confirms the request before calling the API
expect (Affirm, AffirmUtterances)
// Using explicit arguments
flightResult = FlightFinder(
date = findCheapFlights.date,
arrivalCity = findCheapFlights.arrivalCity,
departureCity = findCheapFlights.departureCity
)
// Skill response to the user
// Notify response act indicates that, this response is a notification to the user and FlightFinder API is successfully invoked
// FlightDetailsPayload is passed to the MultiModalResponse which contains both APL and APLA documents
response(
response = MultiModalResponse {
apla = FlightSearchResponsePrompt,
apl = FlightSearchResponseVisual
},
act = Notify {
success = true,
actionName = FlightFinder
},
payload = FlightDetailsPayload {
flightResponse = flightResult
}
)
}
}
- Compile the code.
Copied to clipboard.
askx compile
- Deploy the code.
Copied to clipboard.
askx deploy
- Once the deployment is completed, you can start testing.
Wrap-up
Congratulations! You are now equipped to develop an Alexa Conversations skill by using ACDL and have learned how your skill can support multimodality.
Code
If your skill isn't working or you're getting some kind of syntax error, download the code from the github repository.