AWS Lex lambda function Elicit slots - node.js

I'm building the AWS Lex chat bot right now and faced some issue on the lambda function settings. According to the sample code, it used this lambda function at the end of the conversation. That's why the code was like : function close(.....)
'use strict';
// Close dialog with the customer, reporting fulfillmentState of Failed
or Fulfilled ("Thanks, your pizza will arrive in 20 minutes")
function close(sessionAttributes, fulfillmentState, message) {
return {
sessionAttributes,
dialogAction: {
type: 'Close',
fulfillmentState,
message,
},
};
}
However what I would like to do is using the DialogCodeHook instead of this FulfillmentCodeHook.
The simplest logic inside Lex is asking question 1-->get answer 1-->asking question 2-->get answer 2-->asking question 2-->get answer 3;
What i wanna do is
Ask Question 1- Response Value allowed are 1.1, 1.2
If Response Value= Value 1.1
Ask Question 2
If Response Value= Value 1.2
Ask Question 3
Ask Question 4- Value 4.1, Value 4.2
.. so on
On AWS discussion forum, an answer is like:
Yes, you can use Lambda to implement the decision tree. Lambda allows you to set a specific message and elicit a slot using 'dialogAction'.
For this specific conversation flow
if (response_value = 1.1) {
// set dialogAction.message = "Question 2"
...
// set type = ElicitSlot
...
// slotToElicit = answer2"
}
Similarly you would define conditions to ask Question 3, 4 etc.
But I am not sure where should I put this If..... at and how to use this ElicitSlot function.
Full version of the sample code for the close function is:
'use strict';
// Close dialog with the customer, reporting fulfillmentState of Failed or Fulfilled ("Thanks, your pizza will arrive in 20 minutes")
function close(sessionAttributes, fulfillmentState, message) {
return {
sessionAttributes,
dialogAction: {
type: 'Close',
fulfillmentState,
message,
},
};
}
// --------------- Events -----------------------
function dispatch(intentRequest, callback) {
console.log('request received for userId=${intentRequest.userId}, intentName=${intentRequest.currentIntent.intentName}');
const sessionAttributes = intentRequest.sessionAttributes;
const slots = intentRequest.currentIntent.slots;
const crust = slots.crust;
const size = slots.size;
const pizzaKind = slots.pizzaKind;
callback(close(sessionAttributes, 'Fulfilled',
{'contentType': 'PlainText', 'content': `Okay, I have ordered your ${size} ${pizzaKind} pizza on ${crust} crust`}));
}
// --------------- Main handler -----------------------
// Route the incoming request based on intent.
// The JSON body of the request is provided in the event slot.
exports.handler = (event, context, callback) => {
try {
dispatch(event,
(response) => {
callback(null, response);
});
} catch (err) {
callback(err);
}
};
Hope someone can help! Thank you so much!!!!!!!!!!!!!!!!!!!!!

Please check this code: https://github.com/nicholasjackson/slack-bot-lex-lambda/blob/master/src/dispatcher.js
It lists functions for all possible scenarios, including the close(...) you have, but also the ElicitSlot(...) as well that you're after.
Please note that there is an ElicitIntent dialog action type as well which is not used in the code but it could be useful in some scenarios.
Hope it helps.
Tibor

Related

Firebase function to add document with data to Firestore only works second and subsequent times of executing it, but not first

I'm getting a little problem which I'm not being capable to debug. I wrote a little Firebase Function to get data from a JSON object and to store it in a Firestore Document. Simple.
It works, except the first time I run it after deployed (or after a long time has passed since the last execution). I have to run it once (without working), and then the subsequent tries always work, and I can see the new document being created with all the data inside it.
In the first attempt, there are no logs: Function execution took 601 ms, finished with status code: 200. Despite that, no document is being created nor changes being made.
In the second and subsequent attempts, If I request the function execution with a HTTP POST to https://cloudfunctions/functionName?id=12345, then the document '12345' is created inside collection with all the data inside it.
The collection where the documents are stored (scenarios) already exist in the database before any function call is executed.
This is the code:
const functions = require("firebase-functions");
const admin = require("firebase-admin");
admin.initializeApp();
const db = admin.firestore();
db.settings({ignoreUndefinedProperties: true});
const fetch = require("node-fetch");
let scenarioData;
const fetchScenarioJSON = async (scenarioId) => {
try {
const response = await fetch(`https://url/api/scenarios/single/${scenarioId}`);
const scenarioText = await response.text();
scenarioData = JSON.parse(scenarioText);
} catch (err) {
return ("not valid json");
}
return scenarioData;
};
/**
* Add data to Firestore.
* #param {JSON} scenario JSON array containing the scenario data.
*/
async function addDataToFirestore(scenario) {
const data = {
id: scenario.scenario._id,
name: scenario.scenario.name,
description: scenario.scenario.description,
language: scenario.scenario.language,
author: scenario.scenario.author,
draft: scenario.scenario.draft,
last_modified: scenario.scenario.last_modified,
__v: scenario.scenario.__v,
duration: scenario.scenario.duration,
grade: scenario.scenario.grade,
deleted: scenario.scenario.deleted,
view_count: scenario.scenario.view_count,
comments_count: scenario.scenario.comments_count,
favorites_count: scenario.scenario.favorites_count,
activities_duration: scenario.scenario.activities_duration,
activities: scenario.scenario.activities,
outcomes: scenario.scenario.outcomes,
tags: scenario.scenario.tags,
students: scenario.scenario.students,
created: scenario.scenario.created,
subjects: scenario.scenario.subjects,
};
const res = await db.collection("scenarios").doc(scenario.scenario._id).set(data);
}
exports.functionName =
functions.https.onRequest((request, response) => {
return fetchScenarioJSON(request.query.id).then((scenario) => {
if (typeof scenario === "string") {
if (scenario.includes("not valid json")) {
response.send("not valid json");
}
} else {
addDataToFirestore(scenario);
response.send(`Done! Added scenario with ID ${request.query.id} to the app database.`);
}
});
});
My question is if I am doing anything wrong with the code that makes the execution not work on the first call after it is deployed, but actually does work in subsequent calls.
It is most probably because you don't wait that the asynchronous addDataToFirestore() function is completed before sending back the response.
By doing
addDataToFirestore(scenario);
response.send()
you actually indicate (with response.send()) to the Cloud Function platform that it can terminate and clean up the Cloud Function (see the doc for more details). Since you don't wait for the asynchronous addDataToFirestore() function to complete, the doc is not written to Firestore.
The "erratic" behaviour (sometimes it works, sometimes not) can be explained as follows:
In some cases, your Cloud Function is terminated before the write to Firestore is fully executed, as explained above.
But, in some other cases, it may be possible that the Cloud Functions platform does not immediately terminate your CF, giving enough time for the write to Firestore to be fully executed. This is most probably what happens after the first call: the instance of the Cloud Function is still running and then the docs are written with the "subsequent calls".
The following modifications should do the trick (untested). I've refactored the Cloud Function with async/await, since you use it in the other functions.
// ....
async function addDataToFirestore(scenario) {
const data = {
id: scenario.scenario._id,
name: scenario.scenario.name,
description: scenario.scenario.description,
language: scenario.scenario.language,
author: scenario.scenario.author,
draft: scenario.scenario.draft,
last_modified: scenario.scenario.last_modified,
__v: scenario.scenario.__v,
duration: scenario.scenario.duration,
grade: scenario.scenario.grade,
deleted: scenario.scenario.deleted,
view_count: scenario.scenario.view_count,
comments_count: scenario.scenario.comments_count,
favorites_count: scenario.scenario.favorites_count,
activities_duration: scenario.scenario.activities_duration,
activities: scenario.scenario.activities,
outcomes: scenario.scenario.outcomes,
tags: scenario.scenario.tags,
students: scenario.scenario.students,
created: scenario.scenario.created,
subjects: scenario.scenario.subjects,
};
await db.collection("scenarios").doc(scenario.scenario._id).set(data);
}
exports.functionName =
functions.https.onRequest(async (request, response) => {
try {
const scenario = await fetchScenarioJSON(request.query.id);
if (typeof scenario === "string") {
if (scenario.includes("not valid json")) {
response.send("not valid json");
}
} else {
await addDataToFirestore(scenario); // See the await here
response.send(`Done! Added scenario with ID ${request.query.id} to the app database.`);
}
} catch (error) {
// ...
}
});

Is there a way to make bots aware of what page they are on?

I have a chatbot that will eventually be deployed on multiple websites, and there are a number or variables that need to change based on the site (e.g. language, QnA Database, Dialog, etc.). I'd like to do this with a single bot, and just pass a variable so that it knows which page it is being rendered on (for a simple example, let's assume country pages: us, fr, de, etc.). I have been unsuccessful in passing this information to the bot.
Ideally this would be before the welcome message fires, but I can't even get it to send at all. I have a custom store set up:
const store = window.WebChat.createStore({}, function(dispatch) { return function(next) { return function(action) {
if (action.type === 'WEB_CHAT/SEND_MESSAGE') {
// Message sent by the user
PageTitleNotification.Off();
clearTimeout(interval);
} else if (action.type === 'DIRECT_LINE/INCOMING_ACTIVITY' && action.payload.activity.name !== "inactive") {
// Message sent by the bot
clearInterval(interval);
interval = setTimeout(function() {
// Change title to flash the page
PageTitleNotification.On('Are you still there?');
// Notify bot the user has been inactive
dispatch.dispatch({
type: 'WEB_CHAT/SEND_EVENT',
payload: {
name: 'inactive',
value: ''
}
});
}, 300000)
}
return next(action);
}}});
But for my use case I don't think what's in there actually matters, only that it is defined. The functions here just 1) clear an interval when the user sends a message and 2) set a new interval and send an inactivity message to the bot.
I also have a send message activity that is on a button click for a transcript. It looks like this:
document.querySelector('#transcriptButton').addEventListener('click', function() {
return store.dispatch({
type: 'WEB_CHAT/SEND_MESSAGE',
payload: { text: 'Email me a transcript' }
});
/*return store.dispatch({
type: 'WEB_CHAT/SEND_EVENT',
payload: {
name: 'siteContext',
value: 'eatonchatbot indexBackup.html'
}
});*/
});
This sends a "front channel" message (that I can see in the bot) to request a transcript, which kicks off a dialog. That works. The commented out section alludes to what I'm trying to do. I have a separate dispatch statement as shown below, which has the exact same SEND_EVENT code as is commented out above. The SEND_EVENT does work as expected when it keys off the button click.
Here is the additional code I added. This is the piece that is NOT working. What I want is, when the bot has been rendered (but ideally before the welcome message), send this siteContext event to the bot so that I know where the bot is being rendered. I do not get any activity in the bot with this code. I also tried replacing it with SEND_MESSAGE instead of SEND_EVENT in a sort of reverse test from above, but that didn't work either.
// Test setting site context
store.dispatch({
type: 'WEB_CHAT/SEND_EVENT',
payload: {
name: 'siteContext',
value: 'eatonchatbot indexBackup.html'
}
});
/*store.dispatch({
type: 'WEB_CHAT/SEND_MESSAGE',
payload: {
text: 'eatonchatbot indexBackup.html'
}
});*/
It just occurred to me that this statement is probably running before the bot is rendered. So I put it in an interval and this DOES work. However, it does not fire the message until after the welcome message has been sent.
setTimeout(function() {
store.dispatch({
type: 'WEB_CHAT/SEND_EVENT',
payload: {
name: 'siteContext',
value: 'eatonchatbot indexBackup.html'
}
});
}, 5000);
So this kind of works, but if this siteContext value was needed to determine the language of the welcome message, this would obviously fail. So my main ask here is, is there a better way to try to pass in a siteContext value like this, or is there some way to ensure that the context is received and can be used by the bot before the welcome message fires? I do see that there is a locale setting in the renderWebChat method, but I can't figure out if and how I could access that in the bot, and besides it may not be granular enough depending on the business need. But it seems if I could send some sort of value in that renderWebChat object, that might avoid all of the other crazy stuff I'm trying to do.
With some help from #Hessel and this issue I found on GitHub, I was able to come up with a solution. Just setting the values being passed in via onEvent (which I am now using in place of onTurn to reduce an if statement) isn't good enough if you need to alter content in the welcome message (e.g. language, user name, or an altogether different message). The onMembersAdded still fires before the values can be set, at least if you're setting them in userState. The key is to set up separate welcome messages in onEvent for directline and onMembersAdded for all other channels (I didn't include webchat as in the example as I'm not sending any event for that channel).
Here is the onEvent function I used:
this.onEvent(async (context, next) => {
// Check for inactivity
if (context.activity.name && context.activity.name === 'inactive') {
await context.sendActivity({
text: 'Are you still there? Is there anything else I can help you with?',
name: 'inactive'
});
}
// Check for webchat/join event (directline conversation initiation)
if (context.activity.name && context.activity.name === 'webchat/join') {
const userData = await this.userDialogStateAccessor.get(context, {});
userData.siteContext = context.activity.value;
// Debug
console.log(`The current language is: ${userData.siteContext.language}`);
console.log(`The current page is: ${userData.siteContext.page}`);
//await context.sendActivity(`The current language is: ${userData.siteContext.language}`);
//await context.sendActivity(`The current page is: ${userData.siteContext.page}`);
if (!userData.accountNumber) {
const dc = await this.dialogs.createContext(context);
await dc.beginDialog(AUTH_DIALOG);
await this.conversationState.saveChanges(context);
await this.userState.saveChanges(context);
} else {
if (context.activity.channelId == 'msteams') {
var welcomeCard = CardHelper.GetMenuCardTeams(welcomeMessage,'Y','Y');
} else {
var welcomeCard = CardHelper.GetMenuCard(welcomeMessage,'Y','Y');
}
await context.sendActivity(welcomeCard);
this.appInsightsClient.trackEvent({name:'conversationStart', properties:{accountNumber:userData.accountNumber}});
}
await this.userState.saveChanges(context);
}
// By calling next() you ensure that the next BotHandler is run.
await next();
});
The event I used in the custom store is pretty much the same as above, except I updated it to pull in the most preferred language and current url (was hard coded above).
store = window.WebChat.createStore({}, function (dispatch) {
return function (next) {
return function (action) {
if (action.type === 'DIRECT_LINE/CONNECT_FULFILLED') {
dispatch.dispatch({
type: 'WEB_CHAT/SEND_EVENT',
payload: {
name: 'webchat/join',
value: {
language: navigator.languages[0],
page: window.location.href
}
}
});
}
return next(action);
};
};
});
If you have your renderWebChat method inside a function that you can call so that your bot doesn't automatically start (I have a floating icon that causes the bot to load onclick) this should go outside that function.

Unable to get helper function to work in AWS lambda nodejs

I'm new to nodejs and learning, but can't find out why my helper function won't work.
Essentially this is part of an example alexa lambda function that generally works.
The MQTT operation works if I leave the MQTTcode within the Intent handler, but I need to move it out into the main body of code so I can call the MQTT operation from other code functions.
There are several 'test' functions in this snippet that fail to work, probably because I don't appreciate the correct way to move the code out of the Intent function.
I'm also pretty unclear on handlers.. ( multiple handlers actually ) There are two handlers in the code snippet.. it doesn't cause a problem, but I was hoping to have two lambda triggers ( ask-sdk & smart home) with each calling their own handler - not sure if that's possible.
var APP_ID = "amzn1.ask.skill.xxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; // input the axela skill ID
var AWS = require('aws-sdk');
var Alexa = require("alexa-sdk");
AWS.config.region = "us-east-1";
var iotData = new AWS.IotData({endpoint: "xxxxxxxxxxx.iot.us-east-1.amazonaws.com"}); // input the AWS thing end point
var topic = "esp32/sub"; //input the topic that the device is subscribed to
// Handler for Generic Event handling accepts both SmartHomeand ask-sdk events
// But only works when the handler below is removed.
exports.handler = async function (event, context) {
// Dump the request for logging - check the CloudWatch logs
console.log("index.handler request -----");
console.log(JSON.stringify(event));
if (context !== undefined) {
console.log("index.handler context -----");
console.log(JSON.stringify(context));
}
switchon(); // test call of standalone MQTTfunction ( doesn't work)
};
// Remove this function and the Smarthome Test works.
// But is needed for the ask-sdk events ( Smarthome events fail )
exports.handler = function(event, context, callback) {
var alexa = Alexa.handler(event, context);
alexa.appId = APP_ID;
alexa.registerHandlers(handlers);
alexa.execute();
console.log("index.handler comment -----");
};
//*********************************
// Helper code examples to functionalise the MQTT switch on
// NONE OF THESE WORK WHEN CALLED
function switchon3(){
var dataObj = {
topic: topic,
payload: "on",
qos:0
};
iotData.publish(dataObj, function (err, data) {
if (err) console.log(err, err.stack); // an error occurred
else console.log(data); // successful response
});
}
function switchon (error, data){
var params = {
topic: topic,
payload: "on",
qos:0
};
iotData.publish(params, (error, data)=>{
if (!error){this.emit(':tell', 'Robert, well done its Switched On');
}else{this.emit(':tell', 'Oh dear MQTT returned a switch on error')}
});
}
// End of helper examples
//*********************************
//********* THE PROPER CODE ************************
var handlers = {
'LaunchRequest': function () {
this.emit(':tell', 'Hello. Skill four here. How may I help you?');
},
'SwitchOnIntent': function () {
// None of the example function calls work here
// switchon3();
// this.emit(':tell', 'Test Switch On'); // needs this line to work
// The following original example code DOES work
var params = {
topic: topic,
payload: "on",
qos:0
};
iotData.publish(params, (error, data)=>{
if (!error){this.emit(':tell', 'Robert, well done its Switched On');
}else{this.emit(':tell', 'Oh dear MQTT returned a switch on error')}
});
},
Edited...
No, Tommy, it's not too basic, thanks for the help. I'm actually trying to get the lambda to accept inputs from two AWS triggers.
1. The ASK-API from custom skills
2. The Smarthome trigger.
I'm unsure if the two triggers need separate handler functions, or, if as I suspect, using the smarthome trigger voids the use of the ask-api methods that somehow call the registered Intent functions,
The json that arrives is clearly formatted differently from both trigger types, and I appreciate that it's possible to do all the alexa custom skill parsing manually within the lambda.
My question is then.. if starting out with a custom skill, registering all the function calls with the ask-api becomes void if I then add a smarthome trigger because the one handler that dealt with the ask-api event cannot also deal with the smarthome directive.
Subsequent to sorting that out, is trying to 'bring out' the MQTT call, that works within the Intent functions as originally coded, but fails if I try to put them into separate function calls.
Bear with me ... I know what I want to do.. just don't know this language well at all yet.
I think what you're not grasping here is that your actually overwriting the same variable.
exports is an object (variable) and it can have multiple properties. Forgive me if this is too basic, but a property is basically a variable attached to another variable.
In your code, you first assign the value of this property to a function.
exports.handler = async function (event, context) {
// Dump the request for logging - check the CloudWatch logs
console.log("index.handler request -----");
console.log(JSON.stringify(event));
if (context !== undefined) {
console.log("index.handler context -----");
console.log(JSON.stringify(context));
}
switchon(); // test call of standalone MQTTfunction ( doesn't work)
};
so if you then ran exports.handler() it would run that function. However, you then reassign this variable a few lines down.
So it's now the below:
exports.handler = function(event, context, callback) {
var alexa = Alexa.handler(event, context);
alexa.appId = APP_ID;
alexa.registerHandlers(handlers);
alexa.execute();
console.log("index.handler comment -----");
};
You are replacing the first function with the second one, which is why commenting out the second assignment to exports.handler causes the first bit to work. I'm not 100% clear on what you're asking, but you either need to combine the contents of both functions if you can (or have one handler that checks the event and calls a separate function), or move them into separate lambdas.
For instance:
exports.handler = function(event,context,callback) {
if(event.EventType === "YourGenericEvent") { // replace YourGenericEvent with whatever the eventName is for the first function
genericEvent(event,context)
} else if(event.EventType === "SecondEvent") { // again replace "SecondEvent" with whatever the event is for your second function
secondEvent(event,context,callback)
}
}
function genericEvent (event, context) {
// Dump the request for logging - check the CloudWatch logs
console.log("index.handler request -----");
console.log(JSON.stringify(event));
if (context !== undefined) {
console.log("index.handler context -----");
console.log(JSON.stringify(context));
}
switchon(); // test call of standalone MQTTfunction ( doesn't work)
};
function secondEvent(event,context,callback) {
var alexa = Alexa.handler(event, context);
alexa.appId = APP_ID;
alexa.registerHandlers(handlers);
alexa.execute();
console.log("index.handler comment -----");
}
your console.log(event) statements should hopefully give you an indication of what the value of the EventType property should be for the IF statements.
You can see another post related to Python here
How to have more than one handler in AWS Lambda Function?

Triggering the fulfillment webhook asynchronously from an intent?

I have some intents that need to trigger the fulfillment webhook and don't care about the response. The webhook takes longer than the timeout to respond so I'd like the intent to simply respond with "Thanks for chatting" and then close the conversation while actually triggering the webhook.
Feels easy but I'm missing something. Also I'm new to the dialogflow stuff.
I can do this in any language, but here's an example in Javascript:
fdk.handle(function (input) {
// Some code here that takes 20 seconds.
return {'fulfillmentText': 'i can respond but I will never make it here.'}
});
EDIT 1 - Trying async
When I use an async function, the POST request never happens. So in the following code:
fdk.handle(function (input) {
callFlow(input);
return { 'fulfillmentText': 'here is the response from the webhook!!' }
});
async function callFlow(input) {
console.log("input is --> " + input)
var url = "some_url"
console.log("Requesting " + url)
request(url, { json: true, headers: {'Access-Control-Allow-Origin' : '*'} }, (err, res, body) => {
if (err) { return console.log(err); }
console.log("body is...")
console.log(body)
});
}
I see in the logs the two console.log outputs but nothing from the request. And the request doesn't seem to happen either because I don't see it at my endpoint.
SOLUTION
Thanks Prisoner for the tip. Seems like I needed to return the fulfillment JSON back through the callFlow() and handle() functions. Now Google Home doesn't timeout and both the HTTP call and response are generated.
const fdk = require('#fnproject/fdk');
const request = require('request');
fdk.handle(function (input) {
return callFlow(input);
});
async function callFlow(input) {
var searchPhrase = input || "cats"
var url = "some url"
return new Promise((resolve, reject) => {
request.post(url, {
headers: { 'content-type': 'application/x-www-form-urlencoded' },
body: searchPhrase
},
function (err, resp, body) {
if (err) { return console.log(err) }
r = { 'fulfillmentText': `OK I've triggered the flow function with search term ${searchPhrase}` }
resolve(r)
}
);
});
}
You cannot trigger the fulfillment asynchronously. In a conversational model, it is expected that the fulfillment will perform some logic that determines the response.
You can, however, perform an asynchronous operation in the fulfillment that does not complete before you return the result.
If you are using a sufficiently modern version of node (version 8 and up), you can do this by declaring a function as an async function, but not calling it with the await keyword. (If you did call it with await, it would wait for the asynchronous operation to complete before continuing.)
So something like this should work, given your example:
async function doSomethingLong(){
// This takes 20 seconds
}
fdk.handle(function (input) {
doSomethingLong();
return {'fulfillmentText': 'This might respond before doSomethingLong finishes.'}
});
Update 1 based on your code example.
It seems odd that you report that the call to request doesn't appear to be done at all, but there are some odd things about it that may be causing it.
First, request itself isn't an async function. It is using a callback model and async functions don't just automatically wait for those callbacks to be called. So your callFlow() function calls console.log() a couple of times, calls request() and returns before the callbacks are called back.
You probably should replace request with something like the request-promise-native package and await the Promise that you get from the call. This makes callFlow() truly asynchronous (and you can log when it finishes the call).
Second, I'd point out that the code you showed doesn't do a POST operation. It does a GET by default. If you, or the API you're calling, expect a POST, that may be the source of the error. However, I would have expected the err parameter to be populated, and your code does look like it checks for, and logs, this.
The one unknown in the whole setup, for me, is that I don't know how fdk handles async functions, and my cursory reading of the documentation hasn't educated me. I've done this with other frameworks, and this isn't a problem, but I don't know if the fdk handler times out or does other things to kill a call once it sends a reply.

How do you structure sequential AWS service calls within lambda given all the calls are asynchronous?

I'm coming from a java background so a bit of a newbie on Javascript conventions needed for Lambda.
I've got a lambda function which is meant to do several AWS tasks in a particular order, depending on the result of the previous task.
Given that each task reports its results asynchronously, I'm wondering if the right way make sure they all happen in the right sequence, and the results of one operation are available to the invocation of the next function.
It seems like I have to invoike each function in the callback of the prior function, but seems like that will some kind of deep nesting and wondering if that is the proper way to do this.
For example on of these functions requires a DynamoDB getItem, following by a call to SNS to get an endpoint, followed by a SNS call to send a message, followed by a DynamoDB write.
What's the right way to do that in lambda javascript, accounting for all that asynchronicity?
I like the answer from #jonathanbaraldi but I think it would be better if you manage control flow with Promises. The Q library has some convenience functions like nbind which help convert node style callback API's like the aws-sdk into promises.
So in this example I'll send an email, and then as soon as the email response comes back I'll send a second email. This is essentially what was asked, calling multiple services in sequence. I'm using the then method of promises to manage that in a vertically readable way. Also using catch to handle errors. I think it reads much better just simply nesting callback functions.
var Q = require('q');
var AWS = require('aws-sdk');
AWS.config.credentials = { "accessKeyId": "AAAA","secretAccessKey": "BBBB"};
AWS.config.region = 'us-east-1';
// Use a promised version of sendEmail
var ses = new AWS.SES({apiVersion: '2010-12-01'});
var sendEmail = Q.nbind(ses.sendEmail, ses);
exports.handler = function(event, context) {
console.log(event.nome);
console.log(event.email);
console.log(event.mensagem);
var nome = event.nome;
var email = event.email;
var mensagem = event.mensagem;
var to = ['email#company.com.br'];
var from = 'site#company.com.br';
// Send email
mensagem = ""+nome+"||"+email+"||"+mensagem+"";
console.log(mensagem);
var params = {
Source: from,
Destination: { ToAddresses: to },
Message: {
Subject: {
Data: 'Form contact our Site'
},
Body: {
Text: {
Data: mensagem,
}
}
};
// Here is the white-meat of the program right here.
sendEmail(params)
.then(sendAnotherEmail)
.then(success)
.catch(logErrors);
function sendAnotherEmail(data) {
console.log("FIRST EMAIL SENT="+data);
// send a second one.
return sendEmail(params);
}
function logErrors(err) {
console.log("ERROR="+err, err.stack);
context.done();
}
function success(data) {
console.log("SECOND EMAIL SENT="+data);
context.done();
}
}
Short answer:
Use Async / Await — and Call the AWS service (SNS for example) with a .promise() extension to tell aws-sdk to use the promise-ified version of that service function instead of the call back based version.
Since you want to execute them in a specific order you can use Async / Await assuming that the parent function you are calling them from is itself async.
For example:
let snsResult = await sns.publish({
Message: snsPayload,
MessageStructure: 'json',
TargetArn: endPointArn
}, async function (err, data) {
if (err) {
console.log("SNS Push Failed:");
console.log(err.stack);
return;
}
console.log('SNS push suceeded: ' + data);
return data;
}).promise();
The important part is the .promise() on the end there. Full docs on using aws-sdk in an async / promise based manner can be found here: https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/using-promises.html
In order to run another aws-sdk task you would similarly add await and the .promise() extension to that function (assuming that is available).
For anyone who runs into this thread and is actually looking to simply push promises to an array and wait for that WHOLE array to finish (without regard to which promise executes first) I ended up with something like this:
let snsPromises = [] // declare array to hold promises
let snsResult = await sns.publish({
Message: snsPayload,
MessageStructure: 'json',
TargetArn: endPointArn
}, async function (err, data) {
if (err) {
console.log("Search Push Failed:");
console.log(err.stack);
return;
}
console.log('Search push suceeded: ' + data);
return data;
}).promise();
snsPromises.push(snsResult)
await Promise.all(snsPromises)
Hope that helps someone that randomly stumbles on this via google like I did!
I don't know Lambda but you should look into the node async library as a way to sequence asynchronous functions.
async has made my life a lot easier and my code much more orderly without the deep nesting issue you mentioned in your question.
Typical async code might look like:
async.waterfall([
function doTheFirstThing(callback) {
db.somecollection.find({}).toArray(callback);
},
function useresult(dbFindResult, callback) {
do some other stuff (could be synch or async)
etc etc etc
callback(null);
],
function (err) {
//this last function runs anytime any callback has an error, or if no error
// then when the last function in the array above invokes callback.
if (err) { sendForTheCodeDoctor(); }
});
Have a look at the async doco at the link above. There are many useful functions for serial, parallel, waterfall, and many more. Async is actively maintained and seems very reliable.
good luck!
A very specific solution that comes to mind is cascading Lambda calls. For example, you could write:
A Lambda function gets something from DynamoDB, then invokes…
…a Lambda function that calls SNS to get an endpoint, then invokes…
…a Lambda function that sends a message through SNS, then invokes…
…a Lambda function that writes to DynamoDB
All of those functions take the output from the previous function as input. This is of course very fine-grained, and you might decide to group certain calls. Doing it this way avoids callback hell in your JS code at least.
(As a side note, I'm not sure how well DynamoDB integrates with Lambda. AWS might emit change events for records that can then be processed through Lambda.)
Just saw this old thread. Note that future versions of JS will improve that. Take a look at the ES2017 async/await syntax that streamlines an async nested callback mess into a clean sync like code.
Now there are some polyfills that can provide you this functionality based on ES2016 syntax.
As a last FYI - AWS Lambda now supports .Net Core which provides this clean async syntax out of the box.
I would like to offer the following solution, which simply creates a nested function structure.
// start with the last action
var next = function() { context.succeed(); };
// for every new function, pass it the old one
next = (function(param1, param2, next) {
return function() { serviceCall(param1, param2, next); };
})("x", "y", next);
What this does is to copy all of the variables for the function call you want to make, then nests them inside the previous call. You'll want to schedule your events backwards. This is really just the same as making a pyramid of callbacks, but works when you don't know ahead of time the structure or quantity of function calls. You have to wrap the function in a closure so that the correct value is copied over.
In this way I am able to sequence AWS service calls such that they go 1-2-3 and end with closing the context. Presumably you could also structure it as a stack instead of this pseudo-recursion.
I found this article which seems to have the answer in native javascript.
Five patterns to help you tame asynchronis javascript.
By default Javascript is asynchronous.
So, everything that you have to do, it's not to use those libraries, you can, but there's simple ways to solve this. In this code, I sent the email, with the data that comes from the event, but if you want, you just need to add more functions inside functions.
What is important is the place where your context.done(); is going to be, he is going to end your Lambda function. You need to put him in the end of the last function.
var AWS = require('aws-sdk');
AWS.config.credentials = { "accessKeyId": "AAAA","secretAccessKey": "BBBB"};
AWS.config.region = 'us-east-1';
var ses = new AWS.SES({apiVersion: '2010-12-01'});
exports.handler = function(event, context) {
console.log(event.nome);
console.log(event.email);
console.log(event.mensagem);
nome = event.nome;
email = event.email;
mensagem = event.mensagem;
var to = ['email#company.com.br'];
var from = 'site#company.com.br';
// Send email
mensagem = ""+nome+"||"+email+"||"+mensagem+"";
console.log(mensagem);
ses.sendEmail( {
Source: from,
Destination: { ToAddresses: to },
Message: {
Subject: {
Data: 'Form contact our Site'
},
Body: {
Text: {
Data: mensagem,
}
}
}
},
function(err, data) {
if (err) {
console.log("ERROR="+err, err.stack);
context.done();
} else {
console.log("EMAIL SENT="+data);
context.done();
}
});
}

Resources