@servisbot/conversation-runtime v3.7.2
conversation-runtime
Runtime SDK for Interacting with ServisBOT conversations
Install
npm install @servisbot/conversation-runtimeUsing 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'; // es6Create 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' }
]us1ORus-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
onfunction 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
TimelineMessagethe connector will emit amessageevent 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
HostNotificationevents. PrivateHost Notification events.
Standard Host Notification Events
- You can attach a listener for standard host notification events by using the
onfunction on theConversationConnectorinstance.
connector.on('HostNotification', (hostNotificationEvent) => console.log(hostNotificationEvent));Private Host Notification Events
Private Host Notification's
contents.notificationattribute 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
onfunction on theConversationConnectorinstance, 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
refreshedConversationpassed to the callback in the event above is an instance of theConversationclass 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
errorevent, and pass the error instance to the event listener.
Other Events
- For other events such as
SecureSessionetc. - If the runtime determines that the event is not a
TimelineMessageor anError, it will emit an event using the event'stypeattribute and pass the full event to the event listener. - For example if the runtime gets a
SecureSessionevent, 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
typeattribute will be used to emit the event, so this will result in aSecureSessionevent being emitted with the payload shown above.Conversation Event Handlers should be added before calling
createConversationorresumeConversationon the connector. If you do not attach the handlers before callingcreateConversationorresumeConversationyou risk missing events being emitted.
Conversation Create
- To create a conversation you must call the
createConversationfunction on the connector.
const parameters = { engagementAdapter: 'L2', engagementAdapterVersion: '1.2.3'};
const conversation = await connector.createConversation(parameters);- Any parameters that are passed to the
createConversationfunction will be passed along to the channel which is being used. - The response from initialising the conversation is a
Conversationinstance - 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
ReadyForConversationon 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
createConversationis 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
ConversationConnectorand create the conversation using the new connector.
Conversation Resume
- To continue or resume an existing conversation you must call the
resumeConversationfunction 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
resumeConversationfunction 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
ReadyForConversationendpoint. - 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
Errorwill 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
createImpressionfunction via the web client manager which can be retrieved via thegetWebClientManagerfunction 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
createImpressioncall by suppliying an instance ofWebClientMetadatato thecreateImpressionfunction call. - To construct a
WebClientMetadatainstance, aWebClientMetadataBuildercan be used. - It is recommended to use a
WebClientMetadataBuilderto ensure you construct valid metadata for thecreateImpressionfunction call. - If a
createImpressionis invoked with an invalidWebClientMetadatathe data will not be passed along in the request to the server. - The following metadata can be included when creating a
WebClientMetadatainstance using theWebClientMetadataBuilder.messengerState- the current state of the messenger. Possible values areopenorclosed. TheMessengerStatesenum can be used to specify the state to be used when constructing theWebClientMetadata.
- The following example shows how to invoke
createImpressionwith 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
createTranscriptfunction 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
createConversationyou can do the following
const conversation = connector.getConversation();
const conversationId = conversation.getId();Close Connection
- To close the websocket connection you can call the
closefunction 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 areconnectingstate, 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 areconnectingstate, 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
WebSocketOptionsto 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
WebSocketOptionsclass.reconnectEnabled- a boolean to toggle the reconnect behaviour on/off. Defaults totrue, it is recommended to leave this astruefor reliability reasons.maxReconnectAttempts- an integer representing the maximum amount of reconnect attempts that should be made before emitting thereconnectFailedevent. 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
baseReconnectDelayvalue. Take the following example. - If the
baseReconnectDelayis set to20milliseconds and themaxReconnectAttemptsis set to3and theestablishConnectionTimeoutis set to100milliseconds, and we exhaust all retries, the timings will be as follows:- First reconnect attempt will occur after
20milliseconds, we will then wait100before considering this reconnect attempt a failure. - Second reconnect attempt will occur
40milliseconds after considering the first attempt a failure, we will then wait another100milliseconds before considering this attempt a failure. - Third reconnect attempt will occur
80milliseconds after considering the second attempt a failure, we will then wait another100milliseconds before considering this attempt a failure, we will then emit areconnectFailederror as we have hit the max reconnect attempts limit. - The total time spent trying to get a connection in the above example is
440milliseconds. - 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,40and80were 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
WebSocketOptionsare supplied the following defaults will be used:reconnectEnabled-truemaxReconnectAttempts-3baseReconnectDelay-100(milliseconds)establishConnectionTimeout-3000(milliseconds)maxFailedHealthChecks-3maxHealthCheckResponseTime-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
3seconds. - The minimum amount of time a client will wait before attempting a reconnect is
100milliseconds.
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
maxFailedHealthChecksoption, 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.
6 months ago
6 months ago
6 months ago
9 months ago
5 months ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago