UNPKG

@aws-amplify/interactions

Version:

Interactions category of aws-amplify

334 lines (294 loc) • 8.96 kB
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import { AbstractInteractionsProvider } from './InteractionsProvider'; import { InteractionsOptions, AWSLexV2ProviderOptions, InteractionsResponse, InteractionsMessage, } from '../types'; import { LexRuntimeV2Client, RecognizeTextCommand, RecognizeTextCommandInput, RecognizeTextCommandOutput, RecognizeUtteranceCommand, RecognizeUtteranceCommandInput, RecognizeUtteranceCommandOutput, } from '@aws-sdk/client-lex-runtime-v2'; import { ConsoleLogger as Logger, Credentials, getAmplifyUserAgent, } from '@aws-amplify/core'; import { convert } from './AWSLexProviderHelper/utils'; import { unGzipBase64AsJson } from './AWSLexProviderHelper/commonUtils'; const logger = new Logger('AWSLexV2Provider'); interface RecognizeUtteranceCommandOutputFormatted extends Omit< RecognizeUtteranceCommandOutput, | 'messages' | 'interpretations' | 'sessionState' | 'requestAttributes' | 'audioStream' > { messages?: RecognizeTextCommandOutput['messages']; sessionState?: RecognizeTextCommandOutput['sessionState']; interpretations?: RecognizeTextCommandOutput['interpretations']; requestAttributes?: RecognizeTextCommandOutput['requestAttributes']; audioStream?: Uint8Array; } type AWSLexV2ProviderSendResponse = | RecognizeTextCommandOutput | RecognizeUtteranceCommandOutputFormatted; type lexV2BaseReqParams = { botId: string; botAliasId: string; localeId: string; sessionId: string; }; export class AWSLexV2Provider extends AbstractInteractionsProvider { private _lexRuntimeServiceV2Client: LexRuntimeV2Client; private _botsCompleteCallback: object; /** * Initialize Interactions with AWS configurations * @param {InteractionsOptions} options - Configuration object for Interactions */ constructor(options: InteractionsOptions = {}) { super(options); this._botsCompleteCallback = {}; } /** * get provider name of the plugin * @returns {string} name of the provider */ public getProviderName() { return 'AWSLexV2Provider'; } /** * Configure Interactions part with aws configuration * @param {AWSLexV2ProviderOptions} config - Configuration of the Interactions * @return {AWSLexV2ProviderOptions} - Current configuration */ public configure( config: AWSLexV2ProviderOptions = {} ): AWSLexV2ProviderOptions { const propertiesToTest = [ 'name', 'botId', 'aliasId', 'localeId', 'providerName', 'region', ]; Object.keys(config).forEach(botKey => { const botConfig = config[botKey]; // is bot config correct if (!propertiesToTest.every(x => x in botConfig)) { throw new Error('invalid bot configuration'); } }); return super.configure(config); } /** * Send a message to a bot * @async * @param {string} botname - Bot name to send the message * @param {string | InteractionsMessage} message - message to send to the bot * @return {Promise<InteractionsResponse>} A promise resolves to the response from the bot */ public async sendMessage( botname: string, message: string | InteractionsMessage ): Promise<InteractionsResponse> { // check if bot exists if (!this._config[botname]) { return Promise.reject('Bot ' + botname + ' does not exist'); } // check if credentials are present let credentials; try { credentials = await Credentials.get(); } catch (error) { return Promise.reject('No credentials'); } this._lexRuntimeServiceV2Client = new LexRuntimeV2Client({ region: this._config[botname].region, credentials, customUserAgent: getAmplifyUserAgent(), }); let response: AWSLexV2ProviderSendResponse; // common base params for all requests const reqBaseParams: lexV2BaseReqParams = { botAliasId: this._config[botname].aliasId, botId: this._config[botname].botId, localeId: this._config[botname].localeId, sessionId: credentials.identityId, }; if (typeof message === 'string') { response = await this._handleRecognizeTextCommand( botname, message, reqBaseParams ); } else { response = await this._handleRecognizeUtteranceCommand( botname, message, reqBaseParams ); } return response; } /** * Attach a onComplete callback function to a bot. * The callback is called once the bot's intent is fulfilled * @param {string} botname - Bot name to attach the onComplete callback * @param {(err: Error | null, confirmation: InteractionsResponse) => void} callback - called when Intent Fulfilled */ public onComplete( botname: string, callback: (err: Error | null, confirmation: InteractionsResponse) => void ) { // does bot exist if (!this._config[botname]) { throw new Error('Bot ' + botname + ' does not exist'); } this._botsCompleteCallback[botname] = callback; } /** * @private * call onComplete callback for a bot if configured */ private _reportBotStatus( data: AWSLexV2ProviderSendResponse, botname: string ) { const sessionState = data?.sessionState; // Check if state is fulfilled to resolve onFullfilment promise logger.debug('postContent state', sessionState?.intent?.state); const isConfigOnCompleteAttached = typeof this._config?.[botname].onComplete === 'function'; const isApiOnCompleteAttached = typeof this._botsCompleteCallback?.[botname] === 'function'; // no onComplete callbacks added if (!isConfigOnCompleteAttached && !isApiOnCompleteAttached) return; if ( sessionState?.intent?.state === 'ReadyForFulfillment' || sessionState?.intent?.state === 'Fulfilled' ) { if (isApiOnCompleteAttached) { setTimeout(() => this._botsCompleteCallback?.[botname](null, data), 0); } if (isConfigOnCompleteAttached) { setTimeout(() => this._config[botname].onComplete(null, data), 0); } } if (sessionState?.intent?.state === 'Failed') { const error = new Error('Bot conversation failed'); if (isApiOnCompleteAttached) { setTimeout(() => this._botsCompleteCallback[botname](error), 0); } if (isConfigOnCompleteAttached) { setTimeout(() => this._config[botname].onComplete(error), 0); } } } /** * Format UtteranceCommandOutput's response * decompress attributes * update audioStream format */ private async _formatUtteranceCommandOutput( data: RecognizeUtteranceCommandOutput ): Promise<RecognizeUtteranceCommandOutputFormatted> { const response: RecognizeUtteranceCommandOutputFormatted = { ...data, messages: await unGzipBase64AsJson(data.messages), sessionState: await unGzipBase64AsJson(data.sessionState), interpretations: await unGzipBase64AsJson(data.interpretations), requestAttributes: await unGzipBase64AsJson(data.requestAttributes), inputTranscript: await unGzipBase64AsJson(data.inputTranscript), audioStream: data.audioStream ? await convert(data.audioStream) : undefined, }; return response; } /** * handle client's `RecognizeTextCommand` * used for sending simple text message */ private async _handleRecognizeTextCommand( botname: string, data: string, baseParams: lexV2BaseReqParams ) { logger.debug('postText to lex2', data); const params: RecognizeTextCommandInput = { ...baseParams, text: data, }; try { const recognizeTextCommand = new RecognizeTextCommand(params); const data = await this._lexRuntimeServiceV2Client.send( recognizeTextCommand ); this._reportBotStatus(data, botname); return data; } catch (err) { return Promise.reject(err); } } /** * handle client's `RecognizeUtteranceCommand` * used for obj text or obj voice message */ private async _handleRecognizeUtteranceCommand( botname: string, data: InteractionsMessage, baseParams: lexV2BaseReqParams ) { const { content, options: { messageType }, } = data; logger.debug('postContent to lex2', data); let params: RecognizeUtteranceCommandInput; // prepare params if (messageType === 'voice') { if (typeof content !== 'object') { return Promise.reject('invalid content type'); } const inputStream = content instanceof Uint8Array ? content : await convert(content); params = { ...baseParams, requestContentType: 'audio/x-l16; sample-rate=16000; channel-count=1', inputStream, }; } else { // text input if (typeof content !== 'string') return Promise.reject('invalid content type'); params = { ...baseParams, requestContentType: 'text/plain; charset=utf-8', inputStream: content, }; } // make API call to lex try { const recognizeUtteranceCommand = new RecognizeUtteranceCommand(params); const data = await this._lexRuntimeServiceV2Client.send( recognizeUtteranceCommand ); const response = await this._formatUtteranceCommandOutput(data); this._reportBotStatus(response, botname); return response; } catch (err) { return Promise.reject(err); } } }