Asynchronous webhooks with IBM Watson Assistant
Webhooks must return before a configured timeout or else they fail. See how your chatbot can use web services that require asynchronous handling.
Download the complete example here: https://github.com/spackows/watson-assistant-async
Webhook timeout
Webhook calls by a Watson Assistant chatbot are synchronous: When your chatbot calls a webhook, an answer must be returned within the configured timeout. Otherwise, the webhook call fails.
Web chat API
There are multiple ways to implement a solution for using asynchronous web services with chatbot webhooks. This post shows how you can use the Watson Assistant web chat API to accomplish this:
- Step 1: Build a simple chatbot dialog
- Step 2: Publish a web chat integration
- Step 3: Deploy the sample Node.js app
- Step 4: Update the chat dialog to call a webhook
- Step 5: Message the chat using the web chat API
- Step 6: Handle the async result message in the chatbot
- Step 7: Update the webhook to use /asyncwrapper
Step 1: Build a simple chatbot dialog
In the Watson Assistant web interface (classic experience shown) create a dialog skill with two nodes:
- A “Welcome” node, triggered by the welcome condition, that prompts users to enter data to be processed
- An “Anything else” node, triggered by the anything_else condition, that prints a generic input-not-recognized message
Step 2: Publish a web chat integration
In the Watson Assistant web interface, perform these steps:
2.1 On the Assistants page, create an assistant
2.2 Add the dialog skill created in the previous step to the assistant
2.3 Add a web chat integration to the assistant
2.4 From the Embed tab of the Web chat integration page, collect the integrationID
, region
, and serviceInstanceID
(you’ll need them in the next step)
Step 3: Deploy the sample Node.js app
The sample solution for this blog post is a Node.js web app. To use the sample app with a Watson Assistant chatbot, deploy the app — on IBM Cloud using IBM Code Engine, for example.
Source: https://github.com/spackows/watson-assistant-async
Highlights
In the file server.js, you can see:
- GET
/chatbot
endpoint (views/pages/chatbot.ejs) for viewing the web chat integration in a browser
g_app.get( "/chatbot", function( request, response )
{
var parms = { "integration_id" : integration_id,
"region" : region,
"service_instance_id" : service_instance_id,
"base_url" : g_base_url }; response.render( "pages/chatbot", parms );
} );
- POST
/asyncendpoint
a simple endpoint that takes long enough to respond that using it directly in Watson Assistant as a webhook fails
g_app.post( "/asyncendpoint", function( request, response )
{
var str = request.body.str ? request.body.str : ""; const myTimeout = setTimeout( function()
{
// Wait 10 seconds before responding // Note: Just a random thing to show results.
// *Only works for ASCII text.
var result = str.split("").reverse().join("");
response.status( 200 ).json( { "error_str" : "",
"result" : result } );
}, 10 * 1000 );
} );
- POST
/asyncwrapper
and endpoint for your chatbot to call as a webhook
g_app.post( "/asyncwrapper", function( request, response )
{
var str = request.body.str ? request.body.str : "";
// 1. Respond right away so the chatbot can move on
// 2. Call the async web service
// 3. After the web service responds, send a message
// to the chatbot using the chatbot web api
response.status( 200 ).json( { "error_str" : "",
"result" : "Success" } );
callAsyncWebService( str, function( error_str, result_data )
{
if( error_str )
{
sendMessage( { "error_str" : error_str } );
return;
}
sendMessage( result_data );
} );
} );
The app requires several environment variables to be set:
- WA_INTEGRATION_ID : The
integrationID
collected in the previous step - WA_REGION : The
region
collected in the previous step - WA_SERVICE_INSTANCE_ID : The
serviceInstanceID
collected in the previous step - BASE_URL : Should be set to
http://localhost:8080
when running the app on your local computer and set to the base url of your deployed app when deployed in the Cloud
Step 4: Update the chat dialog to call a webhook
In the Watson Assistant web interface, add a webhook:
4.1 In the webhooks section of the skill-editing page, enter the url of your deployed app, with the /asyncendpoint
endpoint specified
4.2 Add a child node to the “Welcome” node, called “Calling webhook”. Set the condition of the new node to be anything_else. The new node has the sole job of printing the status message ‘Calling the async webhook …’. Set the “Welcome” node to jump to the child node, wait for user input, and then evaluate the condition.
4.3 Add a child node to the “Calling webhook” node, called “Call async wrapper webhook”, with a condition of anything_else. Customize the new node to enable Callout to webhooks. Specify one parameter:
- Key :
str
- Value :
"<? input.text ?>"
In the Assistant responds section, simply print the webhook result.
4.4 Use the Try it button to test the chatbot. The webook will time out.
Step 5: Message the chat using the web chat API
The easiest way to test the next step is by running the sample node.js app on your local computer.
5.1 In a command window, set the environment variables listed in step 3.
5.2 Run the sample app using the command node server.js
5.3 Open http://localhost:8080/chatbot
in a browser. The Watson Assistant chatbot icon appears in the lower-right corner. Click the icon to start the chat. Manually type the same input as with the Try it window befor. Notice the webhook call to /asyncendpoint
times out just like before.
5.4 In a separate command window, use cURL to call the /asyncwrapper
endpoint with some sample text:
> curl -X POST http://localhost:8080/asyncwrapper -d str="bla bla bla"
You will see results in the command window:
{ "error_str" : "", "result" : "Success" }
And you will see a message sent to the chat in your browser:
Explanation
The /asyncwrapper
endpoint called the callAsyncService
routine:
[server.js]
function callAsyncWebService( str, callback )
{
var url = g_some_async_webservice;
var data = { "str" : str };
g_axios.post( url, data ).then( function( result )
{
if( 200 !== result.status )
{
callback( "async call failed", {} );
return;
}
callback( "", result.data );
} ).catch( function( error )
{
callback( error.message, {} );
} );
}
(You would implement your asynchronous call and associated logic in the callAsyncService
routine — such as implementing polling, for example.)
The /asyncwrapper
endpoint then calls sendMessage
to send the results from callAsyncService
to the chatbot integration web page using socket.io:
[server.js]
var g_socket_p = null;
const g_io = g_socketio( g_server );
g_io.on( "connection", function( socket )
{
g_socket_p = socket;
} );function sendMessage( data )
{
g_socket_p.emit( "asyncresult", data );
}
The Javascript in the integration web page uses the Watson Assistant web chat API send method to pass the results to the chat in a message:
[chatbot.ejs]
socket.on( "asyncresult", function( data )
{
messageChatbot( data.result );
} );...
function messageChatbot( txt )
{
var send_obj = { "input": { "message_type" : "text",
"text" : "ASYNCRESPONSE " + txt } };
g_wa_instance.send( send_obj, {} );
}
Step 6: Handle the async result message in the chatbot
In the Watson Assistant web interface, update the chat dialog to handle the message coming from the web chat API:
6.1 Add a pattern entity, called “ASYNCRESPONSE”, with the pattern: ASYNCRESPONSE
6.2 Add a dialog node just above the “Anything else” node, called “ASYNCRESPONSE”. Set the condition to recognize the ASYNCRESPONS Eentity just created.
6.3 In the “ASYNCRESPONSE” dialog node, open the context menu, and then set a user-defined variable, called $WEBHOOK_RESULT
to the value:
"<? input.text.extract( '\\s+(.*)$', 1 ) ?>"
Read more about using a regular expression to extract part of an input string here: Expression language
6.4 In the Assistant responds section of the “ASYNCRESPONSE” dialog node, return a multiline response:
Webhook response received:
<? $WEBHOOK_RESULT ?>
Processing...
(Your chatbot would go on to process the result from the asynchronous web service.)
Explanation
In Step 5, the Javascript method sendMessage
(in [chatbot.ejs]) used the web chat API to send a message to the chat. And sendMessage
prepended the text with "ASYNCRESPONSE "
. Because that string matches the pattern entity just added to the dialog, prepending the message with that string causes the chatbot dialog to recognize that the message contains the response to a call to an asynchronous web service.
If you rerun the test with the curl command from Step 5, the final reply from the chatbot won’t be from the “Anything else node”, it will be from your new “ASYNCRESPONSE” node:
Step 7: Update the webhook to use /asyncwrapper
In the Watson Assistant web interface, update the webhook to point to the /asyncwrapper
endpoint.
When you run through steps 5.1 to 5.3 on the deployed chatbot web page, you’ll see that the asynchronous results appear in the chat:
Notes:
- In this example, the text that the web page Javascript sends to the chat (“ASYNCRESPONSE alb alb alb”) is visible in the chat just to make it easier to follow what’s going on. In your production solution, you can set the
silent
option to “true” to hide the message from the chat. - Designing the dialog to handle on-going chat with the user while waiting for asynchronous results is not included in this blog post (to keep this post focused on the API aspects of the solution.)
Conclusion
Using features such as the web chat API, you can create feature-rich chatbot solutions with Watson Assistant that integrate countless web services — including ones that require asynchronous handling.