UNPKG

wa-chat-server

Version:

Watson Assistant powered chat server

304 lines (303 loc) 13.7 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.WatsonWrapper = void 0; const ConversationServiceValidationException_1 = require("../exception/ConversationServiceValidationException"); const ConversationServiceRuntimeException_1 = require("../exception/ConversationServiceRuntimeException"); /** * This class * 1) pre-process the request to be sent to Watson * 2) transparently calls watson repeatedly if a conversation service call * is reqested in the context */ class WatsonWrapper { constructor(logger, adapter, conversationServices, preProcessingServices, postProcessingServices, request, meta, session) { this.logger = logger; this.adapter = adapter; this.conversationServices = conversationServices; this.preProcessingServices = preProcessingServices; this.postProcessingServices = postProcessingServices; this.request = request; this.meta = meta; this.session = session; // if Conversation Services are used then watson is called more than once for // just one utterance and all watson responses must be taken into account // when preparing the final response for the client (therefore it is array) this.watsonRes = []; } copyWatsonReqFrom(source) { const sourceCopy = Object.assign({}, source); delete sourceCopy.userId; this.watsonReq = sourceCopy; if (source.context) { this.watsonReq.context = Object.assign({}, source.context); } } /** * Adds user id to this.watsonReq */ mapMetaInfoToContext() { if (this.meta) { this.watsonReq.context = this.watsonReq.context || {}; this.watsonReq.context.client = this.watsonReq.context.client || {}; // eslint-disable-next-line @typescript-eslint/camelcase Object.assign(this.watsonReq.context.client, Object.fromEntries(['url', 'userAgent', 'resolution', 'domain', 'dontTrack'] .filter((k) => this.meta[k]) .map((k) => [k, this.meta[k]]))); } } /** * Adds user id to this.watsonReq */ handleUserId() { if (this.request.userId) { this.watsonReq.context = this.watsonReq.context || {}; this.watsonReq.context.metadata = this.watsonReq.context.metadata || {}; // eslint-disable-next-line @typescript-eslint/camelcase this.watsonReq.context.metadata.user_id = this.request.userId; } } copyContextFromResToReq() { const watsonRes = this.watsonRes[this.watsonRes.length - 1]; this.watsonReq.context = Object.assign(Object.assign({}, this.watsonReq.context), Object.entries(watsonRes.result.context).reduce((result, [key, val]) => { if (key.startsWith(WatsonWrapper.requestPrefix)) { result[key] = null; } else { result[key] = val; } return result; }, {})); } callWatson() { return __awaiter(this, void 0, void 0, function* () { const watsonRes = yield this.callWatsonMessage(this.watsonReq); this.watsonRes.push(watsonRes); }); } getConversationService() { return __awaiter(this, void 0, void 0, function* () { yield this.conversationServices.getSchemasOnFS(); this.conversationService = this.conversationServices.getServiceByName(this.conversationServiceName); yield this.conversationService.loadSchemas(); }); } callConversationService() { return __awaiter(this, void 0, void 0, function* () { this.conversationServiceReq = this.getConversationServiceRequest(); try { this.conversationService.validateRequest(this.conversationServiceReq); this.conversationServiceRes = yield this.conversationService.getResponse(this.conversationServiceReq); this.logger.info(`${this.constructor.name}: Conversation service ${this.conversationServiceName} call; ` + `request, response:`, this.conversationServiceReq, this.conversationServiceRes); } catch (e) { this.conversationServiceError = e.toString(); this.logger.error(`${this.constructor.name}: Conversation service ${this.conversationServiceName} failed; ` + `request, error:`, this.conversationServiceReq, e); } }); } getResponse() { return __awaiter(this, void 0, void 0, function* () { try { this.preProcessedReq = Object.assign({}, this.request); for (const preProcessorService of this.preProcessingServices) { yield preProcessorService.processRequest(this.preProcessedReq, this.session); } this.copyWatsonReqFrom(this.preProcessedReq); this.handleUserId(); this.mapMetaInfoToContext(); for (this.callNum = 1; true; this.callNum++) { yield this.callWatson(); this.copyWatsonReqFrom(this.preProcessedReq); this.copyContextFromResToReq(); this.handleUserId(); this.mapMetaInfoToContext(); if (this.conversationServiceCallRequired()) { this.validateServiceName(); yield this.getConversationService(); this.conversationServiceError = null; yield this.callConversationService(); if (this.conversationServiceError) { this.addFailureIntentToWatsonReq(); this.addConversationServiceErrorToWatsonReq(); } else { this.conversationService.validateResponse(this.conversationServiceRes); this.addSuccessIntentToWatsonReq(); this.addConversationServiceResultToWatsonReq(); } } else { return yield this.getFinalWatsonRes(); } } } catch (e) { this.logger.error('WatsonWrapper', e, e === null || e === void 0 ? void 0 : e.message); throw e; } }); } getFinalWatsonRes() { return __awaiter(this, void 0, void 0, function* () { const watsonRes = this.getMergedWatsonRes(); for (const postProcessorService of this.postProcessingServices) { yield postProcessorService.processResponse(watsonRes); } return watsonRes; }); } // Merges all responses in this.watsonRes into just one response getMergedWatsonRes() { if (this.watsonRes.length > 1) { const response = Object.assign({}, this.watsonRes[this.watsonRes.length - 1]); if (response.result) { response.result = Object.assign({}, response.result); } if (response.result.output) { response.result.output = Object.assign({}, response.result.output); } if (response.result.output) { ['generic', 'log_messages', 'nodes_visited', 'text'].forEach((key) => { const value = []; this.watsonRes.forEach((res) => { if (res.result.output[key]) { res.result.output[key].forEach((item) => { value.push(item); }); } }); response.result.output[key] = value; }); } this.logger.info(`${this.constructor.name}: merged watson response`, response); return response; } return this.watsonRes[0]; } addFailureIntentToWatsonReq() { this.addIntentToWatsonReq(`${WatsonWrapper.failureIntent}${this.callNum}`); } addSuccessIntentToWatsonReq() { this.addIntentToWatsonReq(`${WatsonWrapper.successIntent}${this.callNum}`); } addIntentToWatsonReq(intentName) { this.watsonReq.intents = [ { intent: intentName, confidence: 1.0, }, ]; } /** * Returns a function that can map the provided value to the given * path of the watson context; the returned function can handle both * primitive typed parameter as well as an object */ createMapperToWatsonReqContext(path) { return (value) => { this.watsonReq.context = this.watsonReq.context || {}; let ctx = this.watsonReq.context; if (path === '') { if (value instanceof Object) { Object.assign(ctx, value); } else { throw new ConversationServiceRuntimeException_1.ConversationServiceRuntimeException(`Not possible to map primitive value '${value}' to context`); } } else { const parts = path.split('.'); parts.forEach((key, i) => { if (i < parts.length - 1) { ctx[key] = ctx[key] || {}; ctx = ctx[key]; } else { if (value instanceof Object) { ctx[key] = Object.assign({}, value); } else { ctx[key] = value; } } }); } }; } addConversationServiceErrorToWatsonReq() { let path = this.getRequestDirectiveFromWatsonRes('errorTarget'); path = path === null ? WatsonWrapper.defaultErrorPathInWatsonCtx : path; this.createMapperToWatsonReqContext(path)(this.conversationServiceError); } addConversationServiceResultToWatsonReq() { let path = this.getRequestDirectiveFromWatsonRes('resultTarget'); path = path === null ? WatsonWrapper.defaultResultPathInWatsonCtx : path; this.createMapperToWatsonReqContext(path)(this.conversationServiceRes); } conversationServiceCallRequired() { return this.getConversationServiceName() !== null; } getRequestDirectiveFromWatsonRes(param) { const contextKey = `${WatsonWrapper.requestPrefix}${param}`; const watsonRes = this.watsonRes[this.watsonRes.length - 1]; return watsonRes.result && watsonRes.result.context && watsonRes.result.context[contextKey] !== undefined ? watsonRes.result.context[contextKey] : null; } getConversationServiceName() { return this.getRequestDirectiveFromWatsonRes('service'); } validateServiceName() { const serviceName = this.getConversationServiceName(); if (this.conversationServices.getServiceByName(serviceName) === undefined) { const target = `$${WatsonWrapper.requestPrefix}service`; throw new ConversationServiceValidationException_1.ConversationServiceValidationException(`Conversation service '${serviceName}' received in ${target} not registered`); } this.conversationServiceName = serviceName; } getConversationServiceRequest() { const regex = new RegExp(`^${WatsonWrapper.requestPrefix}params-`); const watsonRes = this.watsonRes[this.watsonRes.length - 1]; return Object.entries(watsonRes.result.context).reduce((result, [k, v]) => { if (regex.test(k) && v !== null) { let current = result; const path = k.replace(regex, '').split('-'); path.forEach((key, i) => { if (i < path.length - 1) { current[key] = current[key] || {}; current = current[key]; } else { current[key] = v; } }); } return result; }, {}); } callWatsonMessage(request) { return __awaiter(this, void 0, void 0, function* () { const watsonRequest = Object.assign({}, request); return this.adapter.message(watsonRequest); }); } } exports.WatsonWrapper = WatsonWrapper; WatsonWrapper.successIntent = 'response'; WatsonWrapper.failureIntent = 'error'; WatsonWrapper.requestPrefix = 'request-'; WatsonWrapper.defaultResultPathInWatsonCtx = 'response.result'; WatsonWrapper.defaultErrorPathInWatsonCtx = 'response.error';