I am trying to pass values from one intent to another.
This is how I set the context:
app.intent('welcome', async (conv) => {
const parameters = {
userId: "12345"
};
conv.contexts.set('userDoc', 5, parameters);
conv.followup('second', {})
})
This is how I try to retrieve the context parameters:
app.intent('second', (conv) => {
console.log(conv.userDoc); // returns undefined
})
However, conv.userDoc returns undefined.
This is my dialogflow setup:
Contexts are set as part of the reply that is sent back. Calling conv.followup() discards everything from the reply except for the event type and parameters that are sent.
In general - you probably don't want to actually use conv.followup() anyway. If you feel you need to return something from your webhook - just return it.
Related
I started implementing a slash-command which kept evolving and eventually might hit the 3-second slack response limit. I am using serverless-stack with Node and TypeScript. With sst (and the vscode launchfile) it hooks and attaches the debugger into the lambda function which is pretty neat for debugging.
When hitting the api endpoint I tried various methods to send back an acknowledgement to slack, do my thing and send a delayed message back without success. I didnt have much luck finding info on this but one good source was this SO Answer - unfortunetly it didn't work. I didn't use request-promise since it's deprecated and tried to implement it with vanilla methods (maybe that's where i failed?). But also invoking a second lambda function from within (like in the first example of the post) didn't seem to be within the 3s limitation.
I am wondering if I am doing something wrong or if attachinf the debugger is just taking to long etc.
However, before attempting to send a delayed message it was fine including accessing and scaning dynamodb records, manipulating the results and then responding back to slack while debugger attached without hitting the timeout.
Attempting to use a post
export const answer: APIGatewayProxyHandlerV2 = async (
event: APIGatewayProxyEventV2, context, callback
) => {
const slack = decodeQueryStringAs<SlackRequest>(event.body);
axios.post(slack.response_url, {
text: "completed",
response_type: "ephemeral",
replace_original: "true"
});
return { statusCode: 200, body: '' };
}
The promise never resolved, i guess that once hitting return on the function the lambda function gets disposed and so the promise?
Invoking 2nd Lambda function
export const v2: APIGatewayProxyHandlerV2 = async (
event: APIGatewayProxyEventV2, context, callback
): Promise<any> => {
//tried with CB here and without
//callback(null, { statusCode: 200, body: 'processing' });
const slack = decodeQueryStringAs<SlackRequest>(event.body);
const originalMessage = slack.text;
const responseInfo = url.parse(slack.response_url)
const data = JSON.stringify({
...slack,
})
const lambda = new AWS.Lambda()
const params = {
FunctionName: 'dev-****-FC******SmE7',
InvocationType: 'Event', // Ensures asynchronous execution
Payload: data
}
return lambda.invoke(params).promise()// Returns 200 immediately after invoking the second lambda, not waiting for the result
.then(() => callback(null, { statusCode: 200, body: 'working on it' }))
};
Looking at the debugger logs it does send the 200 code and invokes the new lambda function though slack still times out.
Nothing special happens logic wise ... the current non-delayed-message implementation does much more logic wise (accessing DB and manipulating result data) and manages not to timeout.
Any suggestions or help is welcome.
Quick side note, I used request-promise in the linked SO question's answer since the JS native Promise object was not yet available on AWS Lambda's containers at the time.
There's a fundamental difference between the orchestration of the functions in the linked question and your own from what I understand but I think you have the same goal:
> Invoke an asynchronous operation from Slack which posts back to slack once it has a result
Here's the problem with your current approach: Slack sends a request to your (1st) lambda function, which returns a response to slack, and then invokes the second lambda function.
The slack event is no longer accepting responses once your first lambda returns the 200. Here lies the difference between your approach and the linked SO question.
The desired approach would sequentially look like this:
Slack sends a request to Lambda no. 1
Lambda no. 1 returns a 200 response to Slack
Lambda no. 1 invokes Lambda no. 2
Lambda no. 2 sends a POST request to a slack URL (google incoming webhooks for slack)
Slack receives the POST requests and displays it in the channel you chose for your webhook.
Code wise this would look like the following (without request-promise lol):
Lambda 1
module.exports = async (event, context) => {
// Invoking the second lambda function
const AWS = require('aws-sdk')
const lambda = new AWS.Lambda()
const params = {
FunctionName: 'YOUR_SECOND_FUNCTION_NAME',
InvocationType: 'Event', // Ensures asynchronous execution
Payload: JSON.stringify({
... your payload for lambda 2 ...
})
}
await lambda.invoke(params).promise() // Starts Lambda 2
return {
text: "working...",
response_type: "ephemeral",
replace_original: "true"
}
}
Lambda 2
module.exports = async (event, context) => {
// Use event (payload sent from Lambda 1) and do what you need to do
return axios.post('YOUR_INCOMING_WEBHOOK_URL', {
text: 'this will be sent to slack'
});
}
My problem is passing parameters between intents' hooks.
As example: at one hook I made a request for my own backend and receive a sessionId. I need pass the sessionId to an another intent for second request in it's hook.
How I can do it? I can't find any examples, documentations or best practices.
Thanks for your attention!
You can do this with contexts.
In your first intent you do something like this:
function firstIntentHandler(agent){
return axios.get(`https://yourserver.com`)
.then(function (response) {
agent.add(response.data.sessionId);
const context = {'name': 'yourcontextname', 'lifespan': 3, 'parameters':
{'sessionid': response.data.sessionId }};
agent.setContext(context);
}).catch(function (error) {
agent.add("An error occured.");
});
}
sessionId is the variable name in your json data sent by your server.
In your other intent you can access this data like this:
function otherIntentHandler(agent) {
const context = agent.getContext("yourcontextname");
const sessionid = context.parameters.sessionid;
// do something with this session id
});
}
Be careful with camelcase for your context name. I have the feeling that uppercase letters are stored as lowercase...
I would like to pass along the Twilio phone number's Friendly Name when I forward the SMS message to another phone number. (The Friendly Name is a property assigned to a Twilio number when it is purchased.)
This is not the same as "Send Branded SMS Messages Using Twilio Alphanumeric Sender ID" described here and here.
I am operating in the USA where the above is not supported, and I am OK including the "Friendly Name" in the body of the forwarded message (or the subject line of the email). Also, The "Friendly Name" is not associated with the original sender of the message as in the above examples, but instead is associated with my Twilio numbers. Hope that's all clear.
I am using this example code:
Forwarding SMS messages to another phone number – Twilio Support
Three parameters are passed into the function:
exports.handler = function(context, event, callback) {
context includes environment variables I configure. callback isn't relevant to this question. And for the event parameter, we have these properties:
AccountSid
ApiVersion
Body
From
FromCity
FromCountry
FromState
FromZip
MessageSid
NumMedia
NumSegments
SmsMessageSid
SmsSid
SmsStatus
To
ToCity
ToCountry
ToState
ToZip
Using Twilio Functions, I want to obtain Friendly Name, which is not a property of event. Is it possible to obtain the Friendly Name through one of the other properties? If so, how?
UPDATE: from this Twilio doc I see a clue that I can possibly get Friendly Name from AccountSid.something.outgoing_caller_id.friendlyName
I can't quite understand how to do that in a Twilio function. I tried using:
context.getTwilioClient();
The docs say that "will return an initialized REST client that you can use to make calls to the Twilio REST API." However, it returns an empty httpClient object along with strings for username, password and accountSID. I was expecting a populated object from which I could obtain the phone number's Friendly Name.
As an aside, I would like to ask what objects get passed into the event parameter. In what circumstances would the event parameter contain different properties from those I listed above?
You are well on the right path! Excellent research. In fact, context.getTwilioClient() is part of it. Once you have the Twilio REST API client initialized, you can use another Twilio API to determine what the FriendlyName is from the event.To. I found that here, Filter IncomingPhoneNumbers with exact match.
Below is one way, there certainly may may be others.
const got = require('got');
// async function to deal with async REST API call
exports.handler = async function(context, event, callback) {
const client = context.getTwilioClient();
// await an async response
await client.incomingPhoneNumbers
.list({phoneNumber: event.To, limit: 1})
.then(incomingPhoneNumbers => event.friendlyName = incomingPhoneNumbers[0].friendlyName)
.catch(err => console.log(err));
const requestBody = {
personalizations: [{ to: [{ email: context.TO_EMAIL_ADDRESS }] }],
from: { email: context.FROM_EMAIL_ADDRESS },
subject: `New SMS message from: ${event.From}`,
content: [
{
type: 'text/plain',
value: `${event.Body} - ${event.friendlyName}`
}
]
};
got
.post('https://api.sendgrid.com/v3/mail/send', {
headers: {
Authorization: `Bearer ${context.SENDGRID_API_KEY}`,
"Content-Type": 'application/json'
},
body: JSON.stringify(requestBody)
})
.then(response => {
console.log(response);
let twiml = new Twilio.twiml.MessagingResponse();
twiml.message({to: '+1555xxxxxxx'}, `You Message: ${event.Body} - ${event.friendlyName}`);
callback(null, twiml);
})
.catch(err => {
console.log(err);
callback(err);
});
};
Specific to the key's associated with the event object, this is a handy one to use.
Object.keys(event).forEach( thisKey => console.log(`${thisKey}: ${event[thisKey]}`));
We are using firebase cloud functions for our fulfillment and an external rest api for our crud operations.
We have one intent with a few followups and it looks like this
- Create Note
- - Create Note - fallback*
- - - Create Note - fallback - yes
- - - Create Note - fallback - no
* the fallback allows us to capture free text
In our webhook I have the following fulfillment
app.intent("Create Note", (conv) => {
if (!conv.user.storage.note) conv.user.storage.note = {};
conv.ask("Go ahead, talk to Talkatoo!");
});
app.intent("Create Note - fallback", (conv) => {
conv.user.storage.input = conv.input.raw;
let response = conv.user.storage.note
? conv.user.storage.note.body.concat(conv.input.raw)
: conv.user.storage.input;
conv.ask("So far this is what I heard you say, let me know if this is complete. ", response);
});
app.intent("Create Note - fallback - yes", (conv) => {
// based on the conv.user.storage.note object
// either make a call to create a note or update a note
// make call to external api and based on the response
// set the value for conv.user.storage.note
conv.ask("Great news, let me save that for you!");
});
app.intent("Create Note - fallback - no", (conv) => {
// based on the conv.user.storage.note object
// either make a call to create a note or update a note
// make call to external api and based on the response
// set the value for conv.user.storage.note
// Send the user back to Create Note to capture the rest of their input
conv.followup("my_custom_event");
});
The issue is that conv.user.storage.note is getting set when I get the response from the API, but then it is getting reset to empty and so a new note is created each time. I'm trying to append the various inputs from the user to be one note
Based on ...
app.intent("Create Note - fallback - yes", (conv) => {
// based on the conv.user.storage.note object
// either make a call to create a note or update a note
// make call to external api and based on the response
// set the value for conv.user.storage.note
conv.ask("Great news, let me save that for you!");
});
it looks like either you haven't written the code for the API call yet, or you removed that code before you posted on StackOverflow. Is that correct?
Can you update your question to show where and how you send an HTTP request to your external REST API?
For anyone who this might end up helping, I found a post and realized that dealing with responses to async calls might need more thought (to work in work arounds) How to make asynchronous calls from external services to actions on google?
For our use case we didn't need the response data ie, we didn't need to communicate any of the response data to the user, so instead we saved the user's multiple inputs in user.storage and when they were complete we save the full response.
app.intent("Intent - fallback", (conv) => {
conv.user.storage.input = conv.user.storage.input
? conv.user.storage.input.concat(conv.input.raw)
: conv.input.raw;
conv.ask("Is that complete or would you like to continue?");
});
app.intent("Intent - fallback - yes", (conv) => {
createNote(conv); <--- makes call to save
conv.ask("Great news, let me save that for you!");
});
app.intent("Intent - fallback - no", (conv) => {
conv.followup("my_custom_event");
});
I am trying to build a simple chatbot with DialogFlow.
My aim is to give information from user question, like : where can I slackline above water in croatia ? I have two parameters (croatia, waterline) and a list of slackline places.
So I need a data base to retrieve information from parameters. DialogFlow allows fulfillment with Firebase. I build a database with places (name, country, type of slack) and enable webhook call for my intent.
I use Inline Editor and index.js
const parameters = request.body.queryResult.parameters;
var country = parameters.country.toString();
function show(snap) {
console.log('snap');
agent.add(JSON.stringify(snap.val(),null,2));
}
function slkplc(agent) {
var testRef;
firebase.database().ref('slackplace').once('value',show);
}
// Run the proper function handler based on the matched Dialogflow intent name
let intentMap = new Map();
intentMap.set('slack place', slkplc);
agent.handleRequest(intentMap);
But I do not get the expected result while trying it on DialogFlow or Google Assistant. The function show is asynchronously called but too late and the response is not available for DialogFlow :
I see three way to deal with this problem :
use blocking call to database : another database ?
treat asynchronous message with DialogFlow ???
response to user that an error occured.
The third that I choose, but it is always on error.
After trying several things to wait data from database response, the only thing I managed is to freeze the response, therefore the timeout of DialogFlow - 5s -and Firebase - 60s - were reached.
A workaround
Another way to do it is to separate database acquisition and request/response from DialogFlow. The data of database is collected outside of the dialogflowFirebaseFulfillment
var data;
var inidata = firebase.database().ref().on('value',function(snap) {
console.log('snap');
data = snap.val();
});
exports.dialogflowFirebaseFulfillment = functions.https.onRequest((request, response) => {
const agent = new WebhookClient({ request, response });
...
function slkplc(agent) {
agent.add(JSON.stringify(data,null,2));
}
// Run the proper function handler based on the matched Dialogflow intent name
let intentMap = new Map();
intentMap.set('slack place', slkplc);
agent.handleRequest(intentMap);
}
Now I can do what I want with data, and I am able to find the place where I can practice waterline in croatia. But there is always something weird, the data of the database is duplicated ...
The "right" solution is option 2 that you suggest: since you're doing an asynchronous call, you need to handle this correctly when working with the dialogflow-fulfillment library.
Basically, if your handler makes an asynchronous call, it needs to be asynchronous as well. To indicate to the handleRequest() method that your handler is async, you need to return a Promise object.
Firebase's once() method returns a Promise if you don't pass it a callback function. You can take advantage of this, return that Promise, and also handle what you want it to do as part of a .then() clause. It might look something like this:
function slkplc(agent) {
var testRef;
return firebase.database().ref('slackplace').once('value')
.then( snap => {
var val = snap.val();
return agent.add( JSON.stringify( val, null, 2 ) );
});
}
The important part isn't just that you use a Promise, but also that you return that Promise.