Skip to content

Instantly share code, notes, and snippets.

@muttoni
Created September 6, 2018 14:01
Show Gist options
  • Select an option

  • Save muttoni/e770df120d30276231351ffb67380df2 to your computer and use it in GitHub Desktop.

Select an option

Save muttoni/e770df120d30276231351ffb67380df2 to your computer and use it in GitHub Desktop.
times-tables
// Lambda Function code for Alexa.
// Paste this into your index.js file.
const Alexa = require("ask-sdk");
const https = require("https");
const invocationName = "multiplication test";
// Session Attributes
// Alexa will track attributes for you, by default only during the lifespan of your session.
// The history[] array will track previous request(s), used for contextual Help/Yes/No handling.
// Set up DynamoDB persistence to have the skill save and reload these attributes between skill sessions.
function getMemoryAttributes() { const memoryAttributes = {
"history":[],
"launchCount":0,
"lastUseTimestamp":0,
"lastSpeechOutput":{},
// "nextIntent":[]
// "favoriteColor":"",
// "name":"",
// "namePronounce":"",
// "email":"",
// "mobileNumber":"",
// "city":"",
// "state":"",
// "postcode":"",
// "birthday":"",
// "bookmark":0,
// "wishlist":[],
};
return memoryAttributes;
};
const maxHistorySize = 20; // remember only latest 20 intents
// 1. Intent Handlers =============================================
const AMAZON_FallbackIntent_Handler = {
canHandle(handlerInput) {
const request = handlerInput.requestEnvelope.request;
return request.type === 'IntentRequest' && request.intent.name === 'AMAZON.FallbackIntent' ;
},
handle(handlerInput) {
const request = handlerInput.requestEnvelope.request;
const responseBuilder = handlerInput.responseBuilder;
let sessionAttributes = handlerInput.attributesManager.getSessionAttributes();
let previousSpeech = getPreviousSpeechOutput(sessionAttributes);
return responseBuilder
.speak('Sorry I didnt catch what you said, ' + stripSpeak(previousSpeech.outputSpeech))
.reprompt(stripSpeak(previousSpeech.reprompt))
.getResponse();
},
};
const AMAZON_CancelIntent_Handler = {
canHandle(handlerInput) {
const request = handlerInput.requestEnvelope.request;
return request.type === 'IntentRequest' && request.intent.name === 'AMAZON.CancelIntent' ;
},
handle(handlerInput) {
const request = handlerInput.requestEnvelope.request;
const responseBuilder = handlerInput.responseBuilder;
let sessionAttributes = handlerInput.attributesManager.getSessionAttributes();
let say = 'Okay, talk to you later! ';
return responseBuilder
.speak(say)
.withShouldEndSession(true)
.getResponse();
},
};
const AMAZON_HelpIntent_Handler = {
canHandle(handlerInput) {
const request = handlerInput.requestEnvelope.request;
return request.type === 'IntentRequest' && request.intent.name === 'AMAZON.HelpIntent' ;
},
handle(handlerInput) {
const request = handlerInput.requestEnvelope.request;
const responseBuilder = handlerInput.responseBuilder;
let sessionAttributes = handlerInput.attributesManager.getSessionAttributes();
let history = sessionAttributes['history'];
let intents = getCustomIntents();
let sampleIntent = randomElement(intents);
let say = 'You asked for help. ';
let previousIntent = getPreviousIntent(sessionAttributes);
if (previousIntent && !handlerInput.requestEnvelope.session.new) {
say += 'Your last intent was ' + previousIntent + '. ';
}
// say += 'I understand ' + intents.length + ' intents, '
say += ' Here something you can ask me, ' + getSampleUtterance(sampleIntent);
return responseBuilder
.speak(say)
.reprompt('try again, ' + say)
.getResponse();
},
};
const AMAZON_StopIntent_Handler = {
canHandle(handlerInput) {
const request = handlerInput.requestEnvelope.request;
return request.type === 'IntentRequest' && request.intent.name === 'AMAZON.StopIntent' ;
},
handle(handlerInput) {
const request = handlerInput.requestEnvelope.request;
const responseBuilder = handlerInput.responseBuilder;
let sessionAttributes = handlerInput.attributesManager.getSessionAttributes();
let say = 'Okay, talk to you later! ';
return responseBuilder
.speak(say)
.withShouldEndSession(true)
.getResponse();
},
};
const AMAZON_NavigateHomeIntent_Handler = {
canHandle(handlerInput) {
const request = handlerInput.requestEnvelope.request;
return request.type === 'IntentRequest' && request.intent.name === 'AMAZON.NavigateHomeIntent' ;
},
handle(handlerInput) {
const request = handlerInput.requestEnvelope.request;
const responseBuilder = handlerInput.responseBuilder;
let sessionAttributes = handlerInput.attributesManager.getSessionAttributes();
let say = 'Hello from AMAZON.NavigateHomeIntent. ';
return responseBuilder
.speak(say)
.reprompt('try again, ' + say)
.getResponse();
},
};
const ChooseGameTypeIntent_Handler = {
canHandle(handlerInput) {
const request = handlerInput.requestEnvelope.request;
return request.type === 'IntentRequest' && request.intent.name === 'ChooseGameTypeIntent' ;
},
handle(handlerInput) {
const request = handlerInput.requestEnvelope.request;
const responseBuilder = handlerInput.responseBuilder;
let sessionAttributes = handlerInput.attributesManager.getSessionAttributes();
let slotValues = getSlotValues(request.intent.slots);
// getSlotValues returns .heardAs, .resolved, and .isValidated for each slot, according to request slot status codes ER_SUCCESS_MATCH, ER_SUCCESS_NO_MATCH, or traditional simple request slot without resolutions
let timesTable;
let say;
sessionAttributes.score = 0;
if(slotValues.number && slotValues.number.heardAs && !isNaN(slotValues.number.heardAs)) {
timesTable = parseInt(slotValues.number.heardAs);
sessionAttributes.timesTable = timesTable;
sessionAttributes.currentQuestion = generateQuestion(timesTable);
sessionAttributes.questionsAsked = 1;
say = `Ok let\'s practice our ${timesTable} times tables. What is ${sessionAttributes.currentQuestion.a} times ${sessionAttributes.currentQuestion.b}`
} else {
sessionAttributes.timesTable = undefined;
sessionAttributes.currentQuestion = generateQuestion();
sessionAttributes.questionsAsked = 1;
say = `Alrighty, let\'s start the quiz. What is ${sessionAttributes.currentQuestion.a} times ${sessionAttributes.currentQuestion.b}`
}
return responseBuilder
.speak(say)
.reprompt('try again, ' + say)
.getResponse();
},
};
const AnswerIntent_Handler = {
canHandle(handlerInput) {
const request = handlerInput.requestEnvelope.request;
return request.type === 'IntentRequest' && request.intent.name === 'AnswerIntent' ;
},
handle(handlerInput) {
const request = handlerInput.requestEnvelope.request;
const responseBuilder = handlerInput.responseBuilder;
let sessionAttributes = handlerInput.attributesManager.getSessionAttributes();
let say = '';
let message = '';
let answer;
let question = parseInt(sessionAttributes.currentQuestion.a) * parseInt(sessionAttributes.currentQuestion.b);
let slotValues = getSlotValues(request.intent.slots);
// getSlotValues returns .heardAs, .resolved, and .isValidated for each slot, according to request slot status codes ER_SUCCESS_MATCH, ER_SUCCESS_NO_MATCH, or traditional simple request slot without resolutions
let positive = [
'Great! ',
'Brilliant! ',
'Spot on! ',
'Well done! ',
'Amazing! ',
'Brill!',
'Are you cheating? '
];
let negative = [
'Nope. ',
'Aww, not quite. ',
'I\'m afraid that\'s the wrong answer. '
];
if(slotValues.answer && slotValues.answer.heardAs && !isNaN(slotValues.answer.heardAs)) {
answer = parseInt(slotValues.answer.heardAs);
if(answer === question) {
sessionAttributes.score = sessionAttributes.score + 1;
sessionAttributes.questionsAsked = sessionAttributes.questionsAsked + 1;
message = "<audio src='https://s3.amazonaws.com/ask-soundlibrary/ui/gameshow/amzn_ui_sfx_gameshow_positive_response_01.mp3'/>" + randomElement(positive);
} else {
sessionAttributes.score = sessionAttributes.score - 1;
message = "<audio src='https://s3.amazonaws.com/ask-soundlibrary/ui/gameshow/amzn_ui_sfx_gameshow_neutral_response_02.mp3'/>" + randomElement(negative) + "The answer was: " + question + ". ";
}
if(sessionAttributes.timesTable) {
sessionAttributes.currentQuestion = generateQuestion(sessionAttributes.timesTable);
say = `What is ${sessionAttributes.currentQuestion.a} times ${sessionAttributes.currentQuestion.b}`
} else {
sessionAttributes.currentQuestion = generateQuestion();
say = `What is ${sessionAttributes.currentQuestion.a} times ${sessionAttributes.currentQuestion.b}`
}
}
return responseBuilder
.speak(message + say)
.reprompt('try again, ' + say)
.getResponse();
},
};
const LaunchRequest_Handler = {
canHandle(handlerInput) {
const request = handlerInput.requestEnvelope.request;
return request.type === 'LaunchRequest';
},
handle(handlerInput) {
const responseBuilder = handlerInput.responseBuilder;
let say = 'Welcome to times tables! Is there a specific times tables you want to practice or do you want a general quiz?';
return responseBuilder
.speak(say)
.reprompt('try again, ' + say)
.getResponse();
},
};
const SessionEndedHandler = {
canHandle(handlerInput) {
const request = handlerInput.requestEnvelope.request;
return request.type === 'SessionEndedRequest';
},
handle(handlerInput) {
console.log(`Session ended with reason: ${handlerInput.requestEnvelope.request.reason}`);
return handlerInput.responseBuilder.getResponse();
}
};
const ErrorHandler = {
canHandle() {
return true;
},
handle(handlerInput, error) {
const request = handlerInput.requestEnvelope.request;
console.log(`Error handled: ${error.message}`);
// console.log(`Original Request was: ${JSON.stringify(request, null, 2)}`);
return handlerInput.responseBuilder
.speak(`Sorry, your skill got this error. ${error.message} `)
.reprompt(`Sorry, your skill got this error. ${error.message} `)
.getResponse();
}
};
function generateQuestion(timesTable) {
// if timesTable == 2
// we would generate: 2 x ...
// otherwise, we would generate ... x ... where ... is a random number;
if(timesTable) {
return {
a : timesTable,
b : _randomFrom1to12()
}
} else {
return {
a : _randomFrom1to12(),
b : _randomFrom1to12()
}
}
function _randomFrom1to12() {
return Math.floor(Math.random() * 12 + 1);
}
}
// 2. Constants ===========================================================================
// Here you can define static data, to be used elsewhere in your code. For example:
// const myString = "Hello World";
// const myArray = [ "orange", "grape", "strawberry" ];
// const myObject = { "city": "Boston", "state":"Massachusetts" };
const APP_ID = undefined; // TODO replace with your Skill ID (OPTIONAL).
// 3. Helper Functions ===================================================================
function capitalize(myString) {
return myString.replace(/(?:^|\s)\S/g, function(a) { return a.toUpperCase(); }) ;
}
function randomElement(myArray) {
return(myArray[Math.floor(Math.random() * myArray.length)]);
}
function stripSpeak(str) {
return(str.replace('<speak>', '').replace('</speak>', ''));
}
function getSlotValues(filledSlots) {
const slotValues = {};
Object.keys(filledSlots).forEach((item) => {
const name = filledSlots[item].name;
if (filledSlots[item] &&
filledSlots[item].resolutions &&
filledSlots[item].resolutions.resolutionsPerAuthority[0] &&
filledSlots[item].resolutions.resolutionsPerAuthority[0].status &&
filledSlots[item].resolutions.resolutionsPerAuthority[0].status.code) {
switch (filledSlots[item].resolutions.resolutionsPerAuthority[0].status.code) {
case 'ER_SUCCESS_MATCH':
slotValues[name] = {
heardAs: filledSlots[item].value,
resolved: filledSlots[item].resolutions.resolutionsPerAuthority[0].values[0].value.name,
ERstatus: 'ER_SUCCESS_MATCH'
};
break;
case 'ER_SUCCESS_NO_MATCH':
slotValues[name] = {
heardAs: filledSlots[item].value,
resolved: '',
ERstatus: 'ER_SUCCESS_NO_MATCH'
};
break;
default:
break;
}
} else {
slotValues[name] = {
heardAs: filledSlots[item].value || '', // may be null
resolved: '',
ERstatus: ''
};
}
}, this);
return slotValues;
}
function getExampleSlotValues(intentName, slotName) {
let examples = [];
let slotType = '';
let slotValuesFull = [];
let intents = model.interactionModel.languageModel.intents;
for (let i = 0; i < intents.length; i++) {
if (intents[i].name == intentName) {
let slots = intents[i].slots;
for (let j = 0; j < slots.length; j++) {
if (slots[j].name === slotName) {
slotType = slots[j].type;
}
}
}
}
let types = model.interactionModel.languageModel.types;
for (let i = 0; i < types.length; i++) {
if (types[i].name === slotType) {
slotValuesFull = types[i].values;
}
}
slotValuesFull = shuffleArray(slotValuesFull);
examples.push(slotValuesFull[0].name.value);
examples.push(slotValuesFull[1].name.value);
if (slotValuesFull.length > 2) {
examples.push(slotValuesFull[2].name.value);
}
return examples;
}
function sayArray(myData, penultimateWord = 'and') {
let result = '';
myData.forEach(function(element, index, arr) {
if (index === 0) {
result = element;
} else if (index === myData.length - 1) {
result += ` ${penultimateWord} ${element}`;
} else {
result += `, ${element}`;
}
});
return result;
}
function supportsDisplay(handlerInput) // returns true if the skill is running on a device with a display (Echo Show, Echo Spot, etc.)
{ // Enable your skill for display as shown here: https://alexa.design/enabledisplay
const hasDisplay =
handlerInput.requestEnvelope.context &&
handlerInput.requestEnvelope.context.System &&
handlerInput.requestEnvelope.context.System.device &&
handlerInput.requestEnvelope.context.System.device.supportedInterfaces &&
handlerInput.requestEnvelope.context.System.device.supportedInterfaces.Display;
return hasDisplay;
}
const welcomeCardImg = {
smallImageUrl: "https://s3.amazonaws.com/skill-images-789/cards/card_plane720_480.png",
largeImageUrl: "https://s3.amazonaws.com/skill-images-789/cards/card_plane1200_800.png"
};
const DisplayImg1 = {
title: 'Jet Plane',
url: 'https://s3.amazonaws.com/skill-images-789/display/plane340_340.png'
};
const DisplayImg2 = {
title: 'Starry Sky',
url: 'https://s3.amazonaws.com/skill-images-789/display/background1024_600.png'
};
function getCustomIntents() {
const modelIntents = model.interactionModel.languageModel.intents;
let customIntents = [];
for (let i = 0; i < modelIntents.length; i++) {
if(modelIntents[i].name.substring(0,7) != "AMAZON." && modelIntents[i].name !== "LaunchRequest" ) {
customIntents.push(modelIntents[i]);
}
}
return customIntents;
}
function getSampleUtterance(intent) {
return randomElement(intent.samples);
}
function getPreviousIntent(attrs) {
if (attrs.history && attrs.history.length > 1) {
return attrs.history[attrs.history.length - 2].IntentRequest;
} else {
return false;
}
}
function getPreviousSpeechOutput(attrs) {
if (attrs.lastSpeechOutput && attrs.history.length > 1) {
return attrs.lastSpeechOutput;
} else {
return false;
}
}
function timeDelta(t1, t2) {
const dt1 = new Date(t1);
const dt2 = new Date(t2);
const timeSpanMS = dt2.getTime() - dt1.getTime();
const span = {
"timeSpanMIN": Math.floor(timeSpanMS / (1000 * 60 )),
"timeSpanHR": Math.floor(timeSpanMS / (1000 * 60 * 60)),
"timeSpanDAY": Math.floor(timeSpanMS / (1000 * 60 * 60 * 24)),
"timeSpanDesc" : ""
};
if (span.timeSpanHR < 2) {
span.timeSpanDesc = span.timeSpanMIN + " minutes";
} else if (span.timeSpanDAY < 2) {
span.timeSpanDesc = span.timeSpanHR + " hours";
} else {
span.timeSpanDesc = span.timeSpanDAY + " days";
}
return span;
}
const InitMemoryAttributesInterceptor = {
process(handlerInput) {
let sessionAttributes = {};
if(handlerInput.requestEnvelope.session['new']) {
sessionAttributes = handlerInput.attributesManager.getSessionAttributes();
let memoryAttributes = getMemoryAttributes();
if(Object.keys(sessionAttributes).length === 0) {
Object.keys(memoryAttributes).forEach(function(key) { // initialize all attributes from global list
sessionAttributes[key] = memoryAttributes[key];
});
}
handlerInput.attributesManager.setSessionAttributes(sessionAttributes);
}
}
};
const RequestHistoryInterceptor = {
process(handlerInput) {
const thisRequest = handlerInput.requestEnvelope.request;
let sessionAttributes = handlerInput.attributesManager.getSessionAttributes();
let history = sessionAttributes['history'] || [];
let IntentRequest = {};
if (thisRequest.type === 'IntentRequest' ) {
let slots = [];
IntentRequest = {
'IntentRequest' : thisRequest.intent.name
};
if (thisRequest.intent.slots) {
for (let slot in thisRequest.intent.slots) {
let slotObj = {};
slotObj[slot] = thisRequest.intent.slots[slot].value;
slots.push(slotObj);
}
IntentRequest = {
'IntentRequest' : thisRequest.intent.name,
'slots' : slots
};
}
} else {
IntentRequest = {'IntentRequest' : thisRequest.type};
}
if(history.length > maxHistorySize - 1) {
history.shift();
}
history.push(IntentRequest);
handlerInput.attributesManager.setSessionAttributes(sessionAttributes);
}
};
const RequestPersistenceInterceptor = {
process(handlerInput) {
if(handlerInput.requestEnvelope.session['new']) {
return new Promise((resolve, reject) => {
handlerInput.attributesManager.getPersistentAttributes()
.then((sessionAttributes) => {
sessionAttributes = sessionAttributes || {};
sessionAttributes['launchCount'] += 1;
handlerInput.attributesManager.setSessionAttributes(sessionAttributes);
handlerInput.attributesManager.savePersistentAttributes()
.then(() => {
resolve();
})
.catch((err) => {
reject(err);
});
});
});
} // end session['new']
}
};
const ResponseRecordSpeechOutputInterceptor = {
process(handlerInput, responseOutput) {
let sessionAttributes = handlerInput.attributesManager.getSessionAttributes();
let lastSpeechOutput = {
"outputSpeech":responseOutput.outputSpeech.ssml,
"reprompt":responseOutput.reprompt.outputSpeech.ssml
};
sessionAttributes['lastSpeechOutput'] = lastSpeechOutput;
handlerInput.attributesManager.setSessionAttributes(sessionAttributes);
}
};
const ResponsePersistenceInterceptor = {
process(handlerInput, responseOutput) {
const ses = (typeof responseOutput.shouldEndSession == "undefined" ? true : responseOutput.shouldEndSession);
if(ses || handlerInput.requestEnvelope.request.type == 'SessionEndedRequest') { // skill was stopped or timed out
let sessionAttributes = handlerInput.attributesManager.getSessionAttributes();
sessionAttributes['lastUseTimestamp'] = new Date(handlerInput.requestEnvelope.request.timestamp).getTime();
handlerInput.attributesManager.setPersistentAttributes(sessionAttributes);
return new Promise((resolve, reject) => {
handlerInput.attributesManager.savePersistentAttributes()
.then(() => {
resolve();
})
.catch((err) => {
reject(err);
});
});
}
}
};
function shuffleArray(array) { // Fisher Yates shuffle!
let currentIndex = array.length, temporaryValue, randomIndex;
while (0 !== currentIndex) {
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex -= 1;
temporaryValue = array[currentIndex];
array[currentIndex] = array[randomIndex];
array[randomIndex] = temporaryValue;
}
return array;
}
// 4. Exports handler function and setup ===================================================
const skillBuilder = Alexa.SkillBuilders.standard();
exports.handler = skillBuilder
.addRequestHandlers(
AMAZON_FallbackIntent_Handler,
AMAZON_CancelIntent_Handler,
AMAZON_HelpIntent_Handler,
AMAZON_StopIntent_Handler,
AMAZON_NavigateHomeIntent_Handler,
ChooseGameTypeIntent_Handler,
AnswerIntent_Handler,
LaunchRequest_Handler,
SessionEndedHandler
)
.addErrorHandlers(ErrorHandler)
.lambda();
// End of Skill code -------------------------------------------------------------
// Static Language Model for reference
const model = {
"interactionModel": {
"languageModel": {
"invocationName": "multiplication test",
"intents": [
{
"name": "AMAZON.FallbackIntent",
"samples": []
},
{
"name": "AMAZON.CancelIntent",
"samples": []
},
{
"name": "AMAZON.HelpIntent",
"samples": []
},
{
"name": "AMAZON.StopIntent",
"samples": []
},
{
"name": "AMAZON.NavigateHomeIntent",
"samples": []
},
{
"name": "ChooseGameTypeIntent",
"slots": [
{
"name": "number",
"type": "AMAZON.NUMBER"
}
],
"samples": [
"{number} times tables",
"i want to practice my {number} times tables",
"i want a quiz"
]
},
{
"name": "AnswerIntent",
"slots": [
{
"name": "answer",
"type": "AMAZON.NUMBER"
}
],
"samples": [
"i don't know is it {answer}",
"how about {answer}",
"is it {answer}",
"i think the answer is {answer}",
"{answer}"
]
},
{
"name": "LaunchRequest"
}
],
"types": []
}
}
};
{
"interactionModel": {
"languageModel": {
"invocationName": "multiplication test",
"intents": [
{
"name": "AMAZON.FallbackIntent",
"samples": []
},
{
"name": "AMAZON.CancelIntent",
"samples": []
},
{
"name": "AMAZON.HelpIntent",
"samples": []
},
{
"name": "AMAZON.StopIntent",
"samples": []
},
{
"name": "AMAZON.NavigateHomeIntent",
"samples": []
},
{
"name": "ChooseGameTypeIntent",
"slots": [
{
"name": "number",
"type": "AMAZON.NUMBER"
}
],
"samples": [
"practice my {number} times tables",
"lets practice my {number} times tables",
"{number} times tables",
"i want to practice my {number} times tables",
"i want a quiz"
]
},
{
"name": "AnswerIntent",
"slots": [
{
"name": "answer",
"type": "AMAZON.NUMBER"
}
],
"samples": [
"i don't know is it {answer}",
"how about {answer}",
"is it {answer}",
"i think the answer is {answer}",
"{answer}"
]
}
],
"types": []
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment