@aws-amplify/interactions
Version:
Interactions category of aws-amplify
334 lines (294 loc) • 8.96 kB
text/typescript
// 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);
}
}
}