UNPKG

wa-chat-server

Version:

Watson Assistant powered chat server

200 lines (199 loc) 10.2 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()); }); }; 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 };