Things used in this project

Schematics

Voice User Interface
A simple flowchart showing the iterations between the user and Alexa.
Voice User Interface - Continuation

Code

Amazon WS Lamba JS FileJavaScript
This is the full JS file for Lamba that runs the app.
It connects to an instance of Amazon Dynamo DB
'use strict';
console.log('Loading function');

let doc = require('dynamodb-doc');
let dynamo = new doc.DynamoDB();

var tableName = 'Grill_Pal';

exports.handler = function(event, context) {
    try {
        console.log("event.session.application.applicationId=" + event.session.application.applicationId);
        console.log('Calling App');

        if (event.session.new) {
            onSessionStarted({
                requestId: event.request.requestId
            }, event.session);
        }

        getUserData(event, context, onUserDataLoad);

    } catch (e) {
        context.fail("Exception: " + e);
    }
};

function onUserDataLoad(event, context, userData) {
        console.log('After User load: ' + event.request.type);
        if (event.request.type === "LaunchRequest") {
            onLaunch(userData, event.request, event.session, context);

        } else if (event.request.type === "IntentRequest") {
            onIntent(userData, event.request, event.session, context);

        } else if (event.request.type === "SessionEndedRequest") {
            onSessionEnded(event.request, event.session);
            context.succeed();
        }
}


function getUserData(event, context, callback) {
    // Check if the user ID has an entry on DB. If not, need to create new one. 
    // If exists, start a new barbecue.
    console.log('Get User Data = ' + event.session.user.userId);

    var params = {
        TableName: tableName,
        Key: { 
            userId: event.session.user.userId
        }
    };
    dynamo.getItem(params, function(err, data) {
        if (err) {
            console.log('Error GET Handler: ' + err);
            context.fail("Exception: " + err);
        } else {
            console.log('Got User Data' + JSON.stringify(data));
            if (!data.Item) {
                console.log('New User, creating data');
                data = {
                    Item: {
                        userId: event.session.user.userId,
                        barbecues: []
                    }
                }
            } else {
                console.log('Existing User');
            }

            callback(event, context, data);
        }
    });
}

function saveAndExit(userData, context, response) {
    console.log('Saving: ' + JSON.stringify(userData));
   var params = {
        TableName: tableName,
        Item: userData.Item
    };

    console.log('Saving: ' + JSON.stringify(params));

    dynamo.putItem(params, function(err, data) {
        if (err) {
            console.log('Error Save Handler: ' + err);
            context.fail("Exception: " + err);
        } else {
             context.succeed(response);
        }
    });
}


/**
 * 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(userData, launchRequest, session, context) {
    console.log("onLaunch requestId=" + launchRequest.requestId +
        ", sessionId=" + session.sessionId);

    var cardTitle = 'Grill Pal';
    var cardText = 'Welcome to Grill Pal. Your barbecue personal assistant. Please, use the following commands: ' +
        '\n  - "ask Grill Pal to start a barbecue" = It will clean Alexa grill and start a new barbecue and reset all timers. ' +
        '\n  - "ask Grill Pal to add a beef to the grill for 5 and 10 minutes" = Start the timers for turning in 5 minutes and serving in 10 minutes ' +
        '\n  - "ask Grill Pal to turn food" = I will stop the timer before turning and start the second timer ' +
        '\n  - "ask Grill Pal is the food ready?" = And I will tell you if it is time to turn or serve the food ' +
        '\n  - "ask Grill Pal serve the food" = I will stop the timer for this food ';
    var repromptText = "";
    var sessionAttributes = {};
    var shouldEndSession = true;
    var speechOutput = "Hello, and welcome to Alexa Grill Pal. I am your personal grill assistant. " +
     "My objective is to help controlling the time your food is on grill, so you can enjoy the barbecue with " +
     "your family and friends. You can ask me to count the timer for as many food you want and I will keep different timers for " +
     "each one of them for you.";

     var response = buildSpeechletResponse(cardTitle, cardText, speechOutput, repromptText, shouldEndSession, sessionAttributes);
     saveAndExit(userData, context, response);

}

/**
 * Called when the user specifies an intent for this skill.
 */
function onIntent(userData, intentRequest, session, context) {
    console.log("onIntent requestId=" + intentRequest.requestId +
        ", sessionId=" + session.sessionId);

    var intentName = intentRequest.intent.name;

    // Dispatch to your skill's intent handlers
    if ("StartBarbecue" === intentName) {
        startBarbecue(userData, intentRequest, session, context);
    } else if ("YesNoIntent" === intentName) {
        yesNo(userData, intentRequest, session, context);
    } else if ("AddFoodToGrill" === intentName) {
        addFood(userData, intentRequest, session, context);
    } else if ("CheckFood" === intentName) {
        checkFood(userData, intentRequest, session, context);
    } else if ("TurnFood" === intentName) {
        turnFood(userData, intentRequest, session, context);
    } else if ("ServeFood" === intentName) {
        serveFood(userData, intentRequest, session, context);
    } 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
}


function handleSessionEndRequest(callback) {
    var cardTitle = "Session Ended";
    var speechOutput = "Thank you for trying the Alexa Skills Kit sample. Have a nice day!";
    // Setting this to true ends the session and exits the skill.
    var shouldEndSession = true;

    callback({}, buildSpeechletResponse(cardTitle, speechOutput, null, shouldEndSession, {}));
}

// ---------------- INTENTS ---------------------------------
function serveFood(userData, intent, session, context) {
    console.log('Serve food' + JSON.stringify(intent));
    var food = intent.intent.slots.FoodType.value.toLowerCase();
    var ordinal = intent.intent.slots.Ordinal.value;
    if (!ordinal) ordinal = 'first';
    var number = getNumber(ordinal);
    var curDate = new Date(intent.timestamp);

    var cardTitle = 'Turn Food';
    var cardText = '';
    var repromptText = " " ;
    var sessionAttributes = {};
    var shouldEndSession = true;
    var speechOutput = "";

    var openBBQ = hasOpenBarbecue(userData);
    if(openBBQ) {
        var leng = userData.Item.barbecues.length -1;
        var foodList = userData.Item.barbecues[leng].foods;
        var selectFood;
        // If Ordinal is asked, return only that entry. If not, reply with all entries
        var i = 0;
        for (i = 0; i< foodList.length; i++) {
            if (foodList[i].type === food) {
                if (foodList[i].index === number) {
                    selectFood = foodList[i];
                    break;
                }
            }
        }
        var foodName = getOrdinal(selectFood.index) + " " + selectFood.type;

        if (selectFood.serveTime) {
            var past = getInMinutes(new Date(selectFood.turnTime), curDate);
            cardText = foodName + ' was served ' + getMinutesStr(past) + ' ago. ';
            speechOutput = "Reading my notes, I understand you already served " + foodName + " " + getMinutesStr(past) + " ago. " ;

        } else {
            // Not turned yet, so make it
            selectFood.serveTime = intent.timestamp;
            cardText =  foodName + ' timer stopped. Enjoy your meal';
            speechOutput = foodName + ' timer stopped. Enjoy your meal';
        }

    } else {
        console.log('No BBQ' );
        // In case there is no barbecue:
        cardText = 'There is no barbecue going on my records. Please, start a barbecue and add food to grill before asking for food timers.';
        speechOutput = "Hey, according to my records, you did not start a barbecue yet. Please say: Alexa, ask Grill Pal to start a barbecue.";
    }

    var response = buildSpeechletResponse(cardTitle, cardText, speechOutput, repromptText, shouldEndSession, sessionAttributes);
    saveAndExit(userData, context, response);

}

function turnFood(userData, intent, session, context) {
    console.log('Turn food' + JSON.stringify(intent));
    var food = intent.intent.slots.FoodType.value.toLowerCase();
    var ordinal = intent.intent.slots.Ordinal.value;
    if (!ordinal) ordinal = 'first';
    var number = getNumber(ordinal);
    var curDate = new Date(intent.timestamp);

    var cardTitle = 'Turn Food';
    var cardText = '';
    var repromptText = " " ;
    var sessionAttributes = {};
    var shouldEndSession = true;
    var speechOutput = "";

    var openBBQ = hasOpenBarbecue(userData);
    if(openBBQ) {
        var leng = userData.Item.barbecues.length -1;
        var foodList = userData.Item.barbecues[leng].foods;
        var selectFood;
        // If Ordinal is asked, return only that entry. If not, reply with all entries
        var i = 0;
        for (i = 0; i< foodList.length; i++) {
            if (foodList[i].type === food) {
                if (foodList[i].index === number) {
                    selectFood = foodList[i];
                    break;
                }
            }
        }
        var foodName = getOrdinal(selectFood.index) + " " + selectFood.type;

        if (selectFood.turnTime) {
            if (selectFood.serveTime) {
                cardText +=  'You already served ' + foodName + '. Please try again';
                speechOutput += 'Ups. As I can see you already served ' + foodName + '. Are you sure you chose the right food?';
            } else {
                var past = getInMinutes(new Date(selectFood.turnTime), curDate);
                cardText = foodName + ' was turned ' + getMinutesStr(past) + ' ago. ';
                speechOutput = "Reading my notes, I understand you turned " + foodName + " " + getMinutesStr(past) + " ago. " ;

                var toServe = selectFood.secondTimer - past;
                if (toServe > 0) {
                    cardText +=  ' It will be ready to serve in ' + getMinutesStr(toServe)+ '. When you do, just say: Alexa, ask Grill Pal to serve ' + foodName + ".";
                    speechOutput += "It will be ready to serve in " + getMinutesStr(toServe) + ". When you do, just say: Alexa, ask Grill Pal to serve " + foodName + ".";
                } else if (toServe >  -2) {
                    cardText +=  ' It is time to serve it. When you do, just say: Alexa, ask Grill Pal to serve ' + foodName + ".";
                    speechOutput += "Actually It is time to serve it. When you do, just say: Alexa, ask Grill Pal to serve " + foodName + ".";
                } else {
                    cardText +=  ' You had to serve it ' + getMinutesStr(-toServe) + ' ago. When you do, just say: Alexa, ask Grill Pal to serve ' + foodName + ".";
                    speechOutput += "Actually You had to serve it " + getMinutesStr(-toServe) + " ago. When you do, just say: Alexa, ask Grill Pal to serve " + foodName + ".";
                }

            }

        } else {
            // Not turned yet, so make it
            selectFood.turnTime = intent.timestamp;
            cardText =  foodName + ' turned. It will be ready to serve in  ' + getMinutesStr(selectFood.secondTimer) + '. When you do, just say: Alexa, ask Grill Pal to serve ' + foodName + ".";
            speechOutput = 'OK, done! As you asked before, you should be able to serve ' + foodName + ' in  ' + getMinutesStr(selectFood.secondTimer) + '. When you do, just say: Alexa, ask Grill Pal to serve ' + foodName + ".";
        }

    } else {
        console.log('No BBQ' );
        // In case there is no barbecue:
        cardText = 'There is no barbecue going on my records. Please, start a barbecue and add food to grill before asking for food timers.';
        speechOutput = "Hey, according to my records, you did not start a barbecue yet. Please say: Alexa, ask Grill Pal to start a barbecue.";
    }

    var response = buildSpeechletResponse(cardTitle, cardText, speechOutput, repromptText, shouldEndSession, sessionAttributes);
    saveAndExit(userData, context, response);

}


function checkFood(userData, intent, session, context) {
    console.log('Check food' + JSON.stringify(intent));
    var food = intent.intent.slots.FoodType.value.toLowerCase();
    var ordinal = intent.intent.slots.Ordinal.value;
    var number = getNumber(ordinal);

    var cardTitle = 'Check Food';
    var cardText = '';
    var repromptText = " " ;
    var sessionAttributes = {};
    var shouldEndSession = true;
    var speechOutput = "";

    var openBBQ = hasOpenBarbecue(userData);
    if(openBBQ) {
        var leng = userData.Item.barbecues.length -1;
        var foodList = userData.Item.barbecues[leng].foods;
        var selectFood = [];
        // If Ordinal is asked, return only that entry. If not, reply with all entries
        var i = 0;
        for (i = 0; i< foodList.length; i++) {
            if (foodList[i].type === food) {
                if (number > 0 ) {
                    if (foodList[i].index === number) {
                        selectFood.push(foodList[i]);
                    }
                } else selectFood.push(foodList[i]);
            }
        }
        console.log('Aqui ' + selectFood.length);
        var data = getResponseTextFromFood(selectFood, intent.timestamp);
        cardText = data.cardText;
        speechOutput = data.outputSpeech;


    } else {
        console.log('No BBQ' );
        // In case there is no barbecue:
        cardText = 'There is no barbecue going on my records. Please, start a barbecue and add food to grill before asking for food timers.';
        speechOutput = "Hey, according to my records, you did not start a barbecue yet. Please say: Alexa, ask Grill Pal to start a barbecue.";
    }

    var response = buildSpeechletResponse(cardTitle, cardText, speechOutput, repromptText, shouldEndSession, sessionAttributes);
    saveAndExit(userData, context, response);

}


function addFood(userData, intent, session, context) {
    console.log('Add food' + JSON.stringify(intent));

    var food = intent.intent.slots.FoodType.value.toLowerCase();
    var firstTimer = intent.intent.slots.FirstTimer.value;
    var secondTimer = intent.intent.slots.SecondTimer.value;

    var cardTitle = 'Add Food';
    var cardText = 'Adding ' + food + ' to Grill! I will keep the timers for ' + firstTimer + 
        ' before turning it, and ' + secondTimer + ' before serving it';
    var repromptText = " " ;
    var sessionAttributes = {};
    var shouldEndSession = true;
    var speechOutput = "Starting the timers for " + food + " now. When you " +
        "want to know if it is time to turn or serve, please just say: Alexa, ask Grill Pal how is the " + food;

    var startTimeOpenBB = hasOpenBarbecue(userData);
    if (!startTimeOpenBB) {
        console.log('No BBQ started, starting one now');
        // Has no barbecue running. Create one now
        var bbq = {
            startTime: intent.timestamp,
            foods: []
        }
        userData.Item.barbecues.push(bbq);
    }

    var leng = userData.Item.barbecues.length -1;

    var foodList = userData.Item.barbecues[leng].foods;
    var newFood = {
        type: food,
        index: 1,
        firstTimer: firstTimer,
        secondTimer: secondTimer,
        startTime: intent.timestamp
    }
    // If more than one entry of the same food is created, start using cardinals.
    var i = 0;
    for(i = foodList.length-1; i >= 0 ; i--) {
        console.log(i);
        if (foodList[i].type === food) {
            console.log('Food Found');

            newFood.index = foodList[i].index + 1;
            var ordinal = getOrdinal(newFood.index);
            speechOutput = "I see that you have another :" + food + " cooking already. Don't worry, I can keep track " +
                    "of them all. I will just start calling it " + ordinal + " " + food + " from now on. Starting the timers now. When you " +
                    "want to know if it is time to turn or serve, please just say: Alexa, ask Grill Pal how is the " + ordinal + " " + food;
            break;
        }
    }
    foodList.push(newFood);
    var response = buildSpeechletResponse(cardTitle, cardText, speechOutput, repromptText, shouldEndSession, sessionAttributes);
    saveAndExit(userData, context, response);


}


function startBarbecue(userData, intent, session, context) {
    console.log('Start Barbecue' + JSON.stringify(intent));
    var cardTitle = 'Start Barbecue';
    var cardText = 'A new barbecue is started. When you are ready, start adding the food!';
    var repromptText = "You can ask me things like add a new food to grill, turn a food, serve a food or how long one food is cooking.";
    var sessionAttributes = {};
    var shouldEndSession = true;
    var speechOutput = "Ok, I just started a new barbecue for you. When you are ready, please, start adding new food to the grill and " +
            "I will keep the time for your.";

    var startTimeOpenBB = hasOpenBarbecue(userData);

    if (startTimeOpenBB) {
        console.log('Start BB - Ask Yes or No');

        cardTitle = 'Start Barbecue';
        cardText = 'You already a barbecue. Do you want to start over (and reset all timers) or keep the existing one?';
        repromptText = "You can ask me things like add a new food to grill, turn a food, serve a food or how long one food is cooking.";
        sessionAttributes = {step: 'StartBarbecue'};
        shouldEndSession = false;
        speechOutput = "As I can see, you already started a barbecue at " + startTimeOpenBB + ". Do you want me to start a new barbecue and " +
         "forget all running timers? You can say Yes or No.";

        var response = buildSpeechletResponse(cardTitle, cardText, speechOutput, repromptText, shouldEndSession, sessionAttributes);
        saveAndExit(userData, context, response);
    } else {
        console.log('Start BB - Create new');

        // No open found, so we should start new barbecue.
        var bbq = {
            startTime: intent.timestamp,
            foods: []
        }
        userData.Item.barbecues.push(bbq);

        response = buildSpeechletResponse(cardTitle, cardText, speechOutput, repromptText, shouldEndSession, sessionAttributes);
        saveAndExit(userData, context, response);

    }
}


function yesNo(userData, intent, session, context) {
    console.log('YesNo')
    // First, check what is the step saved on Session attributes
    if (session.attributes) {
        var step = session.attributes.step;
        console.log('YesNo for step ' + step);

        if (step) {
            var answer =  intent.intent.slots.Answer.value.toLowerCase();
            console.log('YesNo - Answer ' + answer);

            if ('StartBarbecue' === step) {
                if ('yes' === answer) {
                    console.log('Start BB Yes')

                    var leng = userData.Item.barbecues.length -1;
                    userData.Item.barbecues[leng].endTime = intent.timestamp;
                    startBarbecue(userData, intent, session, context);
                } else {
                    console.log('Start BB No')
                    var response = buildSpeechletResponse('Start Barbecue', 'We will continue use ', '', '', false, {});
                    saveAndExit(userData, context, response);
                }
            } else {
                // Return nothing
            }
        } else {
            // return nothing
        }

    }

}

// --------------- Helpers that build all of the responses -----------------------

function hasOpenBarbecue(userData){
    var  startTime = null;
    var barbecues = userData.Item.barbecues;
    console.log('Start BB - Barbecue List = ' + barbecues);
    if (barbecues && barbecues.length > 0) {
        console.log('Has old bb');
        var bb = barbecues[barbecues.length-1];
        if (!bb.endTime) {
            // TODO: Check the date. If it more than 12 hours, consider it closed.
            console.log('Has OPEN old bb');
            return bb.startTime;
        } else console.log('Has NO OPEN old bb');
    } else  console.log('Not have old bb');
}

function getOrdinal(number) {
    switch (number) {
        case 1:
            return 'first';
        case 2:
            return 'second';
        case 3:
            return 'third';
        case 4:
            return 'fourth';
        case 5:
            return 'fifth';
        case 6:
            return 'sixth';
        case 7:
            return 'seventh';
        case 8:
            return 'eighth';
        case 9:
            return 'nineth';
        case 10:
            return 'tenth';
        default:
            return '';
    }
}

function getNumber(ordinal) {
    switch (ordinal) {
        case 'first':
            return 1;
        case 'second':
            return 2;
        case 'third':
            return 3;
        case 'fourth':
            return 4;
        case 'fifth':
            return 5;
        case 'sixth':
            return 6;
        case 'seventh':
            return 7;
        case 'eighth':
            return 8;
        case 'nineth':
            return 9;
        case 'tenth':
            return 10;
        default:
            return 0;
    }
}

function getResponseTextFromFood(foodList, currTime) {
    var cardText = '';
    var outputT = '';
    var curDate = new Date(currTime);
    // 2016-04-27T13:21:56Z
    var i = 0;

    if(foodList.length === 0) {
        console.log('No food');
        return {
            cardText: cardText,
            outputSpeech: "I'm sorry, but I could not find any food with the name you said. Please, try again."
        }
    }

    for (i = 0; i < foodList.length; i++) {
        var food = foodList[i];

        var total = getInMinutes(new Date(food.startTime), curDate);
        var foodName = getOrdinal(food.index) + " " + food.type;

        outputT += foodName + " is in the grill for " + getMinutesStr(total) + ". ";
        if (!food.turnTime) {
            var m = food.firstTimer - total;
            console.log('No turn for ' + foodName + " - " + m );
            // No turned yet
            if (m > 0) {
                console.log('Before turn '+ total + " - " + food.firstTimer);
                // Still have time to turn
                outputT += "You should turn it in " + getMinutesStr(food.firstTimer - total) + ". "
            } else if (m === 0) {
                console.log('Same time turn '+ foodName);
                // About time to turn
                outputT += "It is time to turn it. When you do, just say: Alexa ask Grill Pal to turn " + foodName;
            } else {
                console.log('After turn '+ foodName);
                // Turn time passed
                outputT += "You had to turn it " + getMinutesStr(total - food.firstTimer)+ " ago. Hurry up! When you do, just say: Alexa ask Grill Pal to turn " + foodName;
            }
        } else {
            // Did the turn, so calculate the serving time
            var afterTurn = getInMinutes(new Date(food.turnTime), curDate);
            outputT += "You turned it " + getMinutesStr(afterTurn )+ " ago. ";
            var m2 = food.secondTimer - afterTurn;
            if (m2 > 0) {
                console.log('Before serve ' +foodName);
                // Still have time to serve
                outputT += "You will be ready to serve in " + getMinutesStr(food.secondTimer - afterTurn) + ". "
            } else if (m2 === 0) {
                console.log('Same time serve ' +foodName);
                // About time to turn
                outputT += "It is time to serve. When you do, just say: Alexa ask Grill Pal to serve " + foodName;
            } else {
                console.log('After serve '+ foodName);
                // Turn time passed
                outputT += "You had to take it from grill " + getMinutesStr(afterTurn - food.firstTimer) + " ago. Hurry up! When you do, just say: Alexa ask Grill Pal to serve " + foodName;
            }
        }
    }

    return {
        cardText: cardText,
        outputSpeech: outputT
    };
}

function getInMinutes(d1, d2) {
    return Math.round(((d2-d1)/1000)/60);
}

function getMinutesStr(number) {
    if (number <= 1) return number + " minute";
    else  return number + " minutes";
}

function buildSpeechletResponse(title, cardText, output, repromptText, shouldEndSession, attributes) {
    return {
        version: "1.0",
        sessionAttributes: attributes,
        response: {
            outputSpeech: {
                type: "PlainText",
                text: output
            },
            card: {
                type: "Simple",
                title: title,
                text: cardText
            },
            reprompt: {
                outputSpeech: {
                    type: "PlainText",
                    text: repromptText
                }
            },
            shouldEndSession: shouldEndSession
        }
    };
}

Credits

B70e3beef3a6caa24f88fdaad403db63
Mauricio Lempke Nunes
1 project • 1 follower
Contact

Replications

Did you replicate this project? Share it!

I made one

Love this project? Think it could be improved? Tell us what you think!

Give feedback

Comments

Sign up / LoginProjectsPlatformsTopicsContestsLiveAppsBetaBlog