@telefonica/la-bot-sdk
v3.0.1
Published
Living Apps Bot SDK
Downloads
167
Readme
la-bot-sdk
SDK for building Living Apps bots
1. Usage
1.1. Install
Set up the dependencies in package.json
as follows:
"dependencies": {
"la-bot-sdk": "1.0.0-alpha.1",
"@hapi/joi": "^16.1.5",
"botbuilder": "~4.6.2",
"botbuilder-dialogs": "~4.6.2"
}
Now run npm install
.
1.2. Initialization
The Living App using this SDK needs to be loaded into AuraBot using the following function in
index.ts
export = function setup(options: any, imports: any, register: (err: Error, result: any) => void) {
const dialogs = [
'./dialogs/dialog-la-start',
'./dialogs/dialog-la-close',
'./dialogs/dialog-offer-dates',
'./dialogs/dialog-offer-flights',
'./dialogs/dialog-offers',
'./dialogs/dialog-search',
'./dialogs/dialog-call',
'./dialogs/dialog-web'
];
// Remove lib dialogs based on options
sdk.loader.excludeDialogs(dialogs, options);
const settingsPath = path.resolve(__dirname, '..', 'settings');
register(null, {
[LIBRARY_NAME]: {
dialogs: dialogs.map(d => require(d)),
locale: sdk.loader.readLocaleFolder(path.resolve(settingsPath, 'locale')),
env: sdk.loader.readEnv(options.configuration, settingsPath),
config: sdk.loader.readDialogConfig(options.configuration, settingsPath),
configSchema: configurationSchema,
resources: path.resolve(__dirname, '..', 'resources')
}
});
};
1.2.1 ConfigSchema
Since 2.0.0, configuration checks must be done inside a configurationSchema object exported from a configuration-schema.ts
file.
import * as joi from 'joi';
const configurationSchema: joi.SchemaMap = {
LA_NAME_API_BASE_URL: joi.string().uri({ scheme: ['http', 'https'] }).required(),
LA_NAME_API_CODE_URL: joi.string().uri({ scheme: ['http', 'https'] }).required(),
LA_NAME_API_CLIENT_ID: joi.string().required(),
LA_NAME_API_CLIENT_SECRET: joi.string().required(),
LA_NAME_MEDIA_BASE_URL: joi.string().uri({ scheme: ['http', 'https'] }).required(),
LA_NAME_WEB_BASE_URL: joi.string().uri({ scheme: ['http', 'https'] }).required(),
LA_NAME_BACKOFFICE_URL_PRO: joi.string().uri({ scheme: ['http', 'https', 'file'] }).required(),
LA_NAME_BACKOFFICE_URL_PRE: joi.string().uri({ scheme: ['http', 'https', 'file'] }),
LA_NAME_BACKOFFICE_URL_DEV: joi.string().uri({ scheme: ['http', 'https', 'file'] }),
LA_NAME_CACHE_TTL_SEC: joi.number().required(),
LA_NAME_API_TIMEOUT_SEC: joi.number().required(),
LA_NAME_QR_CODE_FORMAT: joi.string().valid('svg', 'utf8').required(),
LA_NAME_URL_SHORTENER_BASE_URL: joi.string().uri({ scheme: ['http', 'https'] }).allow('').optional(),
};
export default configurationSchema;
The values for these environment variables will be checked during deployment from the .env
file inside the settings folder.
1.3. Dialogs
The SDK simplifies the dialog model through the class Dialog
. The following is an example
of what is needed for a dialog using this new class:
export default class SomeDialog extends Dialog {
// private fields for anything needed for the business logic
static readonly dialogPrompt = `${DialogId.SOME_DIALOG}-prompt`;
readonly laConfig: LaConfig;
// We supply the parent class with the library name, a string id (recommended the use of enums),
// and the configuration we receive from Aura Bot. We also initialize any private field here.
constructor(config: Configuration) {
super(LIBRARY_NAME, DialogId.SOME_DIALOG, config);
this.laConfig = new LaConfig(this.config);
}
// This functions return the different stages our dialog may have. Usually 'sendPrompt' &
// 'processPrompt' pairs. If your dialog does not have a prompt then it must end with a replacement
// with a dialog that has one (or another replacement)
protected dialogStages(): WaterfallStep[] {
return [
this._dialogStage.bind(this),
this._promptResponse.bind(this)
];
}
// This function returns the string ids for each prompt we have to define
protected prompts(): string[] {
return [ SomeDialog.dialogPrompt ];
}
// Here we clear any state field we may have defined for this dialog and any other children
// we may create. Note that global state fields (fields used by siblings or parents) shoud not
// be deleted for the Living App to work
protected async clearDialogState(stepContext: WaterfallStepContext): Promise<void> {
const sessionData = await sdk.lifecycle.getSessionData<SessionData>(stepContext);
delete sessionData.offers;
delete sessionData.currentOffer;
}
// This is the "message construction" stage of the dialog
private async _dialogStage(stepContext: WaterfallStepContext): Promise<DialogTurnResult> {
const backoffice = await this.laConfig.getBackoffice();
const sessionData = await sdk.lifecycle.getSessionData<SessionData>(stepContext);
//
// Business Logic goes here
//
// Generate response for all channels
const screenData: any = {
//
// Data for the channels goes here
//
};
const isSpoken = true || false;
const message = new ScreenMessage(Screen.OFFERS, screenData).withText('text');
await sdk.messaging.send(stepContext, message, isSpoken);
const choices = [ /* Prompt choices go here */
ChoiceOperation.NEXT,
ChoiceOperation.PREVIOUS,
ChoiceOperation.OTHER_OPERATION
];
return await sdk.messaging.prompt(stepContext,SomeDialog.dialogPrompt, choices);
}
// This is the "response" stage of the dialog, from where we will change the current dialog or state
private async _promptResponse(stepContext: WaterfallStepContext): Promise<DialogTurnResult> {
const sessionData = await sdk.lifecycle.getSessionData<SessionData>(stepContext);
const cases: PromptCase[] = [
{
operation: ChoiceOperation.NEXT,
logic: () => this.modifyCurrentState(sessionData, 1)
// empty action defaults to repeat this dialog
},
{
operation: ChoiceOperation.PREVIOUS,
logic: () => this.modifyCurrentState(sessionData, -1)
// empty action defaults to repeat this dialog
},
{
operation: ChoiceOperation.OTHER_OPERATION,
action: [RouteAction.PUSH, DialogId.CHILDREN_DIALOG]
}
];
return super.promptHandler(stepContext, cases);
}
private modifyCurrentState(sessionData: SessionData, offset: number) {
/* Modify Current state here */
}
}
1.4. Logging
All dialogs have a logger this.logger
automatically created when the dialog is built. It is also
possible to add loggers in other places with the SDK wrapped logger Logger
.
const logger = new Logger('la.library-name.logger-identifier');
The following methods are available to use:
logger.error({
error: error.message, stck: error.stack,
msg: `Error "${error.name}" doing something"`,
corr: sdk.lifecycle.getCorrelator(stepContext)
});
logger.debug({
msg: `Logging some DEBUG message`,
corr: sdk.lifecycle.getCorrelator(stepContext)
});
logger.info({
msg: `Logging some INFO message`,
corr: sdk.lifecycle.getCorrelator(stepContext)
});
1.5. Base Api Client
The base api client ApiClient is a small class designed to be extended. It is a small wrapper around request-promise-native, with a bit of the design of the BaseWsClient from TroyaBackend, albeit adapted to LAs needs. Note that this wrapper will log every request with all the information about url, parameters, body... as well as the responses, with headers. The logging will include correlator, thus requiring the class to be instanced inside the dialog steps.
- Extend ApiClient
- Always call super(stepContext, isMocked) in the constructor where isMocked is true if the client is to return mocked values and stepContext is the botframework stepContext (needs to be instanced inside a dialog, yeah).
- declare your API calls, using the setupRequest<T>(method, url, message) that is provided through this class, the mock and the response of the execute method will have the type T
Example:
import { HTTPMethod, ApiClient } from 'la-bot-sdk';
import * as sdk from 'la-bot-sdk';
import { WaterfallStepContext } from 'botbuilder-dialogs';
import { LaConfig } from '../config';
import { AUTH_API_RESPONSE } from './mocks-mythirdparty';
export class MyThirdPartyApiClient extends ApiClient {
private laConfig: LaConfig;
constructor(laConfig: LaConfig, stepContext: WaterfallStepContext) {
super(stepContext, laConfig.getApiBaseUrl().indexOf('mock') > 0);
this.laConfig = laConfig;
}
private async auth(): Promise<AuthResponse> {
const msg = `Fetching access token with clientId: ${this.laConfig.getApiClientId()}`;
const fetch: () => Promise<AuthResponse> = () =>
this.setupRequest<AuthResponse>(HTTPMethod.GET, `${this.laConfig.getApiBaseUrl()}/v1/auth`, msg)
.withQueryParameter('clientId', this.laConfig.getApiClientId())
.withQueryParameter('clientSecret', this.laConfig.getApiClientSecret())
.withHeader('Cache-Control', 'no-cache')
.withTimeout(this.laConfig.getApiTimeout())
.withMock(AUTH_API_RESPONSE)
.execute();
return sdk.cacheGet<AuthResponse>('<livingapp>.accessToken', fetch, (authResponse) => (authResponse.expires_in - 60), this.stepContext);
}
}
Request methods
You can apply the folowing methods to a request to configure it:
- withCallback(callback: request.RequestCallback): Request -> Allows passing a callback
- withQueryParameter(key: string, value: string): Request; -> Appends query parameters to the request
- withHeader(key: string, value: any): Request; -> Appends headers to the request
- withAuthentication(auth: request.AuthOptions): Request; -> request-promise auth options (e.g: { bearer: 'token' }
- withOAuth(oauth: request.OAuthOptions): Request; -> request-promise oauth options
- withBody(body: any): Request; -> Body for requests where it is needed
- withRedirectFollow(follow: boolean): Request; -> Sets or unsets the following of redirects
- withEncoding(encoding: string | null): Request; -> Sets the encoding of the response
- withRawResponse(): Request; -> Sets the request for raw mode (not json)
- withTimeout(millis: number): Request; -> applies a timeout to the request
- withAdvancedOptions(options: rp.RequestPromiseOptions): Request; -> Allows the input of requrest-promise options
- withMock(mock: any): this; -> The mocked response for when the client is configured to return mock responses
- execute(): Promise; -> Executes the request, returning a promise of type T (eg. any)
Backward compatibility
The generic type of the setupRequest is optional in order to maintain the compatibility with older versions. In that case you can add the type to the execute method
- execute<K>(): Promise; -> Executes the request, returning a promise of type K (eg. any)
And the promise will have the type K
1.6. Method Reference
Cache
Declaration:
export declare function cacheGet<T>(
key: string,
fetch: () => Promise<T>,
ttl: number | ((value: T) => number)
stepContext?: WaterfallStepContext
): Promise<T>;
Usage:
import * as sdk from '@telefonica/la-bot-sdk';
sdk.cacheGet<SomeClass>(
'cache_key',
fetchPromise,
number,
stepContext
);
sdk.cacheGet<SomeClass>(
'cache_key',
fetchPromise,
(fetchResponse) => (fetchResponse.number),
stepContext
);
Lifecycle: Functions for messing around with LivingApp session data
- getSession(stepContext: WaterfallStepContext) : AuraSession
- getDialogId(stepContext: WaterfallStepContext) : string
- getCorrelator(stepContext: WaterfallStepContext) : string
- getUser(stepContext: WaterfallStepContext): AuraUserBaseModel
- getUserPhoneNumber(stepContext: WaterfallStepContext): string
- getUserId(stepContext: WaterfallStepContext): string
- getUserAuraId(stepContext: WaterfallStepContext): string
- getCurrentChannel(stepContext: WaterfallStepContext): string
- async isChannelJoining(stepContext: WaterfallStepContext): Promise
- check if the channel is running its first dialog (does not include the start dialog)
- async getChannelMessageCount(stepContext: WaterfallStepContext): Promise
- gets the number of messages this channel has sent (including splash message)
- async getSessionData(stepContext: WaterfallStepContext): Promise
- Gets a
T
subset of the session data, thus providing the way to split the interface as needed
- Gets a
- getCallingIntent(stepContext: WaterfallStepContext): string
- Gets the intent that reached the dialog. Eg.
intent.operation.libraryname.back
- Gets the intent that reached the dialog. Eg.
- getCallingEntities(stepContext: WaterfallStepContext): Entity[]
- Gets the list of entities attached to the aura command.
- getCallingEntity(stepContext: WaterfallStepContext, type: string, minScore: number = 0.4): string
- Gets one of the entities attached to the aura command.
Persistence: Recovering cross-session data
- async getStoredData(stepContext: WaterfallStepContext): Promise<{[key: string]: any}>
- Gets the living app user's persisted data
- async storeData(stepContext: WaterfallStepContext, data: {[key: string]: any}): Promise
- Persists the data to database
Events:
- sendEvent(stepContext: WaterfallStepContext, event: {[key: string]: any})
- Sends an event or metric to the metric system
Messaging: Functions for sending messages to the channels
- async send(stepContext: WaterfallStepContext, msg: Message, allowFeedback = true)
- Sends a message to all channels and sets speak & notification feedback
- async sendPartial(stepContext: WaterfallStepContext, msg: Message, allowFeedback = true)
- Sends a partial message to all channels and sets speak & notification feedback. Ends the stage. Expects a following stage with another partial or the dialog's normal prompt.
- async sendToCurrentChannel(stepContext: WaterfallStepContext, msg: Message, allowFeedback = true)
- Sends a message only to the current channel and sets speak & notification feedback
- async sendPartialToCurrentChannel(stepContext: WaterfallStepContext, msg: Message, allowFeedback = true)
- Sends a partial message only to the current channel and sets speak & notification feedback. Ends the stage. Expects a following stage with another partial or the dialog's normal prompt.
- async sendError(stepContext: WaterfallStepContext, errorCode: ErrorCode, text?: string, error?: Error)
- Sends an error to all channels, with an optional text to be spoken, and an optional javacript error for logging purposes.
- async prompt(stepContext: WaterfallStepContext, promptId: string, choices: (Choice | string)[]): Promise
- Sets up a prompt with some choices (operations)
- modal(stepContext: WaterfallStepContext, data: { title?: string; description?: string; icon?: 'info' | 'success' | 'warning' | 'error' | null | string; }, choices?: { [value: string]: { synonyms: string[]; route: RouteActionType }; }): Promise
- Sets up a modal prompt with predefined & optionally customizable choices
- Default choices:
- Accept (value: intent.operation.sdk.accept; synonyms: aceptar, continuar, vale) => Continue the dialog with the next step
- Repeat (value: intent.operation.sdk.repeat; synonyms: reintentar, repetir) => Restarts current dialog
- Cancel (value: intent.operation.sdk.back; synonyms: cancelar, atrás, volver) => Goes back to parent dialog
- Customizable choices: if used, default choices are removed.
- value (key): main intent or text to recognize (if not intent, it is used as human readable title)
- synonyms: all the possible values to recognize (if value is intent, first synomym is used as human readable title)
- route: the RouteActionType to use on choice recognition
- NOTE: Modal, as prompts, always go in a return statement that ends the current stage.
- async function warning(stepContext: WaterfallStepContext, warning: string, message?: string): Promise
- Sets up a feedback prompt with a warning. Accept, Repeat, and Cancel are available since warnings are issued when the dialog is still in a valid state.
- async function error(stepContext: WaterfallStepContext, error: string, code?: string, message?: string): Promise
- Sets up a feedback prompt with an error. Only Repeat and Cancel are accepted here, since errors are issued when the dialog is not in a valid state.
- getText(stepContext: WaterfallStepContext, key: string, parameters: {[key: string]: string} = {}) : string
- Gets a localized text string from a key, for a key-value pair defined in the locale files.
Messages can contain data and actions.
ScreenMessage
The usual way to send messages to the webapps. They contain a screen field for identifying the screen that should be painted as well as data that will be rendered within said screen.
export class ScreenMessage extends Message {
constructor(public screen: string, public screenData: any = {}) {
super();
}
protected _toJson(stepContext: WaterfallStepContext) {
const json = super._toJson(stepContext);
json.screen = this.screen;
return { ...json, ...this.screenData };
}
static error(errorCode: ErrorCode): ScreenMessage {
return new ScreenMessage('error', { error: errorCode });
}
static warning(errorCode: ErrorCode): ScreenMessage {
return new ScreenMessage('warning', { error: errorCode });
}
}
ActionMessage
A special message without data that can incorporate actions for the front to run special logic for those actions.
export class ActionMessage extends Message {
constructor() {
super();
}
protected _toJson(stepContext: WaterfallStepContext) {
const json = super._toJson(stepContext);
return { ...json };
}
}
Actions
Special commands sent to the webapps and channels that allow special actions to be executed.
export class Action {
public name: string;
public target: string;
public postBack?: { ok: Object; };
public params?: Object;
public parameters?: Object;
private constructor() {}
static customAction(name: string, parameters: {[key: string]: any} = {}): Action {
const action = new Action();
action.name = name;
action.target = LA_SDK_ACTION_TARGET;
action.parameters = parameters;
return action;
}
static timer(countdown: number, timedAction: Object): Action {
const action = new Action();
action.name = 'LIVING_APP.TIMER';
action.target = 'aura-sdk';
action.postBack = { ok: timedAction };
action.params = { time: countdown };
return action;
}
static phoneCall(phoneNumber: string): Action {
const action = new Action();
action.name = 'CALL';
action.target = 'channel';
action.parameters = { type: 'tef.phonenumber', value: phoneNumber };
return action;
}
static sound(sound: Sound): Action {
const action = new Action();
action.name = 'SOUND';
action.target = 'channel';
action.parameters = { sound: sound };
return action;
}
static optionSelect(option: number): Action {
const action = new Action();
action.name = 'LIVING_APP.OPTION_SELECT';
action.target = LA_SDK_ACTION_TARGET;
action.parameters = { option };
return action;
}
static next(value?: number): Action {
const action = new Action();
action.name = 'LIVING_APP.NEXT';
action.target = LA_SDK_ACTION_TARGET;
action.parameters = { ...value && { value } };
return action;
}
static previous(value?: number): Action {
const action = new Action();
action.name = 'LIVING_APP.PREVIOUS';
action.target = LA_SDK_ACTION_TARGET;
action.parameters = value === undefined ? {} : { value: value };
return action;
}
static videoRestart(): Action {
const action = new Action();
action.name = 'LIVING_APP.VIDEO_RESTART';
action.target = LA_SDK_ACTION_TARGET;
return action;
}
static videoPause(): Action {
const action = new Action();
action.name = 'LIVING_APP.VIDEO_PAUSE';
action.target = LA_SDK_ACTION_TARGET;
return action;
}
static videoStop(): Action {
const action = new Action();
action.name = 'LIVING_APP.VIDEO_STOP';
action.target = LA_SDK_ACTION_TARGET;
return action;
}
static videoPlay(): Action {
const action = new Action();
action.name = 'LIVING_APP.VIDEO_PLAY';
action.target = LA_SDK_ACTION_TARGET;
return action;
}
static videoOffset(seconds: number): Action {
const action = new Action();
action.name = 'LIVING_APP.VIDEO_OFFSET';
action.target = LA_SDK_ACTION_TARGET;
action.parameters = { seconds };
return action;
}
static toast(text: string, icon: 'info' | 'success' | 'warning' | 'error' = 'info', duration = 0): Action {
const action = new Action();
action.name = 'LIVING_APP.TOAST';
action.target = LA_SDK_ACTION_TARGET;
action.parameters = { text, icon, duration };
return action;
}
static feedback(title: string, subtitle?: string, description?: string,
type: 'info'|'success'|'warning'|'error' = 'info',
showIcon: boolean = true, duration: number = 0
) {
const action = new Action();
action.name = 'LIVING_APP.FEEDBACK';
action.target = LA_SDK_ACTION_TARGET;
action.parameters = { title, subtitle, description, type, showIcon, duration };
return action;
}
}
KGB Utils: Access to the QnA system
Use the KgbClient class to access the QnA utilities:
export declare class KgbClient {
constructor(stepContext: WaterfallStepContext<any>, config: Configuration, configKey?: string);
getAnswer(): Promise<KgbLayout>;
getSuggestions(random = false): Promise<KgbQuestion[]>;
}
In order to use it, you can instantiate and use its methods as follows:
const kgbClient = new KgbClient(stepContext, this.config);
const answer = await kgbClient.getAnswer();
const suggestions: any[] = await kgbClient.getSuggestions();
where the answerData is the answer to the question and the suggestions are the related questions as configured in the datasheet. The optionalSuggestion parameters allows to get linked suggestions instead of the default ones. The boolean parameter is used for randomization.
The answers follow the following model:
export type KgbLayout = {
customType: string;
[key: string]: any;
};
export type KgbQuestion = {
id: string;
question: string;
};
Loader: Functions for registering the Living App with Aura (used in index.ts)
- excludeDialogs(dialogNames: string[], options: any): void (wrapped)
- excludes dialogs based on the options parameter
- readLocaleFolder(localePath: string): any (wrapped)
- readEnv(envPath: string): any (wrapped)
- readDialogConfig(configPath: string): any (wrapped)
Utils: Functions for doing useful things
- async function getSvgQrCode(url: string): Promise
- Gets a SVG QR Code of a url
- equalsIgnoreCase(a: string, b: string): boolean
- True if equals except casing
- equalsIgnoreAccents(a: string, b: string): boolean
- True if equals except casing & accents
- shuffleArray(array: any[]): any[]
- Returns a shuffled copy of the array
- getRandomElement(array: any[]): any
- Returns a random element of the array
- resolveNlpNumber(received: string, regExp: RegExp = /([a-zA-Z1234567890áéíóú]+)/i): string[]
- Returns a number from a human spoken text number
- currentDateInTimezone(tz: string): string
- toTimezone(date: string|Date, tz: string): Date
- toYYYY_MM_DD(date: Date): string