dialogue-builder
v0.3.0
Published
Beautifully readable dialogue for facebook messenger bots
Downloads
11
Maintainers
Readme
Dialogue Builder
The goal of this library is to enable you to write bot dialogue in JavaScript or TypeScript. It utilizes template literals to enable you to write dialogue in a highly readable way, making it easier to review the dialogue at a glance, it currently has been designed to work with Facebook Messenger bots only. See dialogue-builder-example for a working example.
exports.default = dialogue('Onboarding ', (name) => [
say `Hi ${name}, welcome to nosy bot!`,
say `This inquisitive little bot will ask a bunch of questions for no reason`,
say `It will log your answers pointlessly to the console`,
say `You can always type back if you make mistake`,
ask `How old are you?`,
expect `My age is`, {
[onText]: (text) => console.log(`${name}'s age is ${text}`)
},
ask `What length is your hair?`,
expect `My hair length is`, {
'Long': (text) => console.log(`${name}'s hair is ${text}`),
'Short': (text) => console.log(`${name}'s hair is ${text}`),
'Shaved': (text) => {
console.log(`${name}'s hair is ${text}`);
return goto `after_hair_colour`;
},
},
ask `What colour is your hair?`,
expect `My hair colour is`, {
'Black': (text) => console.log(`${name}'s hair colour is ${text}`),
'Brown': (text) => console.log(`${name}'s hair colour is ${text}`),
'Blonde': (text) => console.log(`${name}'s hair colour is ${text}`),
},
'after_hair_colour',
ask `Where do you live?`,
expect `I live at`, {
[location]: (lat, long, title, url) => console.log(`User located at ${lat}, ${long}`)
},
say `Thanks ${name}, have a nice day`,
]);
Installing
npm install dialogue-builder
How it works
If the example dialogue above was defined in a file called onboarding.js
you would write the following to get your bot to follow it:
const botBuilder = require('claudia-bot-builder');
const { Dialogue } = require("dialogue-builder");
const onboarding = require('onboarding');
module.exports = botBuilder(function (message, apiRequest) {
const dialogue = new Dialogue(onboarding, {
store(state: Object) => console.log('Need to persist this somewhere')
retrieve() => return Promise.resolve(new Object())
}, 'Dave');
dialogue.setKeywordHandler('back', 'undo');
return dialogue.consume(message, apiRequest);
});
Dialogue builder is built on top of the excellent bot-builder by claudia.js and the code above is the entry point for a bot builder project.
Each invocation of the function above is caused by an incoming message from the user. The consume
method called above would continue the dialogue where it left off, calling any responses handlers for the incoming message and returning the next set of outgoing messages.
Except, in the example above, the bot would simply repeat the beginning of the dialogue each time the user sent a message because the storage handler (the store
and retrieve
methods) is not persisting the internal dialogue state (which is a JSON object). You would normally store this state under your user record in the persistence storage mechanism on your choosing. See dialogue-builder-example for an example on how to implement this using DynamoDB.
API
The dialogue-builder
module exports the following interface:
dialogue
functionsay
,ask
,audio
,video
,image
,file
,expect
,goto
template literal tag functionsbuttons
,list
functionsonText
,location
,onLocation
,onImage
,onAudio
,onVideo
,onFile
,defaultAction
symbolsUnexpectedInputError
classDialogue
classmock
namespace
dialogue
function
dialogue(name: string, script: (...context: any) => Array<BaseTemplate | Label | Expect | Goto | ResponseHandler>): DialogueBuilder`
This function is used to define your script, the first arg is the name of your dialogue (not shown to the user) and the second is your script function which should return an array (the lines of your script). This function is passed any custom args you passed to the Dialogue constructor.
say
, ask
, audio
, video
, image
, file
, expect
, goto
template literal tag functions
The array passed to the dialogue function form the lines of your script, an element in this array has to be one of:
say
string: Your bot will simply repeat the string passed inask
string: Identical tosay
except onlyask
statements are repeated on undo or an unhandled responseaudio
string: Send an audio file, the string passed in should be a urlvideo
string: Send a video file, the string passed in should be a urlimage
string: Send an image file, the string passed in should be a urlfile
string: Send a file, the string passed in should be a urlexpect
string: This statement marks a break in the dialogue to wait for a user response. The string you pass is the response you expect from the user, it's used as a key when persisting the state of the conversation and so must be a string unique amongst all expect statements. An expect statement must always be immediately followed by aResponseHandler
goto
string: A goto statement will cause the dialogue to jump to another location in the script. The string you pass in specifies the label to jump to.goto
statements can also be returned from aResponseHandler's
methods- string: Any untagged strings in the array are treated as labels which serve as the destination of goto statements. When gathering the next set of outgoing messages, the dialogue will fall through labels by default. You can override this behavior by prefixing your label with an exclamation mark (!) - this causes the dialogue to break at the label. Once dialogue has stopped at a label only a goto statement will restart it (use this feature when you want to wait for postback)
fbTemplate.BaseTemplate
: You can embed any facebook message type supported by bot builder directly in your script, see Facebook Template Builder for more info
buttons
, list
functions
type ButtonHandler = { [title: string]: () => Goto | void }
type Bubble = [string, string, string, ButtonHandler]
function buttons(id: string, text: string, handler: ButtonHandler): Button
function list(id: string, type: 'compact' | 'large', bubbles: Bubble[], handler: ButtonHandler): List
The buttons
function allows you to send a Button Template in your script. The first arg must be a string unique amongst all templates defined in your script, the second is the title to display to the user,and the third is your button handler object which defines the buttons names and handler functions in the same way as quick replies in a ResponseHandler
, it returns a Button
The list
function allows you to send a List Template in your script. The first arg must be a string unique amongst all templates defined in your script, the second is the list type, the third is an array of bubbles in your list, and the forth is a button handler object which defines the button names and handler function in the same way as quick replies in a ResponseHandler
, it returns a List
location
, onText
, onLocation
, onImage
, onAudio
, onVideo
, onFile
, defaultAction
symbols
A ResponseHandler
is an object who's methods are called on receiving a message from the user to handle the response to the immediately preceding expect
statement. The supported methods are:
- string
(text?: string)
: A string property causes a quick reply to be attached to the last outgoing message, the function is called on the user selecting the reply, the text passed in will always be the same as the function name [location](lat: number, long: number, title?: string, url?: string)
: Thelocation
symbol property causes a location quick reply to be attached to the last outgoing message, the function is called on the user selecting the reply[onText](text: string)
: TheonText
symbol property is called when the user types a text response that doesn't match any of the quick replies[onLocation](lat: number, long: number, title?: string, url?: string)
: TheonLocation
symbol property is called when the user types a sends a location, you cannot define bothlocation
andonLocation
properties on the same response handler[onImage](url: string)
: TheonImage
symbol property is called when the user sends an image[onAudio](url: string)
: TheonAudio
symbol property is called when the user sends an audio recording[onVideo](url: string)
: TheonVideo
symbol property is called when the user sends a video[onFile](url: string)
: TheonFile
symbol property is called when the user sends a file[defaultAction]()
: ThedefaultAction
symbol property is called when for any action if no other property matches the user's response so can be used as a catch all. It is also used to specify the default action on buttons and lists
All response handler methods support returning one of Goto | Expect | void
, you can also return a promise resolving to one of the same set of types: Promise<Goto | Expect | void>
Returning a goto statement from a ResponseHandler
method will cause the dialogue to jump to the specified label
Returning a expect statement from a ResponseHandler
method will delegate the handling of the response to the relevant handler function of the response handler defined for the expect statement specified
UnexpectedInputError
class
class UnexpectedInputError {
constructor(message: string, repeatQuestion?: boolean)
}
When a ResponseHandler
recieves a message from the user for which is does not contain a handler method for an instance of UnexpectedInputError
is thrown, this will cause the question to be repeated. You can invoke this behaviour in a handled response by throwing this error from the handler method.
The string you pass to the constructor will be sent to the user followed by repeating the question (the ask statements). If you don't want to repeat the question, pass true
as the second constructor arg
Dialogue
class
class Dialogue {
constructor(builder: DialogueBuilder, storage: Storage, ...context: any)
baseUrl: string
execute(directive: Goto | Expect)
consume(message: Message, apiRequest: Request): Promise<any[]>
setKeywordHandler(keywords: string | string[], handler: 'restart' | 'undo' | (() => void | Goto)): void
}
The Dialogue
class constructor has two required args, the first is the dialogue (the return value from the dialogue
function and the second is the storage handler, you need to pass an object conforming to the following interface to store the dialogue state, typically under your user record in a persistence storage mechanism on your choosing:
interface Storage {
store(state: Object): Promise<void>
retrieve(): Promise<Object>
}
Any additional args passed to the constructor are passed to the dialogue
function this would typically be used to pass through the user's details to customize the dialogue plus any object needed in the ResponseHandlers
to act on user responses.
Setting the baseUrl
property allows you to pass uris into functions that would normally expect a full url, such as audio
, video
, image
, file
template literal tag functions and the buttons
, list
functions
Call the execute
method to jump to another location in the script specified by a goto
or expect
statement. This is useful for writing unit tests in combination with the mock
namespace
Call the consume
method to process the input from the user, you need pass in the message and apiRequest from your bot builder handler method, you can return the result of this method directly from your bot builder handler method.
Call the setKeywordHandler
method to create a keyword which will trigger the callback passed in whenever the user sends any of the keywords passed as the first arg, at any point in the conversation. The callback can return a goto statement to cause the dialogue to jump to the specified label.
Two built-in keyword handlers exist, which you can assign keywords to by replacing the callback with either undo
or restart
undo
The undo keyword handler will repeat the last question asked in the dialogue, allowing the user to correct a mistake
restart
The restart keyword handler will reset the dialogue to the beginning and is useful to enable during development: TIP: Set your restart keyword to match your Get Started button call to action payload so when users delete the conversation and initiate a new one your dialogue will begin from the start
mock
namespace
export namespace mock {
const apiRequest: Request
function message(text: string): Message
function postback(payload?: string): Message
function location(lat: number, long: number, title?: string, url?: string): Message
function multimedia(type: 'image' | 'audio' | 'video' | 'file' | 'location', url: string): Message
}
The constants and functions defined in the mock
namespace allow you to easily mock input when calling the consume
method of the Dialogue
class, for example:
dialogue.consume(mock.message('Hi'), mock.apiRequest)
Behavioral specifications
- it passes the supplied context to the script method
- it throws an exception on empty script given
- it throws an exception on script only containing labels
- it sends the first and only message in a single message dialogue
- it sends all messages with NO_PUSH notification type
- it throws empty array on consume when complete
- it sends multiple messages at once with pauses and typing indicators in between
- it ensure total pauses are less then 10 seconds when sending multiple messages at once
- it trims extraneous whitespace in messages
- it supports bot builder template class instances inline
- it supports null lines
- it throws an exception on script with duplicate expect statements
- it throws an exception on script with duplicate template ids
- it throws an exception on expect statement not followed by a response handler
- it throws an exception on a response handler not preceded by an expect statement
- it pauses on expect to wait for a response
- it resumes where it paused on receiving a response
- it reevaluates a script after executing a response handler
- it reevaluates a script after executing a keyword handler
- it reevaluates a script after executing a postback handler
- it attaches any quick replies defined in response handler to last message
- it attaches location quick reply if defined in response handler
- it supports promises being returned from response handlers
- it invokes a quick reply's handler on receiving the reply
- it invokes a button handler on receiving the postback
- it invokes a list bubble's button handler on receiving the postback
- it supports empty handlers
- it supports null handlers
- it throws an error when both location and onLocation specified on a handler
- it prefers a quick reply handler to the onText handler
- it invokes the location handler on receiving a location quick reply
- it invokes the onText handler on receiving a text response
- it invokes the onLocation handler on receiving a location response
- it invokes the onImage handler on receiving an image response
- it invokes the onVideo handler on receiving an video response
- it invokes the onAudio handler on receiving an audio response
- it invokes the onFile handler on receiving an file response
- it invokes the defaultAction handler if no other more suitable handler defined
- it invokes the defaultAction handler if no other more suitable handler defined
- it prefers any suitable handler over the defaultAction handler
- it handles unexpected response types by repeating only the ask statements
- it supports the throwing of UnexpectedInputError from response handlers
- it does not repeat the question when repeatQuestion arg to UnexpectedInputError constructor is false
- it falls through a label not prefixed with an exclamation mark
- it breaks on hitting a label prefixed with an exclamation mark
- it respects inline gotos
- it respects gotos executed on the dialogue instance
- it jumps to an expect executed on the dialogue instance
- it respects gotos returned from response handlers
- it throws an exception on calling goto with a missing label
- it throws an exception on script with duplicate labels
- it aborts a goto that causes an endless loop
- it resumes from the correct line when a goto skips a response handler
- it supports expects returned from response handlers to delegate handling
- it aborts an expect returned from response handler that causes an endless loop
- it ignores return values from handlers if not gotos or expects
- it calls a keyword handler when message is received that matches insensitive of case
- it calls a keyword handler matching a postback payload if no postback handler found
- it prefers a matching keyword handler over the current response handler
- it resets the dialogue when user sends a restart keyword
- it returns to previously asked question when user sends a undo keyword
- it returns to last asked question when user sends a undo keyword when complete
- it accounts for skipped questions due to goto statements when user sends a undo keyword
- it supports a user sending an undo or restart keyword at the start of a dialogue
- it prefixes uris with the baseUrl but leaves full urls as is