Skip to content

Instantly share code, notes, and snippets.

@germanviscuso
Last active January 31, 2023 15:40
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save germanviscuso/70c979f671660fea811ccfb63801f936 to your computer and use it in GitHub Desktop.
Save germanviscuso/70c979f671660fea811ccfb63801f936 to your computer and use it in GitHub Desktop.
Alexa Skill Basics: Global Persistence with the DynamoDB persistence adapter or the S3 Persistence adapter
{
"interactionModel": {
"languageModel": {
"invocationName": "global persistence demo",
"intents": [
{
"name": "AMAZON.CancelIntent",
"samples": []
},
{
"name": "AMAZON.HelpIntent",
"samples": []
},
{
"name": "AMAZON.StopIntent",
"samples": []
},
{
"name": "SetAttributeIntent",
"slots": [
{
"name": "key",
"type": "AMAZON.FOUR_DIGIT_NUMBER",
"samples": [
"it's {key}",
"the key is {key}",
"key {key}",
"okey {key}",
"all right {key}",
"{key}"
]
},
{
"name": "value",
"type": "AMAZON.Country",
"samples": [
"it's {value}",
"the country {value}",
"all right {value}",
"okey {value}",
"{value}"
]
}
],
"samples": [
"register {value} to {key}",
"register pair",
"set value {value}",
"value {value}",
"set key {key}",
"set a pair",
"set a value",
"assign {value} to {key}",
"match {key} with {value}",
"{key} equals {value}",
"set {value} for {key}",
"pair {key} {value}",
"key {key} value {value}",
"for {key} set {value}"
]
},
{
"name": "AMAZON.NavigateHomeIntent",
"samples": []
},
{
"name": "GetAttributeIntent",
"slots": [
{
"name": "key",
"type": "AMAZON.FOUR_DIGIT_NUMBER",
"samples": [
"it's {key}",
"the key is {key}",
"all right {key}",
"okey {key}",
"key {key}",
"{key}"
]
}
],
"samples": [
"find a value",
"get a value",
"fetch a value",
"tell me a value",
"give me a value",
"fetch value",
"find value",
"get value",
"query {key}",
"find {key}",
"give me {key}",
"code {key}",
"key {key}",
"fetch {key}",
"get {key}"
]
},
{
"name": "AMAZON.FallbackIntent",
"samples": []
},
{
"name": "DeleteAttributeIntent",
"slots": [
{
"name": "key",
"type": "AMAZON.FOUR_DIGIT_NUMBER",
"samples": [
"key {key}",
"the key is {key}",
"the digits are {key}",
"{key}"
]
}
],
"samples": [
"remove a value",
"delete a value",
"eliminate a value",
"discard a value",
"remove value",
"delete value",
"eliminate value",
"discard value",
"remove a key",
"delete a key",
"eliminate a key",
"discard a key",
"remove key",
"delete key",
"eliminate key",
"discard key",
"remove a pair",
"delete a pair",
"eliminate a pair",
"discard a pair",
"remove pair",
"delete pair",
"eliminate pair",
"discard pair",
"remove {key}",
"delete {key}",
"eliminate {key}",
"discard {key}",
"remove key {key}",
"delete key {key}",
"eliminate key {key}",
"discard key {key}",
"remove the key {key}",
"delete the key {key}",
"eliminate the key {key}",
"discard the key {key}"
]
}
],
"types": []
},
"dialog": {
"intents": [
{
"name": "SetAttributeIntent",
"confirmationRequired": true,
"prompts": {
"confirmation": "Confirm.Intent.22881561416"
},
"slots": [
{
"name": "key",
"type": "AMAZON.FOUR_DIGIT_NUMBER",
"confirmationRequired": false,
"elicitationRequired": true,
"prompts": {
"elicitation": "Elicit.Slot.22881561416.1546464456715"
},
"validations": [
{
"type": "isGreaterThanOrEqualTo",
"prompt": "Slot.Validation.461573702210.890266783786.745888534020",
"value": "0"
},
{
"type": "isLessThanOrEqualTo",
"prompt": "Slot.Validation.461573702210.890266783786.128131521422",
"value": "9999"
}
]
},
{
"name": "value",
"type": "AMAZON.Country",
"confirmationRequired": false,
"elicitationRequired": true,
"prompts": {
"elicitation": "Elicit.Slot.22881561416.103485729333"
}
}
]
},
{
"name": "GetAttributeIntent",
"confirmationRequired": false,
"prompts": {},
"slots": [
{
"name": "key",
"type": "AMAZON.FOUR_DIGIT_NUMBER",
"confirmationRequired": false,
"elicitationRequired": true,
"prompts": {
"elicitation": "Elicit.Slot.554346980114.1513789155758"
},
"validations": [
{
"type": "isGreaterThanOrEqualTo",
"prompt": "Slot.Validation.554346980114.1513789155758.784106537043",
"value": "0"
},
{
"type": "isLessThanOrEqualTo",
"prompt": "Slot.Validation.554346980114.1513789155758.1548591292593",
"value": "9999"
}
]
}
]
},
{
"name": "DeleteAttributeIntent",
"confirmationRequired": false,
"prompts": {},
"slots": [
{
"name": "key",
"type": "AMAZON.FOUR_DIGIT_NUMBER",
"confirmationRequired": true,
"elicitationRequired": true,
"prompts": {
"confirmation": "Confirm.Slot.814228627186.471212978356",
"elicitation": "Elicit.Slot.814228627186.471212978356"
}
}
]
}
],
"delegationStrategy": "ALWAYS"
},
"prompts": [
{
"id": "Elicit.Slot.22881561416.1546464456715",
"variations": [
{
"type": "PlainText",
"value": "Value set as {value} . Now please tell me a four digit sequence which will be used as key"
},
{
"type": "PlainText",
"value": "Please tell me a four digit sequence which I will use as key for the pair"
}
]
},
{
"id": "Elicit.Slot.22881561416.103485729333",
"variations": [
{
"type": "PlainText",
"value": "Key set as {key} . Now please tell me a country which will be used as value"
},
{
"type": "PlainText",
"value": "Please tell me a country which I will use as value for the pair"
}
]
},
{
"id": "Confirm.Intent.22881561416",
"variations": [
{
"type": "PlainText",
"value": "The key {key} will be assigned the value {value} . Do you confirm?"
},
{
"type": "PlainText",
"value": "Ok. I will assign the value {value} to the key {key} . Is that all right?"
}
]
},
{
"id": "Slot.Validation.461573702210.890266783786.745888534020",
"variations": [
{
"type": "PlainText",
"value": "Please say four integer and positive digits, one by one"
}
]
},
{
"id": "Slot.Validation.461573702210.890266783786.128131521422",
"variations": [
{
"type": "PlainText",
"value": "Please say four integer and positive digits, one by one"
}
]
},
{
"id": "Elicit.Slot.554346980114.1513789155758",
"variations": [
{
"type": "PlainText",
"value": "Please tell me the four digit key and I will tell you the assigned value"
}
]
},
{
"id": "Slot.Validation.554346980114.1513789155758.784106537043",
"variations": [
{
"type": "PlainText",
"value": "Please say four positive digits in sequence"
}
]
},
{
"id": "Slot.Validation.554346980114.1513789155758.1548591292593",
"variations": [
{
"type": "PlainText",
"value": "Please say four positive digits in sequence"
}
]
},
{
"id": "Elicit.Slot.814228627186.471212978356",
"variations": [
{
"type": "PlainText",
"value": "Okey. Please tell me the four digit key of the element you want to remove"
}
]
},
{
"id": "Confirm.Slot.814228627186.471212978356",
"variations": [
{
"type": "PlainText",
"value": "Are you sure you want to eliminate item {key} ?"
}
]
}
]
}
}
/* eslint-disable func-names */
/* eslint-disable no-console */
const Alexa = require('ask-sdk');
var persistenceAdapter;
/*
This demo will use S3 based persistence if this is an Alexa-Hosted skill and DynamoDB otherwise.
If you're using Alexa Hosted Skills (Beta) you can't use DynamoDB and have to switch to S3 based persistence.
If you want to use the S3 adapter in a self hosted skill you'll have to provide a bucket name
(not take it from process.env) and modify the lambda execution role to give proper access to your S3 service
More info:
https://developer.amazon.com/docs/hosted-skills/build-a-skill-end-to-end-using-an-alexa-hosted-skill.html
https://ask-sdk-for-nodejs.readthedocs.io/en/latest/Managing-Attributes.html
*/
if(isAlexaHosted()) {
const {S3PersistenceAdapter} = require('ask-sdk-s3-persistence-adapter');
persistenceAdapter = new S3PersistenceAdapter({
bucketName: process.env.S3_PERSISTENCE_BUCKET,
objectKeyGenerator: keyGenerator
});
} else {
// IMPORTANT: don't forget to give DynamoDB access to the role you're to run this lambda (IAM)
const {DynamoDbPersistenceAdapter} = require('ask-sdk-dynamodb-persistence-adapter');
persistenceAdapter = new DynamoDbPersistenceAdapter({
tableName: 'global_attr_table',
createTable: true,
partitionKeyGenerator: keyGenerator
});
}
// This function is an indirect way to detect if this is part of an Alexa-Hosted skill
function isAlexaHosted() {
return process.env.S3_PERSISTENCE_BUCKET ? true : false;
}
// This function establishes the primary key of the database as the skill id (hence you get global persistence, not per user id)
function keyGenerator(requestEnvelope) {
if (requestEnvelope
&& requestEnvelope.context
&& requestEnvelope.context.System
&& requestEnvelope.context.System.application
&& requestEnvelope.context.System.application.applicationId) {
return requestEnvelope.context.System.application.applicationId;
}
throw 'Cannot retrieve app id from request envelope!';
}
const LaunchRequestHandler = {
canHandle(handlerInput) {
return handlerInput.requestEnvelope.request.type === 'LaunchRequest';
},
handle(handlerInput) {
const speechText = 'Welcome to the global persistence demo. You can tell me a key value pair, with a four digit key and a country as value. For example you can say, assign Australia to <say-as interpret-as="digits">1234</say-as>. YOu can also just say, register pair, get value or delete key';
return handlerInput.responseBuilder
.speak(speechText)
.reprompt(speechText)
.getResponse();
},
};
const GetAttributeIntentHandler = {
canHandle(handlerInput) {
return handlerInput.requestEnvelope.request.type === 'IntentRequest'
&& handlerInput.requestEnvelope.request.intent.name === 'GetAttributeIntent';
},
handle(handlerInput) {
const {attributesManager} = handlerInput;
const request = handlerInput.requestEnvelope.request;
const intent = request.intent;
const slotValues = getSlotValues(intent.slots);
const key = slotValues["key"] ? slotValues["key"].value.padStart(4, '0') : null;
let speechText;
const attributes = attributesManager.getSessionAttributes();
if(key && attributes[key]) {
const value = attributes[key];
speechText = 'The value assigned to <say-as interpret-as="digits">' + key + '</say-as> is ' + value + '.';
} else {
speechText = 'Sorry, there\'s no value assigned to <say-as interpret-as="digits">' + key + '</say-as> yet.';
}
return handlerInput.responseBuilder
.speak(speechText + ' You can now continue querying, deleting or registering key value pairs')
.reprompt('you can say, find value, or, assign value to key, where key is a four digit number sequence and value is a valid country')
.getResponse();
}
}
const SetAttributeIntentHandler = {
canHandle(handlerInput) {
return handlerInput.requestEnvelope.request.type === 'IntentRequest'
&& handlerInput.requestEnvelope.request.intent.name === 'SetAttributeIntent';
},
handle(handlerInput) {
const {attributesManager} = handlerInput;
const request = handlerInput.requestEnvelope.request;
const intent = request.intent;
const slotValues = getSlotValues(intent.slots);
const key = slotValues["key"] ? slotValues["key"].value.padStart(4, '0') : null;
const value = slotValues["value"] ? slotValues["value"].value : null;
let speechText;
if(key && value && intent.confirmationStatus !== 'DENIED') {
let attributes = attributesManager.getSessionAttributes();
attributes[key] = value;
attributesManager.setSessionAttributes(attributes);
speechText = 'Key value pair <say-as interpret-as="digits">' + key + '</say-as> ' + value + ', registered and will be saved on exit.';
} else {
speechText = 'Ok. I won\'t register this pair.';
}
return handlerInput.responseBuilder
.speak(speechText + ' You can now continue querying, deleting or registering key value pairs')
.reprompt('you can say, find key, or assign value to key, where key is a four digit number sequence and value is a valid country')
.getResponse();
}
};
const DeleteAttributeIntentHandler = {
canHandle(handlerInput) {
return handlerInput.requestEnvelope.request.type === 'IntentRequest'
&& handlerInput.requestEnvelope.request.intent.name === 'DeleteAttributeIntent';
},
handle(handlerInput) {
const {attributesManager} = handlerInput;
const request = handlerInput.requestEnvelope.request;
const intent = request.intent;
const slotValues = getSlotValues(intent.slots);
const key = slotValues["key"] ? slotValues["key"].value.padStart(4, '0') : null;
let speechText;
let attributes = attributesManager.getSessionAttributes();
if(key && attributes[key] && intent.confirmationStatus !== 'DENIED') {
delete attributes[key];
attributesManager.setSessionAttributes(attributes);
speechText = 'Key value pair <say-as interpret-as="digits">' + key + '</say-as> has been deleted and the new status will be saved on exit.';
} else {
if(intent.confirmationStatus === 'DENIED')
speechText = 'Ok, let\'s cancel that. You can now continue querying, registering or deleting key value pairs';
else
speechText = 'I can\'t find an a pair for that key. Please try again saying, for example, delete <say-as interpret-as="digits">1234</say-as>';
}
return handlerInput.responseBuilder
.speak(speechText + ' You can now continue querying, registering or deleting key value pairs')
.reprompt('you can say, find value, delete key or, assign value to key, where key is a four digit number sequence and value is a valid country')
.getResponse();
}
}
const HelpIntentHandler = {
canHandle(handlerInput) {
return handlerInput.requestEnvelope.request.type === 'IntentRequest'
&& handlerInput.requestEnvelope.request.intent.name === 'AMAZON.HelpIntent';
},
handle(handlerInput) {
const speechText = 'You can tell me a key value pair, with a four digit key and a country as value. For example you can say, assign Australia to <say-as interpret-as="digits">1234</say-as>. Or you can just say register pair, get value or delete key';
return handlerInput.responseBuilder
.speak(speechText)
.reprompt(speechText)
.getResponse();
},
};
const CancelAndStopIntentHandler = {
canHandle(handlerInput) {
return handlerInput.requestEnvelope.request.type === 'IntentRequest'
&& (handlerInput.requestEnvelope.request.intent.name === 'AMAZON.CancelIntent'
|| handlerInput.requestEnvelope.request.intent.name === 'AMAZON.StopIntent');
},
handle(handlerInput) {
const speechText = 'Goodbye!';
return handlerInput.responseBuilder
.speak(speechText)
.getResponse();
},
};
const FallbackIntentHandler = {
canHandle(handlerInput) {
return handlerInput.requestEnvelope.request.type === 'IntentRequest'
&& handlerInput.requestEnvelope.request.intent.name === 'AMAZON.FallbackIntent';
},
handle(handlerInput) {
const speechText = 'I don\'t know that! Please try again or say help';
return handlerInput.responseBuilder
.speak(speechText)
.getResponse();
},
};
const SessionEndedRequestHandler = {
canHandle(handlerInput) {
return handlerInput.requestEnvelope.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) {
console.log(`Error handled: ${error}`);
return handlerInput.responseBuilder
.speak('Sorry,there was an error. Please say again.')
.reprompt('Sorry, there was an error. Please say again.')
.getResponse();
},
};
// This request interceptor with each new session loads all global persistent attributes
// into the session attributes and increments a launch counter
const PersistenceRequestInterceptor = {
process(handlerInput) {
if(handlerInput.requestEnvelope.session['new']) {
return new Promise((resolve, reject) => {
handlerInput.attributesManager.getPersistentAttributes()
.then((persistentAttributes) => {
persistentAttributes = persistentAttributes || {};
if(!persistentAttributes['launchCount'])
persistentAttributes['launchCount'] = 0;
persistentAttributes['launchCount'] += 1;
handlerInput.attributesManager.setSessionAttributes(persistentAttributes);
resolve();
})
.catch((err) => {
reject(err);
});
});
} // end session['new']
}
};
// This response interceptor stores all session attributes into global persistent attributes
// when the session ends and it stores the skill last used timestamp
const PersistenceResponseInterceptor = {
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 getSlotValues(filledSlots) {
const slotValues = {};
console.log(`The filled slots: ${JSON.stringify(filledSlots)}`);
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] = {
synonym: filledSlots[item].value,
value: filledSlots[item].resolutions.resolutionsPerAuthority[0].values[0].value.name,
id: filledSlots[item].resolutions.resolutionsPerAuthority[0].values[0].value.id,
isValidated: true,
canUnderstand: true,
canFulfill: true,
};
break;
case 'ER_SUCCESS_NO_MATCH':
slotValues[name] = {
synonym: filledSlots[item].value,
value: filledSlots[item].value,
id: null,
isValidated: false,
canUnderstand: false,
canFulfill: null,
};
break;
default:
break;
}
} else {
slotValues[name] = {
synonym: filledSlots[item].value,
value: filledSlots[item].value,
id: filledSlots[item].id,
isValidated: false,
canUnderstand: false,
canFulfill: false,
};
}
}, this);
return slotValues;
}
const skillBuilder = Alexa.SkillBuilders.custom();
exports.handler = skillBuilder
.addRequestHandlers(
LaunchRequestHandler,
GetAttributeIntentHandler,
SetAttributeIntentHandler,
DeleteAttributeIntentHandler,
HelpIntentHandler,
CancelAndStopIntentHandler,
FallbackIntentHandler,
SessionEndedRequestHandler
)
.addErrorHandlers(ErrorHandler)
.addRequestInterceptors(PersistenceRequestInterceptor)
.addResponseInterceptors(PersistenceResponseInterceptor)
.withPersistenceAdapter(persistenceAdapter)
.lambda();
{
"name": "global-persistence",
"version": "0.9.0",
"description": "alexa utility for quickly building skills",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Amazon Alexa",
"license": "ISC",
"dependencies": {
"ask-sdk": "^2.3.0",
"ask-sdk-s3-persistence-adapter": "^2.3.0",
"aws-sdk": "^2.326.0"
}
}
@giovanemachado
Copy link

I think that's the only place that talks about changing the key generator. I don't know if it's so obvious, but I spent some hours looking for a way to save my data in my own keys instead of user ids.

Thank you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment