wa-chat-server
Version:
Watson Assistant powered chat server
289 lines (288 loc) • 14.3 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
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());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ConversationService = exports.Types = exports.WAChatServer = void 0;
const express_1 = __importDefault(require("express"));
const cors_1 = __importDefault(require("cors"));
const conversation_1 = require("./api/conversation");
const healthCheck_1 = require("./api/healthCheck");
const auth_1 = require("./api/auth");
const Logger_1 = require("./class/Logger/Logger");
const ServicesRegistrator_1 = require("./class/ServicesRegistrator");
const googleSheets_1 = require("./api/feedback/googleSheets");
const Session_1 = require("./class/Session/Session");
const ConversationService_1 = require("./class/ConversationService/ConversationService");
Object.defineProperty(exports, "ConversationService", { enumerable: true, get: function () { return ConversationService_1.ConversationService; } });
const ConversationAssistants_1 = require("./class/ConversationAssistants");
const Types = __importStar(require("./types"));
exports.Types = Types;
const processMessage_1 = require("./methods/processMessage");
const swagger_ui_express_1 = __importDefault(require("swagger-ui-express"));
const yamljs_1 = __importDefault(require("yamljs"));
const verifyJsonRequest_1 = require("./methods/verifyJsonRequest");
const handleParsingError_1 = require("./methods/handleParsingError");
class WAChatServer {
constructor(config) {
const splitConfig = this.separateAppAndAdapterConfig(config);
this.config = Object.assign({ port: 3023, allow_origin: '*' }, splitConfig.appConfig);
const watsonAssistants = this.compileConfig(config);
this.adapterConfig = splitConfig.adapterConfig;
this.logger = config.logger || new Logger_1.Logger(config);
this.conversationAdapters = new ConversationAssistants_1.ConversationAssistants(watsonAssistants, this.config, this.logger);
this.conversationAdapter = this.conversationAdapters.getMaster();
this.servicesRegistrator = new ServicesRegistrator_1.ServicesRegistrator(this.logger, this.config);
this.adapters = new Map();
}
separateAppAndAdapterConfig(config) {
const appConfig = {};
const adapterConfig = {};
new Map(Object.entries(config)).forEach((val, key) => {
const [namespace, name, property] = key.split('__');
if (namespace === 'adapter') {
adapterConfig[name] = adapterConfig[name] || {};
adapterConfig[name][property] = val;
}
else {
appConfig[key] = val;
}
});
return { appConfig, adapterConfig };
}
compileConfig(config) {
const watsonAssistants = [
{
alias: 'MASTER',
id: config['master_id'],
environmentId: config['master_environment_id'],
apikey: config['master_apikey'],
url: config['master_url'],
timeout: Number.parseInt(config['master_timeout_ms']) || undefined,
},
];
const prefix = 'child';
const suffix = '_alias';
for (const c in config) {
if (c.substring(0, prefix.length) == prefix &&
c.substring(c.length - suffix.length, c.length) == suffix) {
const n = c.substring(prefix.length, c.length - suffix.length);
watsonAssistants.push({
alias: config[c],
id: config[prefix + n + '_id'],
environmentId: config[prefix + n + '_environment_id'],
apikey: config[prefix + n + '_apikey'] || config['master_apikey'],
url: config[prefix + n + '_url'] || config['master_url'],
timeout: Number.parseInt(config[prefix + n + '_timeout_ms'] || config['master_timeout_ms']) ||
undefined,
});
}
}
return watsonAssistants;
}
initRouter() {
var _a;
this.router = (0, express_1.default)();
const optionsJson = {
limit: '1mb',
verify: String((_a = this.config) === null || _a === void 0 ? void 0 : _a.json_raw_body) === 'true' && verifyJsonRequest_1.verifyJsonRequest,
};
this.router.use(express_1.default.json(optionsJson));
this.router.use(handleParsingError_1.handleParsingErrors);
if (this.config.allow_origin) {
this.router.use((0, cors_1.default)({
origin: this.config.allow_origin,
}));
}
this.setRoutes();
}
setRoutes() {
var _a;
this.router.use('/conversation', (0, conversation_1.conversationAPI)(this.logger, this.config, this.servicesRegistrator)(this.conversationAdapters));
this.router.use('/auth', (0, auth_1.authentication)(this.logger, this.config)); // Authentication middleware
this.router.use('/health-check', (0, healthCheck_1.healthCheckAPI)(this.logger, this.config)(this.conversationAdapter));
// Feedback middleware
this.router.use('/feedback/googleSheets', (0, googleSheets_1.googleSheetsAPI)(this.logger, this.config));
this.adapters.forEach((adapter, name) => {
this.logger.info(`Starting adapter '${name}' on endpoint 'HOST:${this.getPort()}/adapters/${name}'`);
this.router.use(`/adapters/${name}`, adapter.getRouter());
});
if (String((_a = this.config) === null || _a === void 0 ? void 0 : _a.apidoc_enabled) === 'true') {
const pathToSwagger = `${__dirname}/doc/api.swagger.yaml`;
let swaggerScheme = [];
this.router.use('/apidoc', (req, res, next) => {
try {
swaggerScheme = yamljs_1.default.load(pathToSwagger);
req.swaggerDoc = swaggerScheme;
next();
}
catch (err) {
next(` Error in file located at ${pathToSwagger} \n` + err);
}
}, swagger_ui_express_1.default.serve, swagger_ui_express_1.default.setup());
}
}
getPort() {
return this.config.VCAP_APP_PORT || this.config.port;
}
/*
* Parameter name is used as:
* 1. an adapter name in the configuration (adapter related configuration will have keys
* adapter.ADAPTER_NAME.PROPERTY_NAME)
* 2. adapter identifier in the routes - the route will be /adapters/ADAPTER_NAME
*/
addAdapter(name, AdapterClass) {
if (this.adapters.has(name)) {
throw new Error(`Attempt to overwrite adapter '${name}'`);
}
this.adapters.set(name, new AdapterClass(this.adapterConfig[name] || {}, this));
}
serve() {
const port = this.getPort();
this.initRouter();
this.router.listen(port);
this.logger.info(`Listening on port: ${port}`);
}
getRouter() {
return this.router;
}
getServicesRegistrator() {
return this.servicesRegistrator;
}
getLogger() {
return this.logger;
}
getConversationAdapter() {
return this.conversationAdapter;
}
/**
* Add specific session storage factory implementation. Can be chosen from the **wa-chat-server** library or custom
* implementation can be added which implements interface **Types.ISessionStorageFactory**.
* @param storageName can be used to specify storage when starting session using **storageName** value in configOverride
* @param storageFactory session storage factory implementation
* @param setDefault specify if the storage should be used by default (without explicitly specifying **storageName**)
* *DISCLAIMER: first custom factory added will be set as default regardless of this parameter*
* @returns
*/
addSessionStorageFactory(storageName, storageFactory, setDefault) {
if (!this.sessionStorageFactories) {
this.sessionStorageFactories = new Map();
}
if (this.sessionStorageFactories.has(storageName)) {
throw new Error(`Factory with storageName "${storageName}" already exists`);
}
this.sessionStorageFactories.set(storageName, storageFactory);
if (setDefault || !this.sessionStorageDefaultFactory) {
this.sessionStorageDefaultFactory = storageName;
}
}
// It is not necessary to start session manualy - it will be started automatically in getResponse()
// However in some cases we need to have session before getResponse() is called
// When session is modified, endSession needs to be performed
startSession(sessionId_1) {
return __awaiter(this, arguments, void 0, function* (sessionId, configOverride = {}) {
let config = {
storageName: configOverride.storageName || this.sessionStorageDefaultFactory,
singleUseStorage: configOverride.singleUseStorage || false,
lifeTimeMS: configOverride.lifeTimeMS,
prioritizeRequestContext: typeof configOverride.prioritizeRequestContext === 'boolean'
? configOverride.prioritizeRequestContext
: false,
allowForeignSessionIds: typeof configOverride.allowForeignSessionIds === 'boolean'
? configOverride.allowForeignSessionIds
: true,
};
const session = yield new Session_1.Session(sessionId, config, this.sessionStorageFactories).start();
return session;
});
}
endSession(session) {
return __awaiter(this, void 0, void 0, function* () {
yield session.save();
});
}
getResponse(sessionId_1, watsonRequest_1) {
return __awaiter(this, arguments, void 0, function* (sessionId, watsonRequest, // AssistantV1.MessageRequest
configOverride = {}) {
var _a, _b, _c;
const startTimeMS = new Date().getTime();
try {
const session = yield this.startSession(sessionId, configOverride);
const sessionContext = ((_c = (_b = (_a = session.data) === null || _a === void 0 ? void 0 : _a.assistants) === null || _b === void 0 ? void 0 : _b[session.runningAssistant]) === null || _c === void 0 ? void 0 : _c.context) || {};
const context = session.config.prioritizeRequestContext
? Object.assign(Object.assign({}, sessionContext), (watsonRequest.context || {})) : Object.assign(Object.assign({}, (watsonRequest.context || {})), sessionContext);
const watsonReq = Object.assign(Object.assign({}, watsonRequest), { context });
const watsonResponse = yield (0, processMessage_1.processMessage)({
session,
watsonAssistants: this.conversationAdapters,
preProcessingServices: this.servicesRegistrator.getPreProcessingServices(),
postProcessingServices: this.servicesRegistrator.getPostProcessingServices(),
conversationServices: this.servicesRegistrator.getConversationServices(),
payload: watsonReq,
config: this.config,
logger: this.logger,
});
this.logger.info('WAChatServer - getResponse', {
watsonRequest,
watsonResponse,
durationMS: `${new Date().getTime() - startTimeMS}`,
});
return {
session: session,
watsonResponse: watsonResponse.newPayload,
routeNotification: watsonResponse.addons.route,
};
}
catch (error) {
this.logger.error('WAChatServer - getResponse ERROR', {
watsonRequest,
error,
durationMS: `${new Date().getTime() - startTimeMS}`,
});
}
});
}
}
exports.WAChatServer = WAChatServer;