asksuite-core
Version:
542 lines (453 loc) • 18 kB
JavaScript
/* eslint-disable no-case-declarations */
const AWSLambdaCaller = require('./services/AWSLambdaCaller');
const Util = require('./util');
const DialogFlowAccessor = require('./services/DialogFlowAccessor');
const KeyMatcherAccessor = require('./services/KeyMatcherAccessor');
const NLPClassifierAccessor = require('./services/NLPClassifierAccessor');
const ExactMatcherAccessor = require('./services/ExactMatcherAccessor');
const OpenAiAcessor = require('./services/OpenAiAcessor');
const defaultFallbackIntent = require('./util/defaultFallbackIntent');
const RequestPreprocessor = require('./util/RequestPreprocessor');
const AsksuiteProcessingNaturalUtils = require('./AsksuiteProcessingNaturalUtils');
const IntEncoder = require('./util/IntEncoder')();
const NodeCache = require('node-cache');
const AsksuiteTranslator = require('./util/Translator');
const StringUtils = require('./util/StringUtils');
const _ = require('lodash');
const PrometheusAcessor = require('./services/PrometheusAccessor');
const SophiaAcessor = require('./services/SophiaAccessor');
const GlobalCacheManager = require('./services/GlobalCacheManager');
const QuoteDataAccessor = require('./services/QuoteDataAccessor');
const MediaResolver = require('./util/MediaResolver');
const DEFAULT_LANGUAGE = 'pt-br';
const GROUP_ID = 'GROUP_ID';
const DIALOG_FLOW = 'DIALOG_FLOW';
const KEY_MATCHER = 'KEY_MATCHER';
const EXACT_MATCHER = 'EXACT_MATCHER';
const OPEN_AI = 'OPEN_AI';
const NLP_CLASSIFIER = 'NLP_CLASSIFIER';
const PROMETHEUS = 'PROMETHEUS';
const SOPHIA = 'SOPHIA';
const intentsNotFoundCache = new NodeCache({ stdTTL: 300, checkperiod: 120 });
const globalCachebleLayers = [{ name: NLP_CLASSIFIER, ttl: 7200 }];
/**
* @type {GlobalCacheManager}
*/
let globalCacheManager;
class AsksuiteProcessingNatural {
static get ORDERED_ACCESSORS() {
return [GROUP_ID, PROMETHEUS, EXACT_MATCHER, NLP_CLASSIFIER, DIALOG_FLOW, KEY_MATCHER, OPEN_AI];
}
constructor(config) {
this.config = config;
this.utils = new AsksuiteProcessingNaturalUtils(this.config);
this.exactMatcherAccessor = new ExactMatcherAccessor({
config: config.config,
redis: config.redisCore || config.redis,
});
this.setGlobalCache();
}
resolverOrder(config) {
const resolvers = AsksuiteProcessingNatural.ORDERED_ACCESSORS;
if (config.SOPHIA_NLP_ENABLED) {
return [GROUP_ID, SOPHIA];
}
if (!config.RESOLVERS_TO_BE_USED || !config.RESOLVERS_TO_BE_USED.length) {
return resolvers;
}
return resolvers.filter((r) => config.RESOLVERS_TO_BE_USED.includes(r));
}
findIntent(originalRequest) {
return new Promise((resolve, reject) => {
const executor = async (originalRequest) => {
let intent = {
fromCache: false,
};
// Formata texto
const request = await RequestPreprocessor.process(originalRequest);
const language = this.utils.resolveLanguage(request.language, request.languages);
const key = `${request.companyId}.${request.userId}.${language}`;
const {
keyMatcherAccessor,
openAIAccessor,
nlpClassifierAccessor,
prometheusAccessor,
sophiaAccessor,
dialogFlowAccessor,
} = this.resolveConnectors(request, originalRequest);
this.dialogFlowAccessor = dialogFlowAccessor;
const orderOption = this.resolverOrder(request.config);
request.findIntent = defaultFallbackIntent(request.companyId);
request.translated = false;
request.translation = null;
const processingSteps = [];
for (const accessorName of orderOption) {
const cachedIntent = await this.processCacheGlobal(
accessorName,
originalRequest,
);
if (cachedIntent) {
intent = cachedIntent;
} else {
switch (accessorName) {
case GROUP_ID:
const extractedValue = IntEncoder.extract(originalRequest.text);
if (extractedValue) {
intent = this.utils.findDialogByIntent(request, 'continuar_atendimento');
const [groupId, attendanceId] = extractedValue.split('-');
intent.groupId = groupId;
intent.attendanceId = attendanceId;
}
break;
case DIALOG_FLOW:
intent = await this.resolveDialogFlowAccessor(request, language);
break;
case KEY_MATCHER:
intent = await keyMatcherAccessor.resolveText(request);
if (Util.isNaoentendi(intent.intent) && language !== DEFAULT_LANGUAGE) {
const toSend = await this.utils.copyRequestToDefaultLanguage(request);
intent = await keyMatcherAccessor.resolveText(toSend);
}
intent.keywordTry = { try: true };
break;
case PROMETHEUS:
const [prometheusResult, nlpClassifierResult] = await Promise.all([
prometheusAccessor.resolveText(request, originalRequest, true),
nlpClassifierAccessor.resolveText(originalRequest),
]);
intent = prometheusResult;
intent.processingStep.nlpClassifierResult = nlpClassifierResult;
if (
Util.shouldDisambiguatePrometheusAndNlpResults(
prometheusResult,
nlpClassifierResult,
)
) {
intent.processingStep.disambiguate = true;
intent.dialogsToDisambiguate = [
{
intent: intent.intent,
dialog: intent.dialog,
intentLabel: intent?.intentLabel,
},
{ intent: nlpClassifierResult.intent, dialog: nlpClassifierResult.dialog },
];
}
break;
case SOPHIA:
if (request.config.SOPHIA_NLP_ENABLED && !intent?.groupId) {
intent = await sophiaAccessor.resolveText(originalRequest);
}
break;
case EXACT_MATCHER:
if (request.defaultLanguage) {
intent = await this.exactMatcherAccessor.resolveText(request);
}
break;
case OPEN_AI:
intent = await openAIAccessor.resolveText(originalRequest);
break;
case NLP_CLASSIFIER:
if (nlpClassifierAccessor.isSupportedLanguage(language)) {
const [nlpClassifierResult, dialogFlowResult] = await Promise.all([
nlpClassifierAccessor.resolveText(originalRequest),
this.resolveDialogFlowAccessor(request, language),
]);
intent = nlpClassifierResult;
intent.processingStep.dialogFlowResult = dialogFlowResult;
if (Util.shouldDisambiguateNlpAndDialogFlowResults(intent, dialogFlowResult)) {
intent.processingStep.disambiguate = true;
intent.dialogsToDisambiguate = [
{ intent: intent.intent, dialog: intent.dialog },
{ intent: dialogFlowResult.intent, dialog: dialogFlowResult.dialog },
];
}
}
break;
}
}
this.getProcessorStep(intent, accessorName, request.language, processingSteps);
if (request.findIntent && request.findIntent.groupId) {
intent.groupId = request.findIntent.groupId;
}
request.findIntent = intent;
if (intent.intent) {
if (this.shouldUseGlobalCache(accessorName)) {
try {
await globalCacheManager.set(accessorName, originalRequest, intent);
} catch (err) {
console.log(`Error setting ${intent} intent into ${accessorName}`, err);
}
}
}
if (accessorName === SOPHIA || (intent.intent && !Util.isNaoentendi(intent.intent))) {
intent.resolver = accessorName;
if (intent.resolver === KEY_MATCHER) {
intent.keywordTry.match = intent.intent;
intent.keywordTry.intent = intent.intent;
}
// FOR exit condition
break;
}
}
if (Util.isNaoentendi(intent.intent)) {
let count =
originalRequest.subsequentNotUnderstoodCount || intentsNotFoundCache.get(key) || 0;
intentsNotFoundCache.set(key, ++count);
const { ATTEMPTS_BEFORE_DETECTING_LANGUAGE, LANGUAGE_DETECTION_CONFIDENCE } =
request.config;
if (count >= ATTEMPTS_BEFORE_DETECTING_LANGUAGE) {
const detection = await this.utils.detectLanguage(request);
if (detection.confidence >= LANGUAGE_DETECTION_CONFIDENCE) {
const languageCode = detection.isSupported
? detection.language.code
: detection.fallbackLanguage.code;
if (request.language.substring(0, 2) !== languageCode.substring(0, 2)) {
intent.languageDetected = detection;
intentsNotFoundCache.del(key);
}
}
}
if (count > 1 && !intent.languageDetected && !intent.groupId) {
const fallbackIntent = defaultFallbackIntent(request.companyId, count);
intent = this.utils.findDialogByIntent(request, fallbackIntent.intent);
}
} else {
intentsNotFoundCache.del(key);
}
intent.translated = request.translated;
intent.translation = request.translation;
intent.processingSteps = processingSteps;
return intent;
};
executor(originalRequest).then(resolve).catch(reject);
});
}
async processCacheGlobal(accessorName, originalRequest) {
if (this.shouldUseGlobalCache(accessorName)) {
try {
const cachedIntent = await globalCacheManager.get(accessorName, originalRequest);
if (cachedIntent) {
console.log(`OBTAINED ${cachedIntent.intent} BY CACHE FOR LAYER ${accessorName}`);
return {
...cachedIntent,
processingStep: { fromGlobalCache: true },
};
}
} catch (e) {
console.error(`ERROR ON GLOBAL CACHE`, e);
}
}
return null;
}
resolveConnectors(request, originalRequest) {
const awsLambdaCaller = new AWSLambdaCaller(request.config.CORE_LAMBDA_AWS);
const dialogFlowAccessor = new DialogFlowAccessor(awsLambdaCaller);
const keyMatcherAccessor = new KeyMatcherAccessor(
awsLambdaCaller,
request.config.OPEN_AI_ENABLED,
);
const openAIAccessor = new OpenAiAcessor(
request.config.OPEN_AI_URL,
request.config.OPEN_AI_ENABLED,
originalRequest,
);
const nlpClassifierAccessor = new NLPClassifierAccessor(
request.config.NLP_CLASSIFIER_LAYER_URL,
request.config.NLP_CLASSIFIER_PARAMETERS,
);
const prometheusAccessor = new PrometheusAcessor(
request.config.PROMETHEUS_PARAMETERS,
this.config.redis,
);
const sophiaAccessor = new SophiaAcessor(
request.config.SOPHIA_NLP_URL,
request.config.SOPHIA_NLP_ENABLED,
originalRequest,
);
return {
keyMatcherAccessor,
openAIAccessor,
nlpClassifierAccessor,
prometheusAccessor,
sophiaAccessor,
dialogFlowAccessor,
};
}
async resolveDialogFlowAccessor(request, language) {
if (!this.dialogFlowResult) {
request = _.cloneDeep(request);
this.dialogFlowResult = await this.utils.findIntentDialogFlow(
this.dialogFlowAccessor,
request,
false,
);
this.dialogFlowResult.processingStep = this.dialogFlowResult.processingStep || {};
if (Util.isNaoentendi(this.dialogFlowResult.intent) && language !== DEFAULT_LANGUAGE) {
this.dialogFlowResult = await this.utils.findIntentDialogFlow(
this.dialogFlowAccessor,
request,
true,
);
}
const processingStep = this.dialogFlowResult.processingStep;
const isChangeLanguage =
Util.isChangeLanguage(this.dialogFlowResult.intent) ||
Util.isChangeLanguage(processingStep?.intentFound);
processingStep.isChangeLanguageRequest = isChangeLanguage;
if (
isChangeLanguage &&
this.dialogFlowResult.parameterFields &&
this.dialogFlowResult.parameterFields.language
) {
await this.processChangeLanguageRequest(request);
}
}
return this.dialogFlowResult;
}
getProcessorStep(intent, resolver, language, processingSteps) {
if (intent && intent.processingStep) {
try {
processingSteps.push({
resolver,
language,
processingStep: JSON.parse(
JSON.stringify(intent.processingStep, this.removeCircularReferencesOnStringify()),
),
});
delete intent.processingStep;
} catch (e) {
console.log('[AsksuiteProcessingNatural.getProcessorStep] error', e);
console.log(
'[AsksuiteProcessingNatural.getProcessorStep] error intent',
JSON.stringify(intent),
);
}
}
}
removeCircularReferencesOnStringify() {
const seen = new WeakSet();
return (_, value) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return undefined;
}
seen.add(value);
}
return value;
};
}
async processChangeLanguageRequest(request) {
if (
!(typeof this.dialogFlowResult.parameterFields === 'object') ||
!this.dialogFlowResult.parameterFields.language
) {
return;
}
this.dialogFlowResult.intent = 'trocar_idioma';
const languageListValue = this.dialogFlowResult.parameterFields.language.listValue || {
values: [],
};
const googleTranslateKey = request.config.GOOGLE_TRANSLATE_KEY;
const currentLanguageCode = request.language.substring(0, 2);
let languageRequested;
// targetLanguage is used to discover which language the traveller is using
let targetLanguage = request.translated ? 'pt' : undefined;
if (!targetLanguage) {
const detectedLanguage = await AsksuiteTranslator.detectLanguage(
googleTranslateKey,
request.text,
);
if (detectedLanguage && detectedLanguage.length) {
targetLanguage = detectedLanguage[0].language;
} else {
targetLanguage = currentLanguageCode;
}
}
for (const value of languageListValue.values) {
const language = StringUtils.unaccent(value.stringValue);
const valueLanguageInfo = await AsksuiteTranslator.getCodeByLanguage(
googleTranslateKey,
language,
targetLanguage,
);
// eslint-disable-next-line eqeqeq
if (valueLanguageInfo && valueLanguageInfo.code != currentLanguageCode) {
valueLanguageInfo.name = valueLanguageInfo.name.replace(/\(\w+\)/g, '');
languageRequested = valueLanguageInfo;
break;
}
}
if (!languageRequested) {
return;
}
const languageInfo = await AsksuiteTranslator.getLanguageByCode(
googleTranslateKey,
languageRequested.code,
languageRequested.code,
);
if (!languageInfo) {
return;
}
const enIsSupported = this.utils.isLanguageSupportedByCompany(request, 'en');
const fallbackLanguageCode = enIsSupported ? 'en' : currentLanguageCode;
const fallbackLanguage = await AsksuiteTranslator.getLanguageByCode(
googleTranslateKey,
fallbackLanguageCode,
fallbackLanguageCode,
);
const isSupported = this.utils.isLanguageSupportedByCompany(request, languageInfo.code);
if (!isSupported) {
languageInfo.name = (
await AsksuiteTranslator.getLanguageByCode(
googleTranslateKey,
languageInfo.code,
fallbackLanguageCode,
)
).name;
}
const languageDetected = {
language: languageInfo,
isSupported,
fallbackLanguage,
};
this.dialogFlowResult.languageDetected = languageDetected;
this.dialogFlowResult.processingStep.languageDetected = languageDetected;
}
resolveMedia(request) {
return new Promise((resolve, reject) => {
try {
const mediaResolver = new MediaResolver(request.companyId);
mediaResolver.resolve(request.media, request.intents).then(resolve).catch(reject);
} catch (e) {
reject(e);
}
});
}
async extractQuoteData(request) {
request = await RequestPreprocessor.process(request);
const awsLambdaCaller = new AWSLambdaCaller(request.config.CORE_LAMBDA_AWS);
const quoteDataAccessor = new QuoteDataAccessor(awsLambdaCaller);
return await quoteDataAccessor.resolveText(request);
}
async extractTextNumbers(request) {
request = await RequestPreprocessor.process(request);
const awsLambdaCaller = new AWSLambdaCaller(request.config.CORE_LAMBDA_AWS);
const quoteDataAccessor = new QuoteDataAccessor(awsLambdaCaller);
return await quoteDataAccessor.resolveTextNumbers(request);
}
setGlobalCache() {
if (globalCacheManager) {
return globalCacheManager;
}
if (!this.config.redis) {
return;
}
globalCacheManager = new GlobalCacheManager(this.config.redis);
globalCacheManager.addLayers(globalCachebleLayers);
}
shouldUseGlobalCache(accessorName) {
return globalCacheManager && globalCacheManager.isGlobalLayer(accessorName);
}
}
module.exports = AsksuiteProcessingNatural;