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'].
Related
I want to book an ambulance while having conversation with user via google assistant.I have to get the distance between the pickup point and destination point to calculate fare as per the kms.
I have the user's location in longitudes and latitudes. and the destination location as a address(Fortis Hospital at Bhoiwada, Kalyan). How do I calculate the distance?
'use strict';
const functions = require('firebase-functions');
const { dialogflow } = require('actions-on-google');
const {Card, Suggestion} = require('dialogflow-fulfillment');
const {Permission,Place} = require('actions-on-google');
const app = dialogflow();
app.intent('Default Welcome Intent', conv => {
conv.close(`Welcome to my agent!`);
conv.ask('which type of ambulance would you like to book?');
});
app.intent('Default Fallback Intent', conv=> {
conv.close('sorry i did not understand');
});
app.intent('typeofambulance',conv => {
conv.ask('ac or non-ac?');
});
app.intent('ac', conv => {
conv.ask('do you want to book it from your current location?');
});
app.intent('location', (conv) => {
conv.data.requestedPermission = 'DEVICE_PRECISE_LOCATION';
return conv.ask(new Permission({
context: 'to locate you',
permissions: conv.data.requestedPermission,
}));
});
app.intent('receive', (conv, params, permissionGranted) => {
//conv.ask('rr');
if (permissionGranted) {
// conv.ask('entered');
const {
requestedPermission
} = conv.data;
const {
coordinates
} = conv.device.location;
conv.ask(`You are at latitude ${coordinates.latitude} and longitude
${coordinates.longitude}` );
conv.ask('destination please!');
} else {
return conv.close('Sorry, permission denied.');
}
});
app.intent('destination', conv => {
conv.ask(new Place({
prompt: 'Destination point?',
context: 'To find a place to pick you up',
}));
});
app.intent('actions.intent.PLACE', (conv, input, place, status) => {
if (place) {
conv.ask(` Ah, I see. You want to get dropped at
${place.formattedAddress}`);
const ll=place.formattedAddress;
}
else {
// Possibly do something with status
conv.ask(`Sorry, I couldn't find where you want to get picked up`);
}
});
exports.dialogflowFirebaseFulfillment = functions.https.onRequest(app);
Distance calculation is possible given lat/lng, but the problem is that it won't really give you the distance it will take an ambulance to drive from one to the other, it will only get you the straight-line distance.
What you really need is to calculate (I am assuming) a route distance. You can calculate this using the Google Maps "Direction" API (since you're clearly already using Google things). This will require a valid Google Maps API key! Luckily, this has already been answered on Stack Overflow.
The basic idea is to use the directions service to get the route, then look at then result object for the distance between the start and end points (note that this code is hijacked from that other answer):
const directionsService = new DirectionsService;
const origin = "start address";
const destination = "end address";
const waypoints = addresses.map(stop => ({location: stop}));
directionsService.route({
origin,
waypoints,
destination,
travelMode: TravelMode.DRIVING,
}, (response, status) => {
if(status === DirectionsStatus.OK) {
let totalDistance = 0;
const legs = directionsResult.routes[0].legs;
for(let i=0; i<legs.length; ++i) {
totalDistance += legs[i].distance.value;
}
console.log(totalDistance);
} else {
return reject(new Error(status));
}
});
Alexa has released CanFulfillIntentRequest feature or Name-free Interaction for custom skills recently. I am trying to implement it in my existing skill which uses alexa-sdk. Please find my code below:
'use strict';
const Alexa = require('alexa-sdk');
var handlers = {
'LaunchRequest': function() {
var speechOutput = "You can ask me to read out quotes from Steve Jobs";
var repromptText = "Sorry I didnt understand";
this.emit(':tell', speechOutput, repromptText);
},
'RandomQuote': function() {
let data = getQuoteFunction();
const author = data[0];
const quote = data[1];
let cardTitle = "Quotation from author";
let cardContent = "Actual quote";
let speechOutput = "Actual quote";
// Speak out the output along with card information
this.emit(':tellWithCard', speechOutput, cardTitle, cardContent);
}
}
exports.handler = function (event, context, callback) {
const alexa = Alexa.handler(event, context, callback);
alexa.registerHandlers(handlers);
alexa.execute();
};
Do we need to add handler for CanFulfillIntentRequest, the way I did for other handlers ? for example:
var handlers = {
'LaunchRequest': function() {
},
'RandomQuote': function() {
},
'CanFulfillIntentRequest': function() {
//code to handle
}
}
Is this feature only available in ASK SDK v2 for Node.js ? Can we implement it in skill developed using alexa-sdk. Could anyone please let me know ?
Thanks
This is my first certification. My skill is pretty simple. I've registered two handlers:
const handlers = {
'LaunchRequest': function () {
this.handler.state = "ASKMODE";
this.emit(':ask',
'Ask me to play a musical note, for example, say C sharp');
},
'Unhandled': function () {
this.response.speak('I do not know how to proceed. Try asking for a note like, alexa, ask pitch pipe to give me a d flat.');
this.emit(':responseReady');
},
'PlayPitch': function () {
const noteSlot = this.event.request.intent.slots.Note;
let noteName;
if (noteSlot && noteSlot.value) {
noteName = noteSlot.value.toLowerCase();
var speech = resultingSpeech(noteName);
this.response.speak(speech);
}
else {
this.response.speak('I don\'t know that note.');
}
this.emit(':responseReady');
}
};
const followUpHandlers = Alexa.CreateStateHandler("ASKMODE", {
'PlayPitch': function () {
const noteSlot = this.event.request.intent.slots.Note;
let noteName;
if (noteSlot && noteSlot.value) {
noteName = noteSlot.value.toLowerCase();
var speech = resultingSpeech(noteName);
this.response.speak(speech);
}
else {
const speechOutput = 'Pitch Pipe will play any note of the 12 tone scale surrounding A440 ';
const reprompt = 'Simply speak the name of a note, such as e flat.';
this.response.speak(speechOutput).listen(reprompt);
this.emit(':responseReady');
}
this.emit(':responseReady');
},
'AMAZON.HelpIntent': function () {
const speechOutput = 'Pitch Pipe will play any note of the 12 tone scale surrounding A440 ';
const reprompt = 'Simply speak the name of a note, such as e flat.';
this.response.speak(speechOutput).listen(reprompt);
this.emit(':responseReady');
},
'AMAZON.CancelIntent': function () {
this.response.speak('Goodbye!');
this.emit(':responseReady');
},
'AMAZON.StopIntent': function () {
this.response.speak('See you later!');
this.emit(':responseReady');
},
'Unhandled': function () {
this.response.speak('I think you asked for a note, but I don\'t understand.');
this.emit(':responseReady');
}
});
So the original LaunchRequest on handlers works appropriately and when it comes back in ASKMODE it goes to the followUpHandlers with a PitchPipe intent, which works fine as well.
The problem is in handling help/cancel/stop. If, while in ASKMODE, you say, "Help" it comes back into the PitchPipe intent with no value in the slot. I have hacked it to handle help for now, but can't figure out how this should really work.
This problem was caused by the fact that I had saved my interaction model, but not built it. Once I built the interaction model, everything worked perfectly.
Yikes!
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)
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));
});