In the previous section, you created a dialog model using Alexa console's skill builder and in this section, you will process the dialogs coming from Alexa. Whenever Alexa matches GetCookingIntent, there are important properties that get set whenever the intent begins to initiate a conversation. The GetCookingIntent, when received from the Node.js server the request object, will contain dialogState set to one of STARTED, IN_PROGRESS, and COMPLETED because GetCookingIntent has the dialog models that you created in the previous section. Also, if confirmation is required for the slot given by the user, there will be a status property called confirmationStatus set to either NONE, DENIED, or CONFIRMED.
When a creating conversational flow, you will need to pay attention to the dialogState and confirmationStatus properties properly to handle the responses, so that Alexa can deliver an appropriate response to the user to acquire missing slots or confirm fulfilled slots. First, when the user does not specify DietTypes or Foods, dialogState will be set to STARTED in the request object. When responding to the STARTED state, you need to include Dialog.Delegate in directives with the updatedIntent object. If dialogState is in IN_PROGRESS, you would need to send Dialog.Delegate without updatedIntent. If dialogState is in the COMPLETED state, you will notice that all the DietTypes and Foods slots contain values that you can use to get the result from the Spoonacular search API.
The following screenshot shows the GetCookingIntent dialog flow:
Here are the detailed steps for writing the code to handle the conversational flow using the dialogue models that you created in the previous section:
- Open Visual Studio Code.
- Go to File | Open Folder and to the the Chapter 7 code that you started to create for logging in the previous section: Setting Up Application Logging in Cooking Application.
- Go to the server.js file.
- You will create a function called StartCookingInstructionDialog that will manage the dialog flow for GetCookingIntent. Begin with adding some logging, logger.info(`${request.intent.name} ${request.dialogState}`), to track dialogState. Then, you will handle request.dialogState == 'STARTED'. You will set shouldEndSession to false, and then add Dialog.Delegate to directives with updatedIntent, which contains slot values. In updatedIntent, you will be adding the slot values and names that are involved in the conversation, which are DietTypes and Foods.
- When dialogState is not COMPLETED, you know that the conversation is currently in progress and as long as the dialogState does not change to COMPLETED, you would need to send Dialog.Delegate in directives without updatedIntent. Finally, when the dialog completes, you can resume handling GetCookingIntent. StartCookingInstructionDialog will maintain the conversational flow until COMPLETE dialogState is received and return false only when the conversation is over, which will trigger BuildGetCookingInstruction in the entry point to the cookingApi POST method.
The following code handles the STARTED, IN_PROGRESS, and COMPLETED dialog states:
function StartCookingInstructionDialog(req, res) {
var request = req.body.request;
logger.info(`StartCookingInstructionDialog ${request.intent.name} ${request.dialogState}`);
if(request.dialogState == 'STARTED'){
res.json({
"version": "1.0",
"response": {
"shouldEndSession": false,
"directives": [
{
"type": "Dialog.Delegate",
"updatedIntent":{
"name": "GetCookingIntent",
"slots":{
"DietTypes": {
"name": "DietTypes",
"value": request.intent.slots.DietTypes.value
?
request.intent.slots.DietTypes.value
: ""
},
"Foods": {
"name": "Foods",
"value": request.intent.slots.Foods.value
? request.intent.slots.Foods.value
: ""
}
}
}
}
]
},
});
} else if (request.dialogState != 'COMPLETED'){
res.json({
"version": "1.0",
"response": {
"shouldEndSession": false,
"directives": [
{
"type": "Dialog.Delegate"
}
]
}
});
} else {
return false;
}
return true;
};
- Modify the code that handles the cookingApi POST method. In the cookingApi POST method, there is a section that gets executed when req.body.request.type === 'IntentRequest' && req.body.request.intent.name === 'GetCookingIntent'. Here, you will be checking StartCookingInstructionDialog, which will return false if the conversation is completed; if not, it will return true. When the conversation is over, you can safely execute BuildGetCookingInstruction. At the beginning of the function, you will add logger.info(JSON.stringify(req.body, null, '\t')), which will log all incoming requests from Alexa. Finally, add try-catch around the entire code, which will handle and log errors if there are any exceptions while executing any lines of code.
The following code utilizes StartCookingInstructionDialog in the cookingApi POST method:
alexaRouter.post('/cookingApi', function (req, res) {
try{
logger.info(JSON.stringify(req.body, null, '\t'));
if (req.body.request.type === 'LaunchRequest') {
logger.info("LaunchRequest");
res.json({
"version": "1.0",
"response": {
"shouldEndSession": true,
"outputSpeech": {
"type": "PlainText",
"text": "Welcome to Henry's Cooking App"
}
}
});
}
else if (req.body.request.type === 'IntentRequest' &&
req.body.request.intent.name === 'GetCookingIntent') {
if(!StartCookingInstructionDialog(req, res))
BuildGetCookingInstruction(req, res);
}
else if (req.body.request.type === 'SessionEndedRequest') {
logger.error('Session ended', req.body.request.reason);
if(req.body.request.reason=='ERROR')
logger.error(JSON.stringify(req.body.request, null, '\t'));
}
} catch(e){
logger.error(e);
}
});
- In BuildGetCookingInstruction, you will be putting various bits of logging code. In the beginning of the function, add logger.info("BuildGetCookingInstruction") so we know this method is being called. Then, in unirest.end check whether the result.error is empty; if it's not empty, it means there is an error, so log the error with logger.error('Error processing spoonacular.'), logger.error('Error processing spoonacular.'), and logger.error('Error processing spoonacular.'). If the call is successful, the result received from Spoonacular will be logged with logger.info(result.body.results). Finally, the response that will be sent to Alexa will be logged using logger.info(JSON.stringify(responseToAlexa, null, '\t')).
The following code is for BuildGetCookingInstruction:
function BuildGetCookingInstruction(req, res) {
var url = 'https://spoonacular-recipe-food-nutrition-v1.p.mashape.com/recipes/search?';
url += 'number=3&offset=0&instructionsRequired=true';
var request = req.body.request;
logger.info("BuildGetCookingInstruction");
if(request.intent.slots.Foods.value) {
var foodName = request.intent.slots.Foods.value;
url += `&query=${foodName}`;
}
if(request.intent.slots.DietTypes.value) {
var dietTypes = request.intent.slots.DietTypes.value;
url += `&diet=${dietTypes}`;
}
unirest.get(url)
.header("X-Mashape-Key", "TTWRwOsRs6mshek89pL3XtbMhie9p10gT9ujsnCYKqafAWv5oF")
.header("X-Mashape-Host", "spoonacular-recipe-food-nutrition-v1.p.mashape.com")
.end(function (result) {
var responseText = "";
if(result.error){
logger.error('Error processing spoonacular.');
logger.error(result.body);
logger.error(result.error);
responseText = `I am sorry there was an issue processing your request.`;
} else {
logger.info("Successfully received results from spoonacular.");
logger.info(result.body.results);
var dishTitle = '';
for(i=0; i < result.body.results.length; i++) {
dishTitle += result.body.results[i].title + ', ';
}
responseText = `I found following dishes that you can cook ${dishTitle}`;
}
var responseToAlexa = {
"version": "1.0",
"response": {
"shouldEndSession": false,
"outputSpeech": {
"type": "PlainText",
"text": responseText
}
}
};
logger.info(JSON.stringify(responseToAlexa, null, '\t'));
res.json(responseToAlexa);
});
};