node AWS Lambda + twitter API: data is not defined - node.js

I'm having fun with the Alexa API, so I downloaded a Hello World example from here
https://developer.amazon.com/appsandservices/solutions/alexa/alexa-skills-kit/getting-started-guide
I managed to made some minor changes and have Alexa say other things.
But now I want to have a real world example working, so I tried to get the latest tweet for user.
so I coded a twitter function and it works, I see the tweet on my console.
Besides, the downloaded example works just fine too.
But now, when I try to combine them by adding my twitter function into the Alexa example, it throws the following error when trying to print the value (if I don't print it, it doesn't break):
{"errorMessage": "Exception: ReferenceError: data is not defined"}
here is the code but the modified function is getWelcomeResponse()
// Route the incoming request based on type (LaunchRequest, IntentRequest,
// etc.) The JSON body of the request is provided in the event parameter.
exports.handler = function (event, context) {
try {
console.log("event.session.application.applicationId=" + event.session.application.applicationId);
/**
* Uncomment this if statement and populate with your skill's application ID to
* prevent someone else from configuring a skill that sends requests to this function.
*/
/*
if (event.session.application.applicationId !== "amzn1.echo-sdk-ams.app.[unique-value-here]") {
context.fail("Invalid Application ID");
}
*/
if (event.session.new) {
onSessionStarted({requestId: event.request.requestId}, event.session);
}
if (event.request.type === "LaunchRequest") {
onLaunch(event.request,
event.session,
function callback(sessionAttributes, speechletResponse) {
context.succeed(buildResponse(sessionAttributes, speechletResponse));
});
} else if (event.request.type === "IntentRequest") {
onIntent(event.request,
event.session,
function callback(sessionAttributes, speechletResponse) {
context.succeed(buildResponse(sessionAttributes, speechletResponse));
});
} else if (event.request.type === "SessionEndedRequest") {
onSessionEnded(event.request, event.session);
context.succeed();
}
} catch (e) {
context.fail("Exception: " + e);
}
};
/**
* Called when the session starts.
*/
function onSessionStarted(sessionStartedRequest, session) {
console.log("onSessionStarted requestId=" + sessionStartedRequest.requestId
+ ", sessionId=" + session.sessionId);
}
/**
* Called when the user launches the skill without specifying what they want.
*/
function onLaunch(launchRequest, session, callback) {
console.log("onLaunch requestId=" + launchRequest.requestId
+ ", sessionId=" + session.sessionId);
// Dispatch to your skill's launch.
getWelcomeResponse(callback);
}
/**
* Called when the user specifies an intent for this skill.
*/
function onIntent(intentRequest, session, callback) {
console.log("onIntent requestId=" + intentRequest.requestId
+ ", sessionId=" + session.sessionId);
var intent = intentRequest.intent,
intentName = intentRequest.intent.name;
// Dispatch to your skill's intent handlers
if ("MyColorIsIntent" === intentName) {
setColorInSession(intent, session, callback);
} else if ("WhatsMyColorIntent" === intentName) {
getColorFromSession(intent, session, callback);
} else if ("HelpIntent" === intentName) {
getWelcomeResponse(callback);
} else {
throw "Invalid intent";
}
}
/**
* Called when the user ends the session.
* Is not called when the skill returns shouldEndSession=true.
*/
function onSessionEnded(sessionEndedRequest, session) {
console.log("onSessionEnded requestId=" + sessionEndedRequest.requestId
+ ", sessionId=" + session.sessionId);
// Add cleanup logic here
}
// --------------- Functions that control the skill's behavior -----------------------
function getWelcomeResponse(callback) {
var twit = require('twitter'),
twitter = new twit({
consumer_key:'***',
consumer_secret:'***',
access_token_key:'***',
access_token_secret:'***'
});
//var count = 0;
var util = require('util');
params = {
screen_name: 'kilinkis', // the user id passed in as part of the route
count: 1 // how many tweets to return
};
// request data
twitter.get('https://api.twitter.com/1.1/statuses/user_timeline.json', params, function (data) {
console.log(util.inspect(data[0].text));
});
// If we wanted to initialize the session to have some attributes we could add those here.
var sessionAttributes = {};
var cardTitle = "Welcome";
/*var speechOutput = "Welcome to the Alexa Skills Kit sample, "
+ "Please tell me your favorite color by saying, "
+ "my favorite color is red";*/
//var speechOutput=util.inspect(data[0].text);
var speechOutput=data[0].text;
// If the user either does not reply to the welcome message or says something that is not
// understood, they will be prompted again with this text.
var repromptText = "Please tell me your favorite color by saying, "
+ "my favorite color is red";
var shouldEndSession = true;
callback(sessionAttributes,
buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
}
/**
* Sets the color in the session and prepares the speech to reply to the user.
*/
function setColorInSession(intent, session, callback) {
var cardTitle = intent.name;
var favoriteColorSlot = intent.slots.Color;
var repromptText = "";
var sessionAttributes = {};
var shouldEndSession = false;
var speechOutput = "";
if (favoriteColorSlot) {
favoriteColor = favoriteColorSlot.value;
sessionAttributes = createFavoriteColorAttributes(favoriteColor);
speechOutput = "I now know your favorite color is " + favoriteColor + ". You can ask me "
+ "your favorite color by saying, what's my favorite color?";
repromptText = "You can ask me your favorite color by saying, what's my favorite color?";
} else {
speechOutput = "I'm not sure what your favorite color is, please try again";
repromptText = "I'm not sure what your favorite color is, you can tell me your "
+ "favorite color by saying, my favorite color is red";
}
callback(sessionAttributes,
buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
}
function createFavoriteColorAttributes(favoriteColor) {
return {
favoriteColor: favoriteColor
};
}
function getColorFromSession(intent, session, callback) {
var cardTitle = intent.name;
var favoriteColor;
var repromptText = null;
var sessionAttributes = {};
var shouldEndSession = false;
var speechOutput = "";
if(session.attributes) {
favoriteColor = session.attributes.favoriteColor;
}
if(favoriteColor) {
speechOutput = "Your favorite color is " + favoriteColor + ", goodbye";
shouldEndSession = true;
}
else {
speechOutput = "I'm not sure what your favorite color is, you can say, my favorite color "
+ " is red";
}
// Setting repromptText to null signifies that we do not want to reprompt the user.
// If the user does not respond or says something that is not understood, the session
// will end.
callback(sessionAttributes,
buildSpeechletResponse(intent.name, speechOutput, repromptText, shouldEndSession));
}
// --------------- Helpers that build all of the responses -----------------------
function buildSpeechletResponse(title, output, repromptText, shouldEndSession) {
return {
outputSpeech: {
type: "PlainText",
text: output
},
card: {
type: "Simple",
title: "SessionSpeechlet - " + title,
content: "SessionSpeechlet - " + output
},
reprompt: {
outputSpeech: {
type: "PlainText",
text: repromptText
}
},
shouldEndSession: shouldEndSession
}
}
function buildResponse(sessionAttributes, speechletResponse) {
return {
version: "1.0",
sessionAttributes: sessionAttributes,
response: speechletResponse
}
}
Can some one please guide me on what's wrong? it's probably a scope issue, I'm not sure.

Move your call back inside the twitter get function. Then your callback will be called on a successful get from the twitter api. Also you will have access to the data object. You will probably want to add a failure case as well and include a context.fail().
If you need to, you can also update the timeout parameter under the configuration tab of the AWS console. Its under advanced settings. Also, Its often useful to take Alexa out of the equation when debugging and just get the twitter api piece working first.
// request data
twitter.get('https://api.twitter.com/1.1/statuses/user_timeline.json', params, function (data) {
console.log(util.inspect(data[0].text));
// If we wanted to initialize the session to have some attributes we could add those here.
var sessionAttributes = {};
var cardTitle = "Welcome";
var speechOutput=data[0].text;
var repromptText = "";
var shouldEndSession = true;
callback(sessionAttributes,
buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
});

Related

Alexa skill which can read Facebook posts using AWS Lambda, Node.js and the Alexa Skills Kit

I am developing an Alexa skill using AWS Lambda, Node.js and the Alexa Skills Kit.I am using a forked from skill-sample-nodejs-fact project & successfully deployed & tested the sample fact project .Now I am trying to modify that code to read posts on some Facebook feeds.First I tried to develop some node application which can read posts & it was successful.Please find below code for your reference.I used fb module -
https://www.npmjs.com/package/fb
const FB = require('fb');
FB.setAccessToken('abc');
const query='cnninternational/posts';
FB.api(query, function (res) {
if(!res || res.error) {
console.log(!res ? 'error occurred' : res.error);
return;
}
console.log(res);
});
Next, I tried to integrate above code block into the lambda function.Unfortunately, I was unable, to read Facebook posts using these codes.Please find those code blocks in the below panel .Also, I checked cloudwatch logs as well.I can see the "GetNewsIntent", but I didn't see "fb-init" , "fb-error" or "fb-exit"entries in logs.Surprisingly, no error in logs as well.I would much appreciate it if someone can help to solve that issue.
'use strict';
const Alexa = require('alexa-sdk');
const FB = require('fb');
const APP_ID = 'abc';
const SKILL_NAME = 'test';
const GET_FACT_MESSAGE = "Here's your news: ";
const STOP_MESSAGE = 'Goodbye!';
exports.handler = function(event, context, callback) {
var alexa = Alexa.handler(event, context);
alexa.appId = APP_ID;
alexa.registerHandlers(handlers);
alexa.execute();
};
const handlers = {
'LaunchRequest': function () {
this.emit('GetNewsIntent');
},
'GetNewsIntent': function () {
console.log('GetNewsIntent');
const speechOutput = GET_FACT_MESSAGE;
const query='cnninternational/posts';
FB.setAccessToken('abc');
FB.api(query, function (res) {
console.log('fb-init');
if(!res || res.error) {
console.log(!res ? 'error occurred' : res.error);
console.log('fb-error');
return;
}
console.log(res);
speechOutput = speechOutput + res;
console.log('fb-exit');
});
this.response.cardRenderer(SKILL_NAME, speechOutput);
this.response.speak(speechOutput);
this.emit(':responseReady');
},
'AMAZON.StopIntent': function () {
this.response.speak(STOP_MESSAGE);
this.emit(':responseReady');
},
};
Have you implemented account linking? You should be using event.session.user.accessToken for the parameter to setAccessToken().
I have removed this.response.cardRenderer , this.response.speak & changed the code bit.It's working fine now.Please find the below code snippet which can be used to read posts on the BBC Facebook page.
var accessToken = '';
exports.handler = function(event, context, callback) {
var alexa = Alexa.handler(event, context);
alexa.appId = APP_ID;
alexa.registerHandlers(handlers);
alexa.execute();
};
const handlers = {
'NewSession': function() {
var welcomeMessage = "Welcome to Athena";
welcomeMessage = welcomeMessage +"<break time=\"1s\"/>"+ "<audio src='https://s3.amazonaws.com/my-ssml-samples/Flourish.mp3' />"+"<break time=\"1s\"/>";
welcomeMessage += HELP_MESSAGE;
accessToken = this.event.session.user.accessToken;
if (accessToken) {
FB.setAccessToken(accessToken);
this.emit(':ask', welcomeMessage, HELP_REPROMPT);
}
else {
// If we don't have an access token, we close down the skill.
this.emit(':tellWithLinkAccountCard', "This skill requires you to link a Facebook account. Seems like you are not linked to a Facebook Account. Please link a valid Facebook account and try again.");
}
},
'LaunchRequest': function () {
this.emit('NewSession');
},
'ReadBbcNewsFacebookPostsIntent': function () {
var alexa = this;
FB.api("bbcnews/posts", function (response) {
if (response && !response.error) {
if (response.data) {
var output = "Here are recent posts" + "<break time=\"1s\"/>";
var max = 5;
for (var i = 0; i < response.data.length; i++) {
if (i < max) {
output += "<break time=\"1s\"/>" + "Post " +
(i + 1) +
response.data[i].message.replace(/(?:https?|ftp):\/\/[\n\S]+/g, '')
+ ". ";
}
}
alexa.emit(':ask', output+ ", What would you like to do next?",HELP_MESSAGE);
} else {
// REPORT PROBLEM WITH PARSING DATA
}
} else {
// Handle errors here.
console.log(response.error);
this.emit(':tell', EMPTY_ACCESS_TOKEN_MESSAGE, TRY_AGAIN_MESSAGE);
}
});
}
};

AWS lambda to firebase realtime db not updated

'use strict';
var Firebase = require('firebase');
var config = {
apiKey: "apiKey",
authDomain: "projectId.firebaseapp.com",
databaseURL: "https://databaseName.firebaseio.com",
storageBucket: "bucket.appspot.com"
};
if(Firebase.apps.length === 0) {
Firebase.initializeApp(config);
}
exports.handler = function(event, context,callback) {
try{
var request = event.request;
if(request.type === "LaunchRequest"){
let options = {};
options.speechText= "Welcome to TV. Using this skill you can control over 60 channels on your tv. To change to a different channel? You can say for example, play BBC Earth on tv";
options.endSession= false;
context.succeed(buildResponse(options));
}else if(request.type ==="IntentRequest"){
let options ={};
if(request.intent.name === "ChannelIntent"){
var message = 'hey guys';
var ref = Firebase.database().ref().child("hello");
var messagesRef = ref.child('messages');
var messageRef = messagesRef.push(message);
let ChannelName = request.intent.slots.ChannelName.value;
options.speechText= "Channel changed to " +ChannelName+ ". ";
options.endSession= true;
context.succeed(buildResponse(options));
}else{
throw "unknown intent type";
}
}else if(request.type ==="SessionEndedRequest"){
}else{
throw "unknown intent type";
}
} catch(e){
context.fail("Exception: "+e);
}
}
function buildResponse(options){
var response = {
version: "1.0",
response:{
outputSpeech: {
type: "PlainText",
text: options.speechText
},
shouldEndSession: options.endSession
}
};
if(options.repromptText){
response.reponse.reprompt ={
outputSpeech: {
type: "PlainText",
text: options.repromptText
}
};
}
return response;
}
The idea is to use alexa skill to control android tv app. firebase is used to connect alexa skill to the android tv app. The code runs successfully in AWS lambda but firebase db is not updated.
If someone has encountered similar issue and has found solution, please share the solution.
I will add code snippet of my aws lambda function.
Most Firebase operations are async, so you can't just assume success. You must wait for the call to complete:
messagesRef.push(message, (err) => {
if (err) return context.fail(err);
let ChannelName = request.intent.slots.ChannelName.value;
options.speechText= "Channel changed to " +ChannelName+ ". ";
options.endSession= true;
context.succeed(buildResponse(options));
})

Alexa-SDK Audio Issue

I've been trying to make an alexa skill that involves audio. I found a great guide here.
Here is their example code:
var stateByUser = {};
var podcastURL = "https://feeds.soundcloud.com/stream/309340878-user-652822799-episode-010-building-an-alexa-skill-with-flask-ask-with-john-wheeler.mp3";
// Entry-point for the Lambda
exports.handler = function(event, context) {
var player = new SimplePlayer(event, context);
player.handle();
};
// The SimplePlayer has helpful routines for interacting with Alexa, within minimal overhead
var SimplePlayer = function (event, context) {
this.event = event;
this.context = context;
};
// Handles an incoming Alexa request
SimplePlayer.prototype.handle = function () {
var requestType = this.event.request.type;
var userId = this.event.context ? this.event.context.System.user.userId : this.event.session.user.userId;
var response = null;
// On launch, we tell the user what they can do (Play audio :-))
if (requestType === "LaunchRequest") {
this.say("Welcome to the Simple Audio Player. Say Play to play some audio!", "You can say Play");
// Handle Intents here - Play, Pause and Resume is all for now
} else if (requestType === "IntentRequest") {
var intent = this.event.request.intent;
if (intent.name === "Play") {
this.play(podcastURL, 0);
} else if (intent.name === "AMAZON.PauseIntent") {
// When we receive a Pause Intent, we need to issue a stop directive
// Otherwise, it will resume playing - essentially, we are confirming the user's action
this.stop();
} else if (intent.name === "AMAZON.ResumeIntent") {
var lastPlayed = this.load(userId);
var offsetInMilliseconds = 0;
if (lastPlayed !== null) {
offsetInMilliseconds = lastPlayed.request.offsetInMilliseconds;
}
this.play(podcastURL, offsetInMilliseconds);
}
} else if (requestType === "AudioPlayer.PlaybackStopped") {
// We save off the PlaybackStopped Intent, so we know what was last playing
this.save(userId, this.event);
}
};
/**
* Creates a proper Alexa response using Text-To-Speech
* #param message
* #param repromptMessage
*/
SimplePlayer.prototype.say = function (message, repromptMessage) {
var response = {
version: "1.0",
response: {
shouldEndSession: false,
outputSpeech: {
type: "SSML",
ssml: "<speak> " + message + " </speak>"
},
reprompt: {
outputSpeech: {
type: "SSML",
ssml: "<speak> " + message + " </speak>"
}
}
}
}
this.context.succeed(response);
};
/**
* Plays a particular track, from specific offset
* #param audioURL The URL to play
* #param offsetInMilliseconds The point from which to play - we set this to something other than zero when resuming
*/
SimplePlayer.prototype.play = function (audioURL, offsetInMilliseconds) {
var response = {
version: "1.0",
response: {
shouldEndSession: true,
directives: [
{
type: "AudioPlayer.Play",
playBehavior: "REPLACE_ALL", // Setting to REPLACE_ALL means that this track will start playing immediately
audioItem: {
stream: {
url: audioURL,
token: "0", // Unique token for the track - needed when queueing multiple tracks
expectedPreviousToken: null, // The expected previous token - when using queues, ensures safety
offsetInMilliseconds: offsetInMilliseconds
}
}
}
]
}
}
this.context.succeed(response);
};
// Stops the playback of Audio
SimplePlayer.prototype.stop = function () {
var response = {
version: "1.0",
response: {
shouldEndSession: true,
directives: [
{
type: "AudioPlayer.Stop"
}
]
}
}
this.context.succeed(response);
};
// Saves information into our super simple, not-production-grade cache
SimplePlayer.prototype.save = function (userId, state) {
console.log("Save: " + userId);
stateByUser[userId] = state;
};
// Load information from our super simple, not-production-grade cache
SimplePlayer.prototype.load = function (userId) {
console.log("Load: " + userId);
var state = null;
if (userId in stateByUser) {
state = stateByUser[userId];
console.log("Loaded " + userId + " State: " + state);
}
return state;
};
I am trying to refactor this code so that it follows a similar format to the trivia skills example that amazon provides. However, when I run my refactored code I get an error saying
TypeError: Cannot set property 'say' of undefined
at Object.<anonymous> (/Users/Rob/Desktop/super-simple-audio-player/index.js:47:28)
Here is my attempt at refactoring
"use strict";
var stateByUser = {};
var podcastURL = "https://p.scdn.co/mp3-preview/2385471a5d35709ad90e368dacabe4082af4541a?cid=null";
var Alexa = require("alexa-sdk");
// Entry-point for the Lambda
exports.handler = function(event, context) {
var alexa = Alexa.handler(event, context);
alexa.registerHandlers(SimplePlayer);
alexa.execute();
};
// The SimplePlayer has helpful routines for interacting with Alexa, within minimal overhead
var SimplePlayer = {
"LaunchRequest": function () {
this.emit(":tell","Welcome to the Simple Audio Player. Say play to begin.");
},
"Play": function() {
this.play(podcastURL, 0);
},
"AMAZON.PauseIntent": function() {
this.stop();
},
"AMAZON.ResumeIntent": function () {
var lastPlayed = this.load(userId);
var offsetInMilliseconds = 0;
if (lastPlayed !== null) {
offsetInMilliseconds = lastPlayed.request.offsetInMilliseconds;
}
this.play(podcastURL, offsetInMilliseconds);
},
"AudioPlayer.PlaybackStopped": function () {
this.save(userId, this.event);
}
};
// Handles an incoming Alexa request
SimplePlayer.prototype.say = function (message, repromptMessage) {
var response = {
version: "1.0",
response: {
shouldEndSession: false,
outputSpeech: {
type: "SSML",
ssml: "<speak> " + message + " </speak>"
},
reprompt: {
outputSpeech: {
type: "SSML",
ssml: "<speak> " + message + " </speak>"
}
}
}
}
this.context.succeed(response);
};
/**
* Plays a particular track, from specific offset
* #param audioURL The URL to play
* #param offsetInMilliseconds The point from which to play - we set this to something other than zero when resuming
*/
SimplePlayer.prototype.play = function (audioURL, offsetInMilliseconds) {
var response = {
version: "1.0",
response: {
shouldEndSession: true,
directives: [
{
type: "AudioPlayer.Play",
playBehavior: "REPLACE_ALL", // Setting to REPLACE_ALL means that this track will start playing immediately
audioItem: {
stream: {
url: audioURL,
token: "0", // Unique token for the track - needed when queueing multiple tracks
expectedPreviousToken: null, // The expected previous token - when using queues, ensures safety
offsetInMilliseconds: offsetInMilliseconds
}
}
}
]
}
}
this.context.succeed(response);
};
// Stops the playback of Audio
SimplePlayer.prototype.stop = function () {
var response = {
version: "1.0",
response: {
shouldEndSession: true,
directives: [
{
type: "AudioPlayer.Stop"
}
]
}
}
this.context.succeed(response);
};
// Saves information into our super simple, not-production-grade cache
SimplePlayer.prototype.save = function (userId, state) {
console.log("Save: " + userId);
stateByUser[userId] = state;
};
// Load information from our super simple, not-production-grade cache
SimplePlayer.prototype.load = function (userId) {
console.log("Load: " + userId);
var state = null;
if (userId in stateByUser) {
state = stateByUser[userId];
console.log("Loaded " + userId + " State: " + state);
}
return state;
};
I've added alexa-sdk and changed the exports.handler and the simplePlayer.prototype.handler(). Any thoughts as to why it is not working?
Thanks in advance
I actually created the project you reference. Glad you are finding it useful.
In re-factoring the project, you changed it from prototype-style JS object to an object literal. Both are viable approaches, but the object literal becomes a problem when holding the state for a particular request (the event and context fields in particular).
It also means that the prototype methods defined in the project are not available from the object literal definition. You need to instantiate SimplePlayer (by calling new SimplePlayer(event, context)) before you will get those.
If you want to understand the trade-off between these approaches better, you can read here:
Object literal vs constructor+prototype
Here is an example of working with the Alexa SDK consistent with my project. It defines the "LaunchRequest" function as a prototype function rather than simply a property:
SimplePlayer.prototype.LaunchRequest = function () {
this.emit(":tell", "Welcome to the Simple Audio Player. Say play to begin.");
};
You also need to make sure to instantiate the SimplePlayer (not just reference it). When registering it, it should look like this:
alexa.registerHandlers(new SimplePlayer(event, context));
Hope that makes sense, and good luck with it! Let me know how it goes (I can always be reached at https://gitter.im/bespoken/bst)

Alexa Test Response does not contain outputSpeech

I'm new to Alexa, and have followed the airportinfo tutorial and I have copied the code from github https://github.com/bignerdranch/alexa-airportinfo and when i test it using npm and input an airport code e.g. SFO, Theres no "outputSpeech:" and i tried making a similar skill with the same issue, I'm not sure what I'm doing wrong. I have both index.js and FAADataInfo.js Thanks in advance for your help.
This is the index.js file
'use strict';
module.change_code = 1;
var _ = require('lodash');
var Alexa = require('alexa-app');
var skill = new Alexa.app('airportinfo');
var FAADataHelper = require('./faa_data_helper');
skill.launch(function(req, res) {
var prompt = 'For delay information, tell me an Airport code.';
res.say(prompt).reprompt(prompt).shouldEndSession(false);
});
skill.intent('airportInfoIntent', {
'slots': {
'AIRPORTCODE': 'FAACODES'
},
'utterances': [
'{|flight|airport} {|delay|status} {|info} {|for} {-|AIRPORTCODE}'
]
},
function(req, res) {
var airportCode = req.slot('AIRPORTCODE');
var reprompt = 'Tell me an airport code to get delay information.';
if (_.isEmpty(airportCode)) {
var prompt = 'I didn\'t hear an airport code. Tell me an airport code.';
res.say(prompt).reprompt(reprompt).shouldEndSession(false);
return true;
} else {
var faaHelper = new FAADataHelper();
console.log(airportCode);
faaHelper.getAirportStatus(airportCode).then(function(airportStatus) {
console.log(airportStatus);
res.say(faaHelper.formatAirportStatus(airportStatus)).send();
}).catch(function(err) {
console.log(err.statusCode);
var prompt = 'I didn\'t have data for an airport code of ' +
airportCode;
res.say(prompt).reprompt(reprompt).shouldEndSession(false).send();
});
return false;
}
}
);
module.exports = skill;
and heres FAADataInfo.js
'use strict';
var _ = require('lodash');
var requestPromise = require('request-promise');
var ENDPOINT = 'http://services.faa.gov/airport/status/';
function FAADataHelper() {
}
FAADataHelper.prototype.getAirportStatus = function(airportCode) {
var options = {
method: 'GET',
uri: ENDPOINT + airportCode,
json: true
};
return requestPromise(options);
};
FAADataHelper.prototype.formatAirportStatus = function(aiportStatusObject) {
if (aiportStatusObject.delay === 'true') {
var template = _.template('There is currently a delay for ${airport}. ' +
'The average delay time is ${delay_time}.');
return template({
airport: aiportStatusObject.name,
delay_time: aiportStatusObject.status.avgDelay
});
} else {
//no delay
var template =_.template('There is currently no delay at ${airport}.');
return template({
airport: aiportStatusObject.name
});
}
};
module.exports = FAADataHelper;
This is the response that I get
{
"version": "1.0",
"response": {
"directives": [],
"shouldEndSession": true
},
"sessionAttributes": {},
"dummy": "text"
}
The alexa-app version that the tutorial is using is out of date. When using the latest alexa-app npm version (4.0.0), the return value for the .intent() function should be a Promise and not a boolean if you are running asynchronous functions.
In your index.js, add:
return faaHelper.getAirportStatus(....) {}.catch(){}
and remove the return false; after the catch.
Here's the full skill.intent() code
skill.intent('airportInfoIntent', {
'slots': {
'AIRPORTCODE': 'FAACODES'
},
'utterances': [
'{|flight|airport} {|delay|status} {|info} {|for} {-|AIRPORTCODE}'
]
},
function(req, res) {
var airportCode = req.slot('AIRPORTCODE');
var reprompt = 'Tell me an airport code to get delay information.';
if (_.isEmpty(airportCode)) {
var prompt = 'I didn\'t hear an airport code. Tell me an airport code.';
res.say(prompt).reprompt(reprompt).shouldEndSession(false);
return true;
} else {
var faaHelper = new FAADataHelper();
console.log(airportCode);
return faaHelper.getAirportStatus(airportCode).then(function(airportStatus) {
console.log(airportStatus);
res.say(faaHelper.formatAirportStatus(airportStatus)).send();
}).catch(function(err) {
console.log(err.statusCode);
var prompt = 'I didn\'t have data for an airport code of ' +
airportCode;
res.say(prompt).reprompt(reprompt).shouldEndSession(false).send();
});
//return false;
}
}
);

Amazon Alexa Session Attributes don't persist

I'm using the nodejs 'alexa-sdk' to develop a pension calculator skill for Amazon Alexa. The problem I have is that when switching from one state to another it doesn't keep the session attributes. I'm storing them like
this.attributes['ATTRIBUTE_NAME'] = 'some_value';
But when I get to the point to give an answer (Alexa.CreateStateHandler(states.ANSWER[...]), all attributes are 'undefined'. Can anyone advise what I'm doing wrong with storing or passing on the session attributes?
var Alexa = require('alexa-sdk');
var moment = require('moment'); // deals with dates and date formatting, for instance converts AMAZON.DATE to timestamp
// import pension calculator
var calculator = require('./pensionCalculator');
var GENDERCUTOFFDATE = new Date(1953, 12, 6);
// States are required for conversational skills.
// States assume a context. e.g. _DOB expects date of birth; providing a gender in this state would confuse Alexa.
// UX design principle have to be as unambiguous as possible in language (e.g. "are you male or female" vs "what gender are you?")
var states = {
START: '_STARTMODE', // Prompt the user to start or restart
DOB: '_DOB',
GENDER: '_GENDER',
ANSWER: '_ANSWER'
};
// Outbound messages spoken back to end user.
// alexa-nodejs-sdk wraps all strings in the advanced SSML speech markup (<speak>STRING HERE</speak>) that allows phonetic overrides etc.
var snippets = {
WELCOME: "<s>Welcome to the D W P Pension Age calculator.</s> " +
"<s>You can ask to calculate your pension age or for the U K pension eligibility criteria?</s>",
WELCOME_REPROMPT: "You can say, " +
"Calculate my pension age or, say what are the eligibility criteria in the U K.",
GENDER: "Thank you. Are you female or male?",
GENDER_REPROMPT: "In order to calculate your pension age, please tell me: Are you male or female?",
GENDER_INVALID: "Sorry I couldn't understand your gender, can you please tell me if you are you male or female?",
DATEOFBIRTH: "Ok, please tell me what is your date of birth?",
DATEOFBIRTH_REPROMPT: "In order to calculate your pension age please tell me your date of birth?",
DATEOFBIRTH_INVALID_FUTURE: "Nice you're from the future. Did you bring a hoverboard? Seriously, can you please say your actual date of birth please?",
DATEOFBIRTH_INVALID: "Please say your date of birth. For example you can say, my date of birth is the 23rd of April 1980",
STOP: "Thank you for using the D W P pension calculator.",
HELP: "You can ask things like: What is my pension age or what are the eligibility criteria.",
HELP_REPROMPT: "Simply say: calculate pension or eligibility criteria.",
UNHANDLED: "I'm sorry I couldn't understand what you meant. Can you please say it again?"
};
// You define a set of state handlers for every state PLUS the new session / launch event.
var newSessionHandlers = {
// session variables stored in this.attributes
// session state is stored in this.handler.state
// handler.state vs Intent vs
'LaunchRequest': function() {
// Initialise State
this.handler.state = states.START;
// emitWithState should be called executeStateHandler("Start").
// As such this will call a handler "Start" in the startStateHandlers object.
// Maybe this line and the previous line could be more coherently wrapped into a single
// function:
// this.stateTransition( states.START, "Start" )
this.emit("Start")
},
// It's unclear whether this can ever happen as it's triggered by Alexa itself.
"Unhandled": function () {
var speechText = "I wasn't launched yet";
this.emit(":ask", speechText);
}
};
// This is the beginning of our skill.
// This is a list of accepted intents Alexa is listening for
// when the skill has just started.
// In this specific version, a user can't provide things like date of birth
// or gender as part of the initial skill invocation because we've not included in this set of start state handlers.
// We could but haven't in this particular scenario.
var startStateHandlers = Alexa.CreateStateHandler(states.START, {
'Start': function() {
this.attributes['dob'] = '';
this.attributes['gender'] = '';
this.handler.state = states.START;
var speechText = snippets.WELCOME;
var repromptText = snippets.WELCOME_REPROMPT;
// emit is the exit point to instruct Alexa how to and what to communicate with end user.
// e.g. do we want further information? (:ask) / no further information, skill terminates (:tell)
// do we provide a voice response with or without a card on the mobile device (:ask vs :askWithCard)
// https://github.com/alexa/alexa-skills-kit-sdk-for-nodejs
// as we've said :ask we are expecting the user to provide more information.
// maybe this function could be called this.respond()
// this is going to speak the snippets.WELCOME which implicitly asks a question (hence :ask).
// reprompt text is automatically spoken after a few seconds. This is a feature of the NodeJS SDK.
// See Unhandled for the fallback / unrecognised utteranes.
this.emit(':ask', speechText, repromptText);
},
// the intent text is defined in the
// Alexa interaction model web page at developer.amazon.com/ask
// represented as sample utterances.
'StartCalculationIntent': function () {
var speechText = snippets.DATEOFBIRTH;
var repromptText = snippets.DATEOFBIRTH_REPROMPT;
// Change State to calculation
this.handler.state = states.DOB;
this.emit(':ask', speechText, repromptText);
},
// a predefined Utterance that you don't need to define in your interaction model
// We are choosing to provide this help function but equally you don't need to.
"AMAZON.HelpIntent": function () {
var speechText = snippets.HELP;
var repromptText = snippets.HELP_REPROMPT;
this.emit(':ask', speechText, repromptText);
},
"Unhandled": function () {
var speechText = snippets.UNHANDLED;
this.emit(":ask", speechText);
},
// User says stop. Stops even in the middle of a response.
"AMAZON.StopIntent": function () {
var speechText = snippets.STOP;
this.emit(":tell", speechText);
},
// unclear really what the difference is; default working practice is
// to do the same thing
// in a production system we'd probably dedupe this function.
"AMAZON.CancelIntent": function () {
var speechText = snippets.STOP;
this.emit(":tell", speechText);
},
"AMAZON.StartOverIntent": function () {
this.emit("Start")
},
// TODO determine when this is requested and what initiates it
// Implement handler to save state if state should be stored persistently e.g. to DynamoDB
// 'SessionEndedRequest': function () {
// console.log('session ended!');
// this.emit(':saveState', true);
// }
// TODO add 'AMAZON.RepeatIntent' that repeats the last question.
});
var dobStateHandlers = Alexa.CreateStateHandler(states.DOB, {
'DateOfBirthIntent': function () {
var speechText = "",
repromptText = "";
var date_string = this.event.request.intent.slots.dob.value;
var date = moment(date_string);
if (date.isValid()) {
if (!isFutureDate(date)) {
// ALL GOOD – dob not in the future
speechText = snippets.GENDER;
repromptText = snippets.GENDER_REPROMPT;
this.attributes['dob'] = date_string;
if(isGenderNeeded(date)) {
// Transition to next state
this.handler.state = states.GENDER;
// this.emit(":saveState", false);
this.emit(':ask', speechText, repromptText);
} else {
// gender not needed
// this.attributes['gender'] = "unisex";
this.handler.state = states.ANSWER;
// this.emit(":saveState", false);
this.emit("Answer")
}
} else {
// dob in the future
speechText = snippets.DATEOFBIRTH_INVALID_FUTURE;
// this.emit(":saveState", false);
repromptText = snippets.DATEOFBIRTH_INVALID_FUTURE; // could be improved by using alternative prompt text
this.emit(':ask', speechText, repromptText);
}
} else {
// not a valid Date
speechText = snippets.DATEOFBIRTH_INVALID;
// this.emit(':saveState', false);
repromptText = snippets.DATEOFBIRTH_INVALID; // could be improved by using alternative prompt text
this.emit(':ask', speechText, repromptText);
}
},
"AMAZON.HelpIntent": function () {
var speechText = snippets.HELP;
var repromptText = snippets.HELP_REPROMPT;
this.emit(':ask', speechText, repromptText);
},
"Unhandled": function () {
var speechText = snippets.UNHANDLED;
this.emit(":ask", speechText);
},
"AMAZON.StopIntent": function () {
var speechText = snippets.STOP;
this.emit(":tell", speechText);
},
"AMAZON.CancelIntent": function () {
var speechText = snippets.STOP;
this.emit(":tell", speechText);
},
"AMAZON.StartOverIntent": function () {
this.emit("Start")
},
'SessionEndedRequest': function () {
// this.emit(':saveState', false);
// this.attributes['dob'] = date_string;
// this.attributes['dob'] = date_string;
console.log('session ended!');
}
});
var genderStateHandlers = Alexa.CreateStateHandler(states.GENDER, {
'GenderIntent': function () {
var speechText = "",
repromptText = "";
var gender = this.event.request.intent.slots.gender.value;
if (isGenderSlotValid(gender)) {
// valid gender
this.attributes['gender'] = gender;
this.handler.state = states.ANSWER;
this.emit(':saveState', false);
this.emit("Answer");
} else {
// not a valid gender
speechText = snippets.GENDER_INVALID;
repromptText = snippets.GENDER_INVALID; // could be improved by using alternative prompt text
this.emit(':saveState', false);
this.emit(':ask', speechText, repromptText);
}
},
"AMAZON.HelpIntent": function () {
var speechText = snippets.HELP;
var repromptText = snippets.HELP_REPROMPT;
this.emit(':ask', speechText, repromptText);
},
"Unhandled": function () {
var speechText = snippets.UNHANDLED;
this.emit(":ask", speechText);
},
"AMAZON.StopIntent": function () {
var speechText = snippets.STOP;
this.emit(":tell", speechText);
},
"AMAZON.CancelIntent": function () {
var speechText = snippets.STOP;
this.emit(":tell", speechText);
},
"AMAZON.StartOverIntent": function () {
this.emitWithState("Start")
},
'SessionEndedRequest': function () {
this.emit(':saveState', false);
console.log('session ended!');
}
});
var answerStateHandlers = Alexa.CreateStateHandler(states.ANSWER, {
'Answer': function () {
console.log(`##### START INPUT SNIPPETS #####`);
// var dob =
// var gender = ;
console.log(`this.attributes.dob: "${this.attributes.dob}"`);
console.log(`this.attributes.gender: "${this.attributes.gender}"`);
var pensionDate = calculator.calculatePension(this.attributes.dob, this.attributes.gender);
console.log(`pensionDate: "${pensionDate}"`);
var speechText = calculator.createPensionSnippet(pensionDate);
// Change State to Start again
this.handler.state = states.START;
this.emit(':tell', speechText);
console.log(`##### STOP SNIPPET #####`);
console.log(``);
},
"Unhandled": function () {
var speechText = snippets.UNHANDLED;
this.emit(":ask", speechText);
}
});
function isGenderSlotValid(gender) {
if (gender == "male" || gender == "female") {
return true
} else {
return false
}
}
function isFutureDate(dob) {
var today = moment();
if (dob > today) {
return true
} else {
return false
}
}
function isGenderNeeded(dob) {
return dob < GENDERCUTOFFDATE;
}
exports.handler = function(event, context, callback) {
var alexa = Alexa.handler(event, context);
alexa.appId = process.env.appId;
// alexa.appId = "your skill ID"; // better store it as ENV variable at AWS Lambda
// alexa.resources = languageStrings;
// register intent handlers for each state + new session.
// Each state constrains possible intents Alexa is listening for.
// If you only have one handler you are context-free and cannot have state.
alexa.registerHandlers(newSessionHandlers, startStateHandlers, dobStateHandlers, genderStateHandlers, answerStateHandlers);
alexa.execute();
};
You're missing a few ";"'s after this.emit("..."), They may be optional but I'd add them.
Try enabling "strict mode" for better error detection
Also why are you using
this.emit(':saveState', false);
It's commented out in a number of places except for inside GENDER
Finally, I reference my state variables as
this.attributes['gender']
Not a bad thing to try.
Possibility 1:
this.emit(':saveState', false);
Is used to save data to a DynamoDB database. Since you do not appear to be saving to a database this line of code is unnecesary. Alexa will persist your variables throughout a user session and they will then be forgotten either when the skill times out or the user ends the skill.
It's possible there is a mixup trying to save to a non existant database and that's what's causing your dropped data.
If you're interested in saving to a database follow the instructions found in the Alexa Node SDK
https://github.com/alexa/alexa-skills-kit-sdk-for-nodejs
It's as simple as adding 1 line of code:
alexa.dynamoDBTableName = 'YourTableName'; // That's it!
Advice: Remove all save state directives
Possibility 2:
You have multiple states linked together in an odd way.
if (isGenderSlotValid(gender)) {
// valid gender
this.attributes['gender'] = gender;
this.handler.state = states.ANSWER;
this.emit(':saveState', false);
this.emit("Answer");
}
In this code your user has invoked the 'GenderIntent' you've determined what the gender is and then you call another intent "Answer". I see you "try to save the data" between switching intent but as stated above this.emit(':saveState', false); saves to a database not locally. It's very likely that the session attributes are saved when you emit and :ask or a :tell therefore since you are switching to a new intent handler Answer before returning an :ask or a :tell it's likely you are over writing or discarding the previous event handler, gender's, actions.
Advice: move the code from Answer and put it inside gender. Once you have all the information you need there's no need to jump to a different handler.
if (isGenderSlotValid(gender)) {
// valid gender
this.attributes['gender'] = gender;
var pensionDate = calculator.calculatePension(this.attributes.dob, this.attributes.gender);
console.log(`pensionDate: "${pensionDate}"`);
var speechText = calculator.createPensionSnippet(pensionDate);
// Change State to Start again
this.handler.state = states.START;
this.emit(':tell', speechText);
}
PS you do the same thing in DOB:
} else {
// gender not needed
// this.attributes['gender'] = "unisex";
this.handler.state = states.ANSWER;
// this.emit(":saveState", false);
this.emit("Answer")
}
Delete your answer state and move its functionality into both of these functions where it says this.emit("Answer")
Just add :
'SessionEndedRequest': function () {
console.log('session ended!'); // Optional
this.emit(':saveState', true);
}
In your handler function beside other intents and it will save the data whenever the user ends the session unexpectedly.
Have you tried accessing them like this:
this.attributes['gender'].

Resources