npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@telefonica/la-bot-sdk

v3.0.1

Published

Living Apps Bot SDK

Downloads

164

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
  • getCallingIntent(stepContext: WaterfallStepContext): string
    • Gets the intent that reached the dialog. Eg. intent.operation.libraryname.back
  • 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