wa-chat-server
Version:
Watson Assistant powered chat server
304 lines (303 loc) • 13.7 kB
JavaScript
"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';