I'm trying to write an Alexa skill which reads from Firebase
I'm in a position where I have a NodeJS method that gets called when I use the Alexa test console, but if I add in code to retrieve data from Firebase the method hangs until the lambda times out
const HelloWorldIntentHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
&& Alexa.getIntentName(handlerInput.requestEnvelope) === 'plantsIntent';
},
async handle(handlerInput) {
const snapshot = (await db.collection('plants').get()).data();
const names = snapshot.docs.map(doc => doc.data().name);
const speakOutput = 'Get yourself some cool plants like' + names.join(' and ');
console.log(speakOutput);
var response = handlerInput.responseBuilder
.speak(speakOutput)
//.reprompt('add a reprompt if you want to keep the session open for the user to respond')
.getResponse();
console.log(response);
return response;
}
};
When I run this code I get both the speakOutput string and response object output into the logs, so I know the code is managing to get that far
I'm suspicious it's something to do with Firebase as if I remove the db.collection('plants').get() snippet (and the associated variables) then the code runs to completion
I'm suspicious that it's to do with the method not returning rather than an exception happening, because the output for response is the same in the working version (without Firebase .get()), and the non-working version
Any help would be appreciated!
Related
I have created a skill for Amazon Alexa using node.js, which plays an MP3 stream.
Now I have problems to play a jingle with a fixed URL before the stream starts.
How do I have to proceed to realize this project?
Below is the most important part of the code of the simple player:
const LaunchRequestHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'LaunchRequest'
|| (Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
&& Alexa.getIntentName(handlerInput.requestEnvelope) === 'PlayStationIntent');
},
handle(handlerInput) {
const speakOutput = messages.welcome;
return handlerInput.responseBuilder
.speak(speakOutput)
.addAudioPlayerPlayDirective("REPLACE_ALL", url, token(), 0)
.getResponse();
}
};
There are multiple options for implementing this:
SSML if the jingle is very short and comply to some encodings, you may include it in the speakOutput by using SSML and the audio tag.
M3U Instead of the including the URL of the stream directly in the AudioPlayerPlayDirective, you can include there the URL to an M3U, which then includes a playlist of the Jingle URL and the stream URL.
PlayBackFinished Intent Just sent as first play directive the url of the Jingle and add support for the PlayBackFinished Intent, which will be invoked by the AudioPlayer itself when playing Jingle has been finished and then send inside this intent an audio player play directive (without a speak) but with the URL of the stream. But be aware if that gets finished, the same PlayBackFinished Intent will get called, so you need to identify that it already has been called to avoid making an infinity loop. Best way would to use token attribute on both play commands with (first with "Jingle" and second with "Stream" or token()) so if PlayBackFinished Intent is called, check token in the request and only send the second play command, if token is "Jingle" and so identifying the Jingle has ended.
The last option would change your code to something like:
const LaunchRequestHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'LaunchRequest'
|| (Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
&& Alexa.getIntentName(handlerInput.requestEnvelope) === 'PlayStationIntent');
},
handle(handlerInput) {
const speakOutput = messages.welcome;
return handlerInput.responseBuilder
.speak(speakOutput)
.addAudioPlayerPlayDirective("REPLACE_ALL", url_jingle, "jingle", 0)
.getResponse();
}
};
const PlayBackFinishedHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'AudioPlayer.PlaybackFinished';
},
handle(handlerInput) {
if (handlerInput.requestEnvelope.request.token === 'jingle') {
return handlerInput.responseBuilder
.addAudioPlayerPlayDirective("REPLACE_ALL", url, token(), 0)
.getResponse();
}
}
};
I built an app in Slack, that on interactions in Slack, will send an HTTP POST request to a URL. That URL is a Firebase Function that is triggered with an HTTP request.
The Firebase Function looks like this...
// process incoming shortcuts
exports.interactions = functions.https.onRequest(async (request, response) => {
response.send();
const payload = JSON.parse(request.body.payload);
functions.logger.log(payload);
if (payload.type === 'shortcut') {
functions.logger.log('Found a shortcut...');
const shortcuts = require('./shortcuts');
await shortcuts(payload);
} else if (payload.type === 'block_actions') {
functions.logger.log('Found a block action...');
const blockActions = require('./blockActions');
await blockActions(payload);
} else if (payload.type === 'view_submission') {
functions.logger.log('Found a view submission...');
const viewSubmissions = require('./viewSubmissions');
await viewSubmissions(payload);
}
functions.logger.log('Done with interactions.');
});
The problem is, is that Firebase is taking 5-10 seconds to respond, and Slack is expecting a response in 3 seconds.
So the app in Slack erroring out.
It turns out while I thought it would be useful to do a response.send() immediately when the function was called so that Slack had its instant response, I was then also inadvertently starting background activities in Firebase.
The line in the above Firebase docs that gave me the biggest clue was:
Background activity can often be detected in logs from individual invocations, by finding anything that is logged after the line saying that the invocation finished.
Which I found here... the function started, and completed, and then the code to open a modal began to be executed...
I then found in the Firebase docs
Terminate HTTP functions with res.redirect(), res.send(), or res.end().
So all I really had to do was move response.send() to the end of the function. Also I had to make sure that I had await statements before my async functions, so that async functions waited to be resolved before executing the final response.send()
// process incoming shortcuts
exports.interactions = functions.https.onRequest(async (request, response) => {
const payload = JSON.parse(request.body.payload);
functions.logger.log(payload);
if (payload.type === 'shortcut') {
functions.logger.log('Found a shortcut...');
const shortcuts = require('./shortcuts');
await shortcuts(payload);
} else if (payload.type === 'block_actions') {
functions.logger.log('Found a block action...');
const blockActions = require('./blockActions');
await blockActions(payload);
} else if (payload.type === 'view_submission') {
functions.logger.log('Found a view submission...');
const viewSubmissions = require('./viewSubmissions');
await viewSubmissions(payload);
}
functions.logger.log('Done with interactions.');
response.send();
});
The modal interaction response times in Slack are much quicker and usable now.
I implemented a Firebase function to be called plainly on HTTPS via browser (I use postman for testing) in node.js :
exports.notifToAdmin = functions.https.onRequest((request, response) => {
const title = request.query.title
const body = request.query.body
const badge = request.query.badge
if (typeof title === 'undefined') { return response.status(500).send("title missing") }
if (typeof body === 'undefined') { return response.status(500).send("body missing") }
if (typeof badge === 'undefined') { return response.status(500).send("badge missing") }
notifications.sendNotifToAdmin(title, body, badge)
.then(message => {
const ackString = fingerPrint(msg);
return response.send(ackString);
})
.catch(error => {
console.error(error);
return response.status(500).send(error);
});
});
am I using a correct way to send errors back to the caller (via the response.status(500).send("...."))? In the Firebase errors documentation I see the usage of throw new Error(...). So I am unsure if what I do is the most optimal way? I did notice the doc saying //Will cause a cold start if not caught(linked to this throw error), I don't want to restart anything just report an error to the caller...
I know that the onRequest result should be a promise should I change my code and put a return in front of the notifications.SendNotifToAdmin(...) (this returns a promise) but how does this add up with the return response.send(...)? Is this also returning a promise then?
am I using a correct way to send errors back to the caller (via the response.status(500).send("...."))
Yes, that is standard for HTTP type functions that need to send an HTTP status code. But you should send a 4xx range HTTP codes for errors that are related to the client sending incorrect information.
I know that the onRequest result should be a promise
There is absolutely no obligation for an onRequest type function to return a promise. The function just needs to send a response after all promises are resolved so that the async work can complete before the function is terminated when the response is delivered.
i am struggling a bit with my google assistant action. Right now i am using Dialogflow and Firebase for my webhook. In my code i would like to get data from an API, for example this one: API. I am coding with Node.js by the way. Since Node is asynchronous i do not know how to get the Data. When i try to make an Callback it doesnt work e.g.:
app.intent(GetData, (conv) => {
var test= "error";
apicaller.callApi(answer =>
{
test = answer.people[0].name
go()
})
function go ()
{
conv.ask(`No matter what people tell you, words and ideas change the world ${test}`)
}
For some reason this works when i test it in an other application. With Dialogflow it does not work
I have also tried to use asynch for the function app.intent and tried it with await but this did not work too.
Do you have any idea how i could fix this?
Thank you in advance and best regards
Luca
You need to return Promise like
function dialogflowHanlderWithRequest(agent) {
return new Promise((resolve, reject) => {
request.get(options, (error, response, body) => {
JSON.parse(body)
// processing code
agent.add(...)
resolve();
});
});
};
See following for details:
Dialogflow NodeJs Fulfillment V2 - webhook method call ends before completing callback
If this works in another application then I believe you're getting an error because you're attempting to access an external resource while using Firebases free Spark plan, which limits you to Google services only. You will need to upgrade to the pay as you go Blaze plan to perform Outbound networking tasks.
Due to asynchronicity, the function go() is gonna be called after the callback of callapi been executed.
Although you said that you have tried to use async, I suggest the following changes that are more likely to work in your scenario:
app.intent(GetData, async (conv) => {
var test= "error";
apicaller.callApi(async answer =>
{
test = answer.people[0].name
await go()
})
async function go ()
{
conv.ask(`No matter what people tell you, words and ideas change the world ${test}`)
}
First follow the procedure given in their Github repository
https://github.com/googleapis/nodejs-dialogflow
Here you can find a working module already given in the README file.
You have to make keyFilename object to store in SessionsClient object creation (go at the end of post to know how to genearate credentials file that is keyFileName). Below the working module.
const express = require("express");
const bodyParser = require("body-parser");
const app = express();
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
//////////////////////////////////////////////////////////////////
const dialogflow = require("dialogflow");
const uuid = require("uuid");
/**
* Send a query to the dialogflow agent, and return the query result.
* #param {string} projectId The project to be used
*/
async function runSample(projectId = "<your project Id>") {
// A unique identifier for the given session
const sessionId = uuid.v4();
// Create a new session
const sessionClient = new dialogflow.SessionsClient({
keyFilename: "<The_path_for_your_credentials_file>"
});
const sessionPath = sessionClient.sessionPath(projectId, sessionId);
// The text query request.
const request = {
session: sessionPath,
queryInput: {
text: {
// The query to send to the dialogflow agent
text: "new page",
// The language used by the client (en-US)
languageCode: "en-US"
}
}
};
// Send request and log result
const responses = await sessionClient.detectIntent(request);
console.log("Detected intent");
console.log(responses);
const result = responses[0].queryResult;
console.log(` Query: ${result.queryText}`);
console.log(` Response: ${result.fulfillmentText}`);
if (result.intent) {
console.log(` Intent: ${result.intent.displayName}`);
} else {
console.log(` No intent matched.`);
}
}
runSample();
Here you can obtain the keyFileName file that is the credentials file using google cloud platform using
https://cloud.google.com/docs/authentication/getting-started
For complete tutorial (Hindi language) watch youtube video:
https://www.youtube.com/watch?v=aAtISTrb9n4&list=LL8ZkoggMm9re6KMO1IhXfkQ
In zapier I use an action of Code By Zapier. It's based on node.js.
I need to use fetch for implementing REST-API of my CRM.
Here is the code I wrote, which runs well when I tried it with VS Code (outside Zapier):
// the code by zapier includes already the require('fetch')
var api_token = "..."; // my api
var deal_name = "Example"; // a string
fetch("https://api.pipedrive.com/v1/deals/find?term="+deal_name+"&api_token=" + api_token)
.then(function(res) {
return res.json();
}).then(function(json) {
var deal_id = json.data[0].id;
console.log("deal_id="+deal_id);
}).catch(function(error) {
console.log("error");
});
output = {id: 1, hello: "world"}; // must include output...
The error I got from Zapier is:
If you are doing async (with fetch library) you need to use a
callback!
Please help me with solving it.
This is a classic mistake when coding in Node.js/callback environments.
You are using console.log which prints to your console, but doesn't return data to the parent (Zapier in this case).
Here is an example of bad and good code:
// bad code
fetch(url)
.then(function(res) {
return res.json();
}).then(function(json) {
// when i run this in my node repl it works perfect!
// the problem is this doesn't return the data to zapier
// it just prints it to the system output
console.log(json);
});
// good code
fetch(url)
.then(function(res) {
return res.json();
}).then(function(json) {
// but if i swap this to callback, this works perfect in zapier
callback(null, json);
});
I hope this helps!
These days, you can also use async/await, as noted by the default comment at the top of the sample code block:
// this is wrapped in an `async` function
// you can use await throughout the function
const response = await fetch('http://worldclockapi.com/api/json/utc/now')
return await response.json()
See further examples in the docs: https://zapier.com/help/create/code-webhooks/javascript-code-examples-in-zaps#step-2
Note that the free-tier has a 1 second timeout (especially relevant if you use Promise.all() to execute multiple fetches!)