Alexa-skills-kit-sdk-for-nodejs: Provide sample code for Attribute Persistence in ASK v2

Created on 25 Apr 2018  路  18Comments  路  Source: alexa/alexa-skills-kit-sdk-for-nodejs

I'm submitting a...


[ ] Regression (a behavior that used to work and stopped working in a new release)
[ ] Bug report  
[ ] Performance issue
[ ] Feature request
[x] Documentation issue or request
[ ] Other... Please describe:

Expected Behavior



Please provide some sample code or modify the current samples to include using Persistence Adapters in the new ASK v2

Current Behavior




Possible Solution

// Not required, but suggest a fix/reason for the bug,
// or ideas how to implement the addition or change

Steps to Reproduce (for bugs)

// Provide a self-contained, concise snippet of code
// For more complex issues provide a repo with the smallest sample that reproduces the bug
// Including business logic or unrelated code makes diagnosis more difficult

Context



I've not been able to implement persistence to DynamoDB using either the Standard or Custom Alexa builders. The code samples are great but currently don't show the use of this feature. I won't be able to move to ASK v2 until I understand this better.

Your Environment

  • ASK SDK for Node.js used: x.x.x
  • Operating System and version:

Node.js and NPM Info

  • Node.js version used for development:
  • NPM version used for development:

Most helpful comment

@hoellein I had a hard time getting it work and found timing was a big factor. Make sure you aren't calling the example code too early. I only grab from Dynamo at skill launch with an interceptor and then use session from there. I used this setup to get it all working:

'use strict';

// Use the ASK SDK for v2
const Alexa = require('ask-sdk');

// Define the skill features
let skill;

/**
 * If this is the first start of the skill, grab the user's data from Dynamo and 
 * set the session attributes to the persistent data. 
 */
const GetUserDataInterceptor = {
    process(handlerInput) {
        let attributes = handlerInput.attributesManager.getSessionAttributes();
        if (handlerInput.requestEnvelope.request.type === 'LaunchRequest' && !attributes['isInitialized']) {
            return new Promise((resolve, reject) => {
                handlerInput.attributesManager.getPersistentAttributes()
                    .then((attributes) => {
                        attributes['isInitialized'] = true;
                        saveUser(handlerInput, attributes, 'session');
                        resolve();
                    })
                    .catch((error) => {
                        reject(error);
                    })
            });
        }
    }
};

function saveUser(handlerInput, attributes, mode) {
        if(mode === 'session'){
            handlerInput.attributesManager.setSessionAttributes(attributes);
        } else if(mode === 'persistent') {
            console.info("Saving to Dynamo: ",attributes);
            return new Promise((resolve, reject) => {
                handlerInput.attributesManager.getPersistentAttributes()
                    .then((persistent) => {
                        delete attributes['isInitialized'];
                        handlerInput.attributesManager.setPersistentAttributes(attributes);

                        resolve(handlerInput.attributesManager.savePersistentAttributes());
                    })
                    .catch((error) => {
                            reject(error);
                    });
            });
        }
   }

 const LaunchHandler = {
        canHandle(handlerInput) {
            return handlerInput.requestEnvelope.request.type === 'LaunchRequest';

        },
        handle(handlerInput) {
            console.info("LaunchRequest");
            let attributes = handlerInput.attributesManager.getSessionAttributes();
            console.info("Test the load: " + attributes['isInitialized']);

            attributes['FOO'] = "BAR";
            saveUser(handlerInput, attributes, 'persistent');

            return handlerInput.responseBuilder
                .speak('Hello')
                .reprompt('Hello')
                .getResponse();
        }
    }

exports.handler = Alexa.SkillBuilders.standard()
    .withSkillId(APPID)
    .addRequestHandlers(
        LaunchHandler
    )
    .addRequestInterceptors(GetUserDataInterceptor)
    .withTableName('SkillUsers')
    .withAutoCreateTable(true)
    .withDynamoDbClient()
    .lambda();

All 18 comments

Looking for the same. I tried out the example from https://github.com/alexa/alexa-skills-kit-sdk-for-nodejs/wiki/Request-Processing#request-and-response-interceptors but that doesn't work.

@hoellein I had a hard time getting it work and found timing was a big factor. Make sure you aren't calling the example code too early. I only grab from Dynamo at skill launch with an interceptor and then use session from there. I used this setup to get it all working:

'use strict';

// Use the ASK SDK for v2
const Alexa = require('ask-sdk');

// Define the skill features
let skill;

/**
 * If this is the first start of the skill, grab the user's data from Dynamo and 
 * set the session attributes to the persistent data. 
 */
const GetUserDataInterceptor = {
    process(handlerInput) {
        let attributes = handlerInput.attributesManager.getSessionAttributes();
        if (handlerInput.requestEnvelope.request.type === 'LaunchRequest' && !attributes['isInitialized']) {
            return new Promise((resolve, reject) => {
                handlerInput.attributesManager.getPersistentAttributes()
                    .then((attributes) => {
                        attributes['isInitialized'] = true;
                        saveUser(handlerInput, attributes, 'session');
                        resolve();
                    })
                    .catch((error) => {
                        reject(error);
                    })
            });
        }
    }
};

function saveUser(handlerInput, attributes, mode) {
        if(mode === 'session'){
            handlerInput.attributesManager.setSessionAttributes(attributes);
        } else if(mode === 'persistent') {
            console.info("Saving to Dynamo: ",attributes);
            return new Promise((resolve, reject) => {
                handlerInput.attributesManager.getPersistentAttributes()
                    .then((persistent) => {
                        delete attributes['isInitialized'];
                        handlerInput.attributesManager.setPersistentAttributes(attributes);

                        resolve(handlerInput.attributesManager.savePersistentAttributes());
                    })
                    .catch((error) => {
                            reject(error);
                    });
            });
        }
   }

 const LaunchHandler = {
        canHandle(handlerInput) {
            return handlerInput.requestEnvelope.request.type === 'LaunchRequest';

        },
        handle(handlerInput) {
            console.info("LaunchRequest");
            let attributes = handlerInput.attributesManager.getSessionAttributes();
            console.info("Test the load: " + attributes['isInitialized']);

            attributes['FOO'] = "BAR";
            saveUser(handlerInput, attributes, 'persistent');

            return handlerInput.responseBuilder
                .speak('Hello')
                .reprompt('Hello')
                .getResponse();
        }
    }

exports.handler = Alexa.SkillBuilders.standard()
    .withSkillId(APPID)
    .addRequestHandlers(
        LaunchHandler
    )
    .addRequestInterceptors(GetUserDataInterceptor)
    .withTableName('SkillUsers')
    .withAutoCreateTable(true)
    .withDynamoDbClient()
    .lambda();

@sharpstef Thanks for the response, I'll check this out. I didn't think an interceptor would be required for this, so that's pretty clever. I'm also interested in using the custom builder and a PersistenceAdapter so maybe the ASK team will add another sample using that method (please?).

I agree that there are examples missing here.
For now I went with dynasty as a Dynamo DB ORM. This gives you quite some freedom in how and when to interact with the DB and works straight out of the box. Assuming you host the skill on Lambda and the Lambda Execution Role includes DynamoDB, there is no config needed.

I am also looking for an example this, is there a way to do it with out an interceptor?

@error404notfound Yes, as I mentioned using dynasty.

I wrote a quick and dirty example here on how I use it.

const Alexa   = require('ask-sdk-core');
const dynasty   = require('dynasty')({ region: 'eu-west-1' });

const TABLE_NAME = 'name_of_the_dynamoDB_table';

const dbhelpers = {

  getStoredStep(userId) {
    return dynasty.table(TABLE_NAME)
      .find(userId)
      .then((result) => {
        return result;
      })
      .catch((e) => {
        console.log(`error in read in helper: ${e}`);
        return e;
      });
  },

  storeStep(userId, step, someOtherData) {
    const toStore = {
      userId,
      step,
      someOtherData,
    };
    return dynasty.table(TABLE_NAME)
      .insert(toStore)
      .catch((e) => {
        console.log(`error while saving: ${e}`);
        return e;
      });
  },

  updateStep(userId, dataToUpdate) {
    return dynasty.table(TABLE_NAME)
      .update(
        userId,
        dataToUpdate,
      )
      .catch((e) => {
        console.log(`error while updating: ${e}`);
        return e;
      });
  },
};

const LaunchRequestHandler = {
  canHandle(handlerInput) {
    return handlerInput.requestEnvelope.request.type === 'LaunchRequest';
  },
  handle(handlerInput) {
    const { userId } = handlerInput.requestEnvelope.session.user;
    return dbhelpers.getSavedStep(userId)
      .then((step) => {

        if (!step) {
          // New User, we don't have anything stored for him
          // Lets store something about him
          return dbhelpers.storeStep(userId, 0, null)
            .then(() => {
              const speechText = 'Welcome new user!';
              return handlerInput.responseBuilder
                .speak(speechText)
                .reprompt(speechText)
                .withShouldEndSession(false)
                .getResponse();
            })
            .catch((e) => {
              console.log(e);
            });
        }
        // We already stored something about this user
        const speechText = `I can see you already started, you are at step ${step}`;
        return handlerInput.responseBuilder
          .speak(speechText)
          .reprompt(speechText)
          .withShouldEndSession(false)
          .getResponse();
      })
      .catch((e) => {
        console.log(e);
      });
  },
};

Rather different to just adding one line of code in the "old" SDK !

@hoellein You could implement your customized persistenceAdapter with getAttributes() and saveAttributes() methods, and add it in the custom skill builder in ask-sdk-core package by withPersistenceAdapter() method there.

thank you @warhost! sorry I see now where you said that before but thank you so much for taking the time to expalin it further! and thanks @hoellein for your example too.

@error404notfound I can't take credit, you probably mean @sharpstef .

eep! thanks for the heads up @hoellein, @sharpstef thank you for the example!

Hi all,

Thanks @sharpstef for the great example!

And to answer the question from @error404notfound, you don't have to use interceptor. the AttributesManager is available in the handlerInput all the time. You can use the following line to retrieve attributes from DB.

handlerInput.attributesManager.getPersistentAttributes().then((attributes) => {...});

Please note that SDK uses lazy loading to save unnecessary roundtrip to DB in order to improve performance. So only the first time getPersistentAttributes() is called for the single request handler cycle, a DB call will be made. The retrieved attributes is then cached locally. You can use setPersistentAttributes to overwrite the cached value. In order to save attributes to DB, you can use the following line:

handlerInput.attributesManager.savePersistentAttributes().then(() => {...});

If you turned on the auto create table in StandardSkillBuilder, it will take some time for the table to be created after the first DB call is initiated, which will cause the subsequent db call to fail within a short period of time. After the DB table is setup, the error should go away.

Regards

It would still be helpful to see some sample code from the Alexa team for the Custom builder, since that's most used in the current samples. withAutoCreateTable has not worked in the Standard builder, using the same roles as used in ASK v1.

Hi all,

The newly released High Low Game sample shows how to use the persistent attributes with v2 SDK. Please check them out here.

Regards,

Although it is a bit late, I was also facing the same issue and in the end, I got the solution from this tut:

http://whatdidilearn.info/2018/09/16/how-to-keep-state-between-sessions-in-alexa-skill.html

Although it is a bit late, I was also facing the same issue and in the end, I got the solution from this tut:

http://whatdidilearn.info/2018/09/16/how-to-keep-state-between-sessions-in-alexa-skill.html

This has since been removed for anyone who finds this thread whille looking for documentation on persisting data on Alexa.

@hoellein You could implement your customized persistenceAdapter with getAttributes() and saveAttributes() methods, and add it in the custom skill builder in ask-sdk-core package by withPersistenceAdapter() method there.

@TianTXie , I don't suppose you have a custom implementation on an open project that I could look at, do you?

@hoellein I had a hard time getting it work and found timing was a big factor. Make sure you aren't calling the example code too early. I only grab from Dynamo at skill launch with an interceptor and then use session from there. I used this setup to get it all working:

'use strict';

// Use the ASK SDK for v2
const Alexa = require('ask-sdk');

// Define the skill features
let skill;

/**
 * If this is the first start of the skill, grab the user's data from Dynamo and 
 * set the session attributes to the persistent data. 
 */
const GetUserDataInterceptor = {
    process(handlerInput) {
        let attributes = handlerInput.attributesManager.getSessionAttributes();
        if (handlerInput.requestEnvelope.request.type === 'LaunchRequest' && !attributes['isInitialized']) {
            return new Promise((resolve, reject) => {
                handlerInput.attributesManager.getPersistentAttributes()
                    .then((attributes) => {
                        attributes['isInitialized'] = true;
                        saveUser(handlerInput, attributes, 'session');
                        resolve();
                    })
                    .catch((error) => {
                        reject(error);
                    })
            });
        }
    }
};

function saveUser(handlerInput, attributes, mode) {
        if(mode === 'session'){
            handlerInput.attributesManager.setSessionAttributes(attributes);
        } else if(mode === 'persistent') {
            console.info("Saving to Dynamo: ",attributes);
            return new Promise((resolve, reject) => {
                handlerInput.attributesManager.getPersistentAttributes()
                    .then((persistent) => {
                        delete attributes['isInitialized'];
                        handlerInput.attributesManager.setPersistentAttributes(attributes);

                        resolve(handlerInput.attributesManager.savePersistentAttributes());
                    })
                    .catch((error) => {
                            reject(error);
                    });
            });
        }
   }

 const LaunchHandler = {
        canHandle(handlerInput) {
            return handlerInput.requestEnvelope.request.type === 'LaunchRequest';

        },
        handle(handlerInput) {
            console.info("LaunchRequest");
            let attributes = handlerInput.attributesManager.getSessionAttributes();
            console.info("Test the load: " + attributes['isInitialized']);

            attributes['FOO'] = "BAR";
            saveUser(handlerInput, attributes, 'persistent');

            return handlerInput.responseBuilder
                .speak('Hello')
                .reprompt('Hello')
                .getResponse();
        }
    }

exports.handler = Alexa.SkillBuilders.standard()
    .withSkillId(APPID)
    .addRequestHandlers(
        LaunchHandler
    )
    .addRequestInterceptors(GetUserDataInterceptor)
    .withTableName('SkillUsers')
    .withAutoCreateTable(true)
    .withDynamoDbClient()
    .lambda();

Thank you! This saved me 馃檶

Was this page helpful?
0 / 5 - 0 ratings

Related issues

oprog picture oprog  路  6Comments

sandeepajesh picture sandeepajesh  路  5Comments

z4o4z picture z4o4z  路  3Comments

dillonharlessNHRMC picture dillonharlessNHRMC  路  6Comments

developer170883 picture developer170883  路  4Comments