Mauricio Lempke Nunes
Published © MIT

Alexa Grill Pal

Want to barbecue? I can help. Relax and enjoy your friends while I keep track how long the food is cooking.

BeginnerProtipOver 1 day1,268
Alexa Grill Pal

Things used in this project

Story

Read more

Schematics

Voice User Interface

A simple flowchart showing the iterations between the user and Alexa.

Voice User Interface - Continuation

Code

Amazon WS Lamba JS File

JavaScript
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

Mauricio Lempke Nunes

Mauricio Lempke Nunes

1 project • 1 follower

Comments