wa-chat-server
Version:
Watson Assistant powered chat server
200 lines (199 loc) • 10.2 kB
JavaScript
;
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 = void 0;
/**
* An interface for a class implementing a service which may be called from
* the conversation.
*
* The approach how to call such service from the conversation is:
* 1) Populate the context variable $request-service with the name of the
* service to be called.
* 2) Populate (typically in the same node) the input parameters relevant
* for the selected service. All parameters are prefixed with
* $request-params (so the complete name for each parameter is
* $request-params-PARAM_NAME resp. $request-params-KEY-NESTED_KEY in
* case of a hierarchical request structure).
* 3) Optionally we may populate also $request-resultTarget and
* $request-errorTarget. The first one defaults to 'response.result' and
* the second one to 'response.error' which means that in case of success
* the service result will be written to the Watson context variable
* $response.result and in case of failure the error object will be
* written to the context variable $response.error. If we need to covert
* existing Watson skill which uses e. g. direct cloud function calls to a
* solution with conversation services and we want to minimize the impact
* on the skill then it may be convenient to use these parameters. to
* align the output with existing implementation. Setting
* $request-resultTarget or $error-errorTarget to an empty string will
* cause that the data will be written directly to the context root. For
* new implementations we should always use the default locations.
* 4) Respond with some text (possibly empty). If there are more conversation
* service calls within one chat response, the texts defined in watson
* assistant are joined into one response.
* Once the service returns the application, it will populate Watson context
* $response variable and it will call Watson again with a forced intent
* #response1 or #error1. The conversation is expected to:
* 1. Handle the intent #response1 and
* a) return some response which typically uses the returned result
* $response.result - the format of this object is specific for
* each service).
* b) or make another call by populating $request (unlike usual
* context variables the application cleans $request with each
* Watson call so the conversation developer does not need to
* care about the values stored in $request before the previous
* call). In this case the result is communicated to
* the conversation as a forced intent #response2 resp. #error2
* (#response3, #response4 etc. - the incrementing is reset
* once we stop filling $request and return a standard response
* to the chat user).
* 2. Handle the intent #error1 and communicate the problem to the chat
* user. Error details are stored in $response.error.
*
* In order to define a new service callable from the Watson skill we have to:
* 1) Implement a class extending ConversationService in
* src/class/ConversationService (e. g. ConversationServiceHelloWorld.ts).
* 2) Define the structure of the request and the response in a JSON schema
* stored in the directories src/jsonschema/request and
* src/jsonschema/response (e. g. src/jsonschema/request/HelloWorld.json and
* src/jsonschema/response/HelloWorld.json - the name must match with
* the name of the service). Don't forget to fill in all
* descriptions as the schemas will be used for generating of the
* documentation).
* For PoCs we may skip JSON schemas by setting the conversation
* service properties 'requestValidation' and 'responseValidation'
* to false - in this case json schemas are not required. It also makes
* sense to set 'responseValidation' to false in production in case
* the conversion service is passing the result of an external call
* which is weakly typed. However it still makes sense to have the
* response JSON schema which will be used for the generation of the
* documentation.
* 3) Register the service in src/api/conversation.ts
* either using ConversationServices.registerService()
* or ConversationServices.registerAllServices()
*/
const ajv_1 = __importDefault(require("ajv"));
const SchemaValidationException_1 = require("../../exception/SchemaValidationException");
class ConversationService {
constructor(config) {
this.config = config;
// a compiled request schema
this.requestSchema = null;
// a compiled response schema
this.responseSchema = null;
this.config = Object.assign({ services: null, requestValidation: true, responseValidation: true }, config);
this.logger = config.logger;
}
setServices(services) {
this.config.services = services;
}
getObligatorySchemas() {
const retVal = [];
if (this.config.requestValidation) {
retVal.push(`${this.getRequestDir()}/${this.getServiceName()}.json`);
}
if (this.config.responseValidation) {
retVal.push(`${this.getResponseDir()}/${this.getServiceName()}.json`);
}
return retVal;
}
getOptionalSchemas() {
const retVal = [];
if (!this.config.requestValidation) {
retVal.push(`${this.getRequestDir()}/${this.getServiceName()}.json`);
}
if (!this.config.responseValidation) {
retVal.push(`${this.getResponseDir()}/${this.getServiceName()}.json`);
}
return retVal;
}
getRequestDir() {
return this.config.services.absolutePath('request');
}
getResponseDir() {
return this.config.services.absolutePath('response');
}
getServiceName() {
return this.constructor.name.replace(/^ConversationService/, '');
}
getCompiledSchema(path, ajv) {
return __awaiter(this, void 0, void 0, function* () {
let schemaJson;
try {
const schemaStr = yield this.config.services.getSchemaAsStr(path);
schemaJson = JSON.parse(schemaStr);
}
catch (e) {
throw new Error(`Schema ${path}: Invalid JSON - ${e.toString()}`);
}
try {
return ajv.compile(schemaJson);
}
catch (e) {
throw new Error(`Schema ${path}: Invalid schema - ${e.toString()}`);
}
});
}
/**
* @pre ConversationServices.getSchemasOnFS() has already been called
* @post Compiled json schemas are stored in this.requestSchema and this.responseSchema
*/
loadSchemas() {
return __awaiter(this, void 0, void 0, function* () {
const reqSchemaPath = `${this.getRequestDir()}/${this.getServiceName()}.json`;
const resSchemaPath = `${this.getResponseDir()}/${this.getServiceName()}.json`;
const ajv = new ajv_1.default({ validateSchema: true, allErrors: true });
yield Promise.all([
(() => __awaiter(this, void 0, void 0, function* () {
if (!this.requestSchema && (yield this.config.services.isSchemaOnFS(reqSchemaPath))) {
this.requestSchema = yield this.getCompiledSchema(reqSchemaPath, ajv);
}
}))(),
(() => __awaiter(this, void 0, void 0, function* () {
if (!this.responseSchema && (yield this.config.services.isSchemaOnFS(resSchemaPath))) {
this.responseSchema = yield this.getCompiledSchema(resSchemaPath, ajv);
}
}))(),
]);
});
}
validateRequest(request) {
if (this.requestSchema && this.config.requestValidation) {
const valid = this.requestSchema(request);
if (!valid) {
const error = `request validation failed - request='${JSON.stringify(request)}'`;
const details = `, errors='${this.decodeAjvErrors(this.requestSchema.errors)}'`;
throw new SchemaValidationException_1.SchemaValidationException(`Conversation service ${this.getServiceName()}: ${error}${details}`);
}
}
}
validateResponse(response) {
if (this.responseSchema && this.config.responseValidation) {
const valid = this.responseSchema(response);
if (!valid) {
const error = `response validation failed - response='${JSON.stringify(response)}'`;
const details = `, errors='${this.decodeAjvErrors(this.responseSchema.errors)}'`;
throw new SchemaValidationException_1.SchemaValidationException(`Conversation service ${this.getServiceName()}: ${error}${details}`);
}
}
}
decodeAjvErrors(errors) {
return errors
.map((error) => {
return `${error.dataPath}: ${error.message}`;
})
.join(', ');
}
}
exports.ConversationService = ConversationService;
module.exports = { ConversationService };