@servisbot/conversation-runtime
v3.6.0
Published
Runtime SDK for Interacting with ServisBOT conversations
Downloads
51
Readme
conversation-runtime
Runtime SDK for Interacting with ServisBOT conversations
Install
npm install @servisbot/conversation-runtime
Using the Module
Quick Start
Creating a Conversation & Sending/Receiving Messages
if you are looking for synchronous mode see below
import { ConversationConnector, ConversationChannelTypes, IngressEventFactory } from '@servisbot/conversation-runtime';
const organization = 'acme';
const apiKey = '<servisbot-conversation-api-key>'
const endpointAddress = 'acme-MyBot';
// can be "eu-1", "us1" or "usccif1"
const sbRegion = 'us1';
const logger = console;
const conversationConnector = await ConversationConnector.createConnector({
channelType: ConversationChannelTypes.OnDemandChannelRestWs,
apiKey,
endpointAddress,
logger,
sbRegion,
organization,
});
// setup conversation event listeners before creating the conversation
conversationConnector.on('message', (msg) => console.log('MESSAGE', msg));
conversationConnector.on('error', (err) => console.log('ERROR', err));
await conversationConnector.createConversation({
engagementAdapter: 'L2', engagementAdapterVersion: '1.0.0'
});
const message = IngressEventFactory.createMessage({ message: 'hello world' })
await conversationConnector.send(message);
Note
- To close the web socket connection add the following to the end of the examples
await conversationConnector.close();
Importing the Module
const { ConversationConnector } = require('@servisbot/conversation-runtime'); // es5
import { ConversationConnector } from '@servisbot/conversation-runtime'; // es6
Create an Anonymous Conversation Connector
import { ConversationConnector, ConversationChannelTypes } from '@servisbot/conversation-runtime';
const apiKey = 'conversation-api-key';
const endpointAddress = 'acme-MyBot';
const context = { lang: 'en' };
const sbRegion = 'eu-1';
const organization = 'acme';
const customerReference = 'customer-123';
const regionalEndpoints = [
{ region: 'venus-eu-west-1.eu-1.servisbot.com' },
{ region: 'venus-eu-east-1.eu-1.servisbot.com' },
]
const conversationConnector = await ConversationConnector.createConnector({
channelType: ConversationChannelTypes.OnDemandChannelRestWs,
apiKey,
endpointAddress,
context,
logger: console,
sbRegion,
organization,
regionalEndpoints, // optional
customerReference // optional
});
Regional Endpoints
- If you do not supply a list of regional endpoints for conversation-runtime to use the runtime will attempt to use default endpoints based on the ServisBOT region that was passed to the runtime when creating the conversation connector instance.
- The regional endpoints default to the following:
eu-private-3
[
{ region: 'venus-eu-west-1.eu-private-3.servisbot.com' }
]
eu-private-1
[
{ region: 'venus-eu-west-1.eu-private-1.servisbot.com' }
]
eu-1
[
{ region: 'venus-eu-west-1.eu-1.servisbot.com' },
{ region: 'venus-eu-central-1.eu-1.servisbot.com' }
]
us1
ORus-1
(some clients pass us-1 instead of us1)
[
{ region: 'venus-us-east-1.us1.servisbot.com' },
{ region: 'venus-us-east-2.us1.servisbot.com' }
]
usscif1
[
{ region: 'venus-us-west-1.usscif1.servisbot.com' }
]
- An error will be thrown if an invalid ServisBOT region is supplied and there is no regional endpoints supplied.
Attach Conversation Handlers
- You can attach conversation event handlers by using the
on
function on the created connector.
connector.on('message', (data) => console.log(data));
connector.on('error', (err) => console.log(err));
Conversation Events
Message
- For events of type
TimelineMessage
the connector will emit amessage
event the full message object passed to the event listener.
Host Notifications
There are two types of notifications which can be emitted from the runtime:
- Standard
HostNotification
events. Private
Host Notification events.
Standard Host Notification Events
- You can attach a listener for standard host notification events by using the
on
function on theConversationConnector
instance.
connector.on('HostNotification', (hostNotificationEvent) => console.log(hostNotificationEvent));
Private Host Notification Events
Private Host Notification's
contents.notification
attribute are prefixed withSB:::
.This is the list of private host notifications which can be emitted from the runtime:
SB:::UserInputDisabled
SB:::UserInputEnabled
SB:::ValidationPassed
SB:::ValidationFailed
SB:::ProcessingPassed
SB:::ProcessingFailed
SB:::FileProcessorPipelinePassed
SB:::FileProcessorPipelineFailed
SB:::ResetConversation
SB:::Typing
SB:::MessengerOpen
SB:::MessengerClose
SB:::MessengerPopup
SB:::MessengerNavigate
SB:::MessengerStyle
SB:::UserInputNumericEnabled
SB:::UserInputNumericDisabled
- You can attach a listener for private host notification events by using the
on
function on theConversationConnector
instance, and specifying one of the private host notification event names listed above.
connector.on('<PRIVATE_HOST_NOTIFICATION_NAME_GOES_HERE>', () => console.log('Received private host notification'));
- Depending on the private host notification that is emitted, the notification may or may not have data passed along in the event callback.
- Below is a list of private host notifications which supply data when they are fired
connector.on('SB:::Typing', (seconds) => console.log(seconds));
connector.on('SB:::ProcessingPassed', docId => console.log(docId));
connector.on('SB:::ProcessingFailed', docId => console.log(docId));
connector.on('SB:::ValidationPassed', docId => console.log(docId));
connector.on('SB:::ValidationFailed', docId => console.log(docId));
connector.on('SB:::FileProcessorPipelinePassed', context => console.log(context));
connector.on('SB:::FileProcessorPipelineFailed', context => console.log(context));
connector.on('SB:::MessengerPopup', seconds => console.log(seconds));
connector.on('SB:::MessengerNavigate', url => console.log(url));
connector.on('SB:::MessengerStyle', data => console.log(data));
Conversation Refreshed Event
- A conversation refreshed event occurs when the conversation's JWT auth token has expired and the conversation-runtime has successfully managed to refresh the auth tokens.
- You can listen to this event using the following:
connector.on('ConversationRefreshed', (refreshedConversation) => {
const newAuthToken = refreshedConversation.getAuthToken();
const newRefreshToken = refreshedConversation.getRefreshToken();
console.log(newRefreshToken, newRefreshToken);
})
- The
refreshedConversation
passed to the callback in the event above is an instance of theConversation
class within the conversation-runtime. - This event gives the client an opportunity to store the new refreshed tokens, for example L2 would update local storage with the new tokens.
Conversation Expired Event
- A conversation expired event occurs when the conversation has expired and the conversation-runtime catches the expired conversation from the ping health checks.
- You can listen to this event using the following:
connector.on('ConversationExpired', () => {
console.log('ConversationExpired event');
})
Error
- For error events the connector will emit a
error
event, and pass the error instance to the event listener.
Other Events
- For other events such as
SecureSession
etc. - If the runtime determines that the event is not a
TimelineMessage
or anError
, it will emit an event using the event'stype
attribute and pass the full event to the event listener. - For example if the runtime gets a
SecureSession
event, the event will look like the following
{
"organizationId": "acme",
"id": "aa8a6b64-2565-43ff-ab9d-007ebb0faa0b",
"conversationId": "conv-APD491fxU7xaYWU7M-CuU",
"identityId": "acme:::SYAGZio7T1T0_OAVEmdiC",
"sessionId": "uZ0790y6WKvnrvTdaRm5_",
"contentType": "state",
"contents": {
"state": "disabled"
},
"source": "server",
"type": "SecureSession",
"token": "SECURESESSION■conv-APD491fxU7xaYWU7M-CuU■■aa8a6b64-2565-43ff-ab9d-007ebb0faa0b"
}
The event's
type
attribute will be used to emit the event, so this will result in aSecureSession
event being emitted with the payload shown above.Conversation Event Handlers should be added before calling
createConversation
orresumeConversation
on the connector. If you do not attach the handlers before callingcreateConversation
orresumeConversation
you risk missing events being emitted.
Conversation Create
- To create a conversation you must call the
createConversation
function on the connector.
const parameters = { engagementAdapter: 'L2', engagementAdapterVersion: '1.2.3'};
const conversation = await connector.createConversation(parameters);
- Any parameters that are passed to the
createConversation
function will be passed along to the channel which is being used. - The response from initialising the conversation is a
Conversation
instance - see the class for more details. - Once the create call completes a conversation is created, and a websocket connection is established to listen for events related to the conversation.
- Once a websocket connection has successfully opened, the runtime will make a request to the
ReadyForConversation
on the server. - This will tell the server that the client is ready and listening for events on the websocket, and the server can proceed and send a new conversation or conversation resumed event into the system.
NOTE
- A connector may only interact with one conversation at a time.
- If a connector already contains an initialised conversation and a call to
createConversation
is made, an error will be thrown.
import { ConversationErrorCodes } from '@servisbot/conversation-runtime';
try {
const parameters = { engagementAdapter: 'L2', engagementAdapterVersion: '1.2.3'};
await connector.createConversation(parameters);
// second call to "createConversation" will throw as the connector already has a conversation.
await connector.createConversation(parameters);
} catch(err) {
if (err.code === ConversationErrorCodes.CONVERSATION_ALREADY_INITIALISED) {
console.log(err.message);
}
// unexpected error has occurred
throw err;
}
- If you want to create multiple conversations you must create a new
ConversationConnector
and create the conversation using the new connector.
Conversation Resume
- To continue or resume an existing conversation you must call the
resumeConversation
function on the connector.
import { Conversation } from '@servisbot/conversation-runtime';
const conversation = new Conversation({
authToken: 'some-token',
refreshToken: 'refresh-token',
conversationId: 'some-conversation-id',
hasActivity: true
});
const parameters = {
engagementAdapter: 'L2',
engagementAdapterVersion: '1.2.3',
conversation
};
const conversationResumeResponse = await connector.resumeConversation(parameters);
- Any parameters that are passed to the
resumeConversation
function will be passed along to the channel which is being used. - Once the resumeConversation call completes, a websocket connection is established to listen for events related to the connector.
- Once a websocket connection has successfully opened, the conversation runtime will make a request to the
ReadyForConversation
endpoint. - This will inform the server that the client is ready and listening for events on the websocket, and the server can proceed and send a new conversation or conversation resumed event into the system.
A ConversationError
can be thrown from a resumeConversation
call in the following cases:
- If the conversation has expired.
- The auth tokens for the conversation are no longer valid and can not be refreshed
A ConversationError
will be thrown with a code and a message. The code can be inspected within a try/catch
using the ConversationErrorCodes
.
import { ConversationErrorCodes } from '@servisbot/conversation-runtime';
try {
const conversationResumeResponse = await connector.resumeConversation(parameters);
} catch (_err) {
if (_err.code === ConversationErrorCodes.CONVERSATION_EXPIRED) {
console.log("Conversation has expired");
}
if(_err.code === ConversationErrorCodes.UNAUTHORIZED) {
console.log("Auth tokens are invalid")
}
// unexpected error has occurred
return {};
}
Retry Behaviour
- Conversation operations (create/resume/create event) use network retries to retry operations against a conversation in the event an operation fails due to a network failure.
- If all retry attempts are exhausted then an
Error
will be thrown.
Send Events
You can send events by using the send
function on the conversation connector.
Depending on the runtime mode your running in, the responses will be in one of two places.
For OnDemandChannelRestWs, once an event is sent successfully it will be emitted on the conversation event emitter so that clients can acknowledge that an event was sent successfully.
For OnDemandChannelRestSync, once an event is sent successfully you will get a response, this response will contain any response messages from your the bot triggered by the message that was sent.
const sendResult = await conversationConnector.send(message);
// calling getMessages() will get return the response messages from the bot if there are any.
sendResult.getMessages().forEach(message => {
// calling getRawData() will give you the raw message payload
console.log('MESSAGE', message.getRawData())
})
- All events can take the following attributes:
id
- the event id, defaults to a v4 uuid if the provided value is not a string or is not a valid v4 uuid.correlationId
- the correlation id used to trace the event through the system, defaults to a v4 uuid if the provided value is not a string or is not a valid v4 uuid.
Send Message
import { IngressEventFactory } from '@servisbot/conversation-runtime';
const id = uuidV4(); // if not provided it will default to a v4 uuid
const correlationId = uuidV4(); // if not provided it will default to a v4 uuid
const message = 'hello world';
const message = IngressEventFactory.createMessage({ message, id, correlationId });
await connector.send(message);
Send Markup Interaction
import { IngressEventFactory } from '@servisbot/conversation-runtime';
const id = uuidV4(); // if not provided it will default to a v4 uuid
const correlationId = uuidV4(); // if not provided it will default to a v4 uuid
const source = {
eventType: 'TimelineMessage',
timestamp: 1647959518700,
id: 'jmfuvLOVT-',
conversationId: 'conv-kTKya6Oarb8lw2qUAIQae'
};
const interaction = {
action: 'listItemInteraction',
value: { id: '1', title: 'Item one' },
timestamp: 1647959522136
};
const markupInteractionEvent = IngressEventFactory.createMarkupInteraction({
source, interaction, id, correlationId
});
await connector.send(markupInteractionEvent);
Send Page Event
import { IngressEventFactory } from '@servisbot/conversation-runtime';
const id = uuidV4(); // if not provided it will default to a v4 uuid
const correlationId = uuidV4(); // if not provided it will default to a v4 uuid
const body = { // optional
name: 'test-user'
};
const alias = 'page-event-alias';
const pageEvent = IngressEventFactory.createPageEvent({
alias, body, id, correlationId
});
await connector.send(pageEvent);
Vend User Document
import { VendUserDocumentRequest } from '@servisbot/conversation-runtime';
const params = {
documentLabel: 'my-document', // required
metadata: { FileType: 'png', CustomerReference: 'cust-123' }, // optional, defaults to {}
additionalPathwayPath: '/some/additional/path' // optional
}
const vendUserDocumentRequest = new VendUserDocumentRequest(params);
const secureFileUploadManager = connector.getSecureFileUploadManager();
const response = await secureFileUploadManager.vendUserDocument(vendUserDocumentRequest);
const { url: urlForUploadingDocument, docId } = response;
console.log('Url for Uploading Document', url);
console.log('Document Id:', docId);
Create Secure Input
import { CreateSecureInputRequest } from '@servisbot/conversation-runtime';
const params = {
enigmaUrl: 'https://enigma.com',
jwt: 'some-jwt-token',
input: 'this is some sensitive input',
ttl: 124242 // optional, time to live in seconds
}
const createSecureInputRequest = new CreateSecureInputRequest(params);
const secureInputManager = connector.getSecureInputManager();
const secureInputResponse = await secureInputManager.createSecureInput(createSecureInputRequest);
if(secureInputResponse.isOk()) {
const secureInputSrn = secureInputResponse.getSrn();
console.log(`Secure input srn: ${secureInputSrn}`);
} else {
const {message: errMessage} = secureInputResponse.getBody();
console.log(`${errMessage} with status code ${secureInputResponse.getStatusCode()}`);
}
Create Impression
- To create an impression you can use the
createImpression
function via the web client manager which can be retrieved via thegetWebClientManager
function on the conversation connector instance. - There is no need to have an ongoing conversation to create an impression.
const webClientManager = connector.getWebClientManager();
const impressionId = 'some-impression-id';
await webClientManager.createImpression({ impressionId });
- It is possible to associate metadata with the
createImpression
call by suppliying an instance ofWebClientMetadata
to thecreateImpression
function call. - To construct a
WebClientMetadata
instance, aWebClientMetadataBuilder
can be used. - It is recommended to use a
WebClientMetadataBuilder
to ensure you construct valid metadata for thecreateImpression
function call. - If a
createImpression
is invoked with an invalidWebClientMetadata
the data will not be passed along in the request to the server. - The following metadata can be included when creating a
WebClientMetadata
instance using theWebClientMetadataBuilder
.messengerState
- the current state of the messenger. Possible values areopen
orclosed
. TheMessengerStates
enum can be used to specify the state to be used when constructing theWebClientMetadata
.
- The following example shows how to invoke
createImpression
with an instance ofWebClientMetadata
.
import { WebClientMetadataBuilder, MessengerStates } from '@servisbot/conversation-runtime';
const webClientMetadata = new WebClientMetadataBuilder()
.withMessengerState(MessengerStates.OPEN)
.build();
const webClientManager = connector.getWebClientManager();
const impressionId = 'some-impression-id';
await webClientManager.createImpression({ impressionId, webClientMetadata });
Create Transcript
- To create a transcript you can use the
createTranscript
function via the venus connector. It will use the current conversation and identity to request a transcript be generated and return a signed get url to that transcript.
const transcript = await connector.createTranscript();
Get Conversation
- To get the conversation id for a conversation after calling
createConversation
you can do the following
const conversation = connector.getConversation();
const conversationId = conversation.getId();
Close Connection
- To close the websocket connection you can call the
close
function on the conversation - You can pass an object to the close function to make it available inside the runtime.
await connector.close();
Reconnection Behaviour
- The conversation-runtime will automatically attempt to reconnect to the websocket in the following cases:
- If the socket receives a "close" event from the server.
- If the initial connection to the server errors.
- If the runtime can establish a connection again, everything resumes as usual, and a reconnect success event is sent to the client.
- If the runtime hits the max amount of reconnect retries, a failure event is sent up to the client.
Reconnection Events
reconnecting
- this event is emitted to the client when the runtime receives a "close" event from the server, and the runtime is about to start attempting to reconnect to the web socket.reconnectSuccess
- this event is emitted to the client when the runtime is in areconnecting
state, and manages to establish a successful "open" event from a new web socket connection.reconnectFailed
- this event is emitted to the client when the runtime is in areconnecting
state, and hits the maximum number of reconnect attempts allowed.- To listen to these events you can attach a listener on the connector like so:
connector.on('reconnecting', () => console.log('Reconnecting'));
connector.on('reconnectFailed', () => console.log('Reconnecting Failed'));
connector.on('reconnectSuccess', () => console.log('Reconnecting Success'));
Reconnection Options
- To configure the reconnection options you can supply
WebSocketOptions
to the create/resume functions on the conversation connector, see below for an example:
import { WebSocketOptions } from '@servisbot/conversation-runtime';
const webSocketOptions = new WebSocketOptions({
maxReconnectAttempts: 3,
baseReconnectDelay: 500,
establishConnectionTimeout: 1000
});
const parameters = { engagementAdapter: 'L2', engagementAdapterVersion: '1.2.3', webSocketOptions};
const conversation = await connector.createConversation(parameters);
- The following reconnect parameters can be passed as options to the
WebSocketOptions
class.reconnectEnabled
- a boolean to toggle the reconnect behaviour on/off. Defaults totrue
, it is recommended to leave this astrue
for reliability reasons.maxReconnectAttempts
- an integer representing the maximum amount of reconnect attempts that should be made before emitting thereconnectFailed
event. This excludes the initial connection attempt.baseReconnectDelay
- The base number of milliseconds to use in the exponential back off for reconnect attempts. Defaults to 100 ms.establishConnectionTimeout
- an integer in milliseconds representing the maximum amount of time to wait for the websocket to receive an "open" event from the server before considering the connection attempt a failure.- This value will have an affect on the
baseReconnectDelay
value. Take the following example. - If the
baseReconnectDelay
is set to20
milliseconds and themaxReconnectAttempts
is set to3
and theestablishConnectionTimeout
is set to100
milliseconds, and we exhaust all retries, the timings will be as follows:- First reconnect attempt will occur after
20
milliseconds, we will then wait100
before considering this reconnect attempt a failure. - Second reconnect attempt will occur
40
milliseconds after considering the first attempt a failure, we will then wait another100
milliseconds before considering this attempt a failure. - Third reconnect attempt will occur
80
milliseconds after considering the second attempt a failure, we will then wait another100
milliseconds before considering this attempt a failure, we will then emit areconnectFailed
error as we have hit the max reconnect attempts limit. - The total time spent trying to get a connection in the above example is
440
milliseconds. - NOTE the above example does not take into account that the exponential back off used within the conversation-runtime is based on the "Full Jitter" algorithm found which is explained here, and it is unlikely the delays will ever be in even increasing increments. Fixed delays of
20
,40
and80
were used to simplify the example above.
- First reconnect attempt will occur after
- This value will have an affect on the
maxFailedHealthChecks
- an integer representing the maximum number of health check failures in a row before considering the web socket unhealthymaxHealthCheckResponseTime
- an integer in milliseconds representing the maximum amount of time to wait for a "pong" response after sending a "ping" request before considering the health check a failure.healthCheckInterval
- an integer in milliseconds representing how long to wait between health checks.
Reconnection Default Options
- If no
WebSocketOptions
are supplied the following defaults will be used:reconnectEnabled
-true
maxReconnectAttempts
-3
baseReconnectDelay
-100
(milliseconds)establishConnectionTimeout
-3000
(milliseconds)maxFailedHealthChecks
-3
maxHealthCheckResponseTime
-500
(milliseconds)healthCheckInterval
-5000
(milliseconds) - 5 seconds
Reconnect "Full Jitter" Back Off
- The conversation-runtime uses the "Full Jitter" algorithm found in this blog https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
- This max amount of time a client will wait before attempting a reconnect is
3
seconds. - The minimum amount of time a client will wait before attempting a reconnect is
100
milliseconds.
Health Checking
- Once a websocket connection is in an "open" state, conversation-runtime will continuously health check the web socket
- Health checks are done by sending a HTTP request for a specific conversation id to the server, the server then sends the same ping id back down the web socket
- If a HTTP ping request fails which results in a pong response not being returned on the web socket this will count as a failed health check.
- If a response for a ping takes too long or does not arrive, it will count as a failed health check.
- Once a websocket connection gets a "close" event, the health checking is stopped as the socket is no longer open.
- If the number of failed health checks in a row breaches the
maxFailedHealthChecks
option, the conversation-runtime will attempt to get a new web socket connection by starting the reconnect process.
Synchronous mode
When using synchronous, all request are made synchronously. Each time you create a conversation or send in an event it will wait for the response including any messages related to that event. These are returned from the function call.
For more information on the event handlers you can register see here
Note that host notifications and messages will come back in the response from both the send() and createConversation() calls.
import { ConversationConnector, ConversationChannelTypes, IngressEventFactory } from '@servisbot/conversation-runtime';
const organization = 'acme';
const apiKey = '<servisbot-conversation-api-key>'
const endpointAddress = 'acme-MyBot';
// can be "eu-1", "us1" or "usccif1"
const sbRegion = 'us1';
// When using sync mode the connector should use the `OnDemandChannelRestSync` channel type from the ConversationChannelTypes
const conversationConnector = await ConversationConnector.createConnector({
channelType: ConversationChannelTypes.OnDemandChannelRestSync,
apiKey,
endpointAddress,
logger,
sbRegion,
organization,
});
// Setup conversation event listeners before creating the conversation
conversationConnector.on('ConversationRefreshed', (msg) => console.log('ConversationRefreshed', msg));
const conversation = await conversationConnector.createConversation({
engagementAdapter: 'L2', engagementAdapterVersion: '1.0.0'
});
// When a conversation is created using the synchronous channel, the conversation can contain welcome messages as part of the conversation instance
// You can call getWelcomeMessages() to get any messages from the result
const messages = conversation.getWelcomeMessages();
messages.forEach(message => {
// calling getRawData() will give you the raw message payload
console.log('MESSAGE', message.getRawData())
})
const message = IngressEventFactory.createMessage({ message: 'content' })
const sendResult = await conversationConnector.send(message);
// calling getMessages() will get any messages from response
sendResult.getMessages().forEach(message => {
// calling getRawData() will give you the raw message payload
console.log('MESSAGE', message.getRawData())
})
Limitations of Synchronous mode
There is no continuous health checking (pinging) to the server when using the synchronous channel. This means the detection of a conversation expiring will not occur until the connector attempts to send a message or resume a conversation.
An error will be thrown if the server determines that the conversation has expired in both of the above cases.