UNPKG

node-red-node-watson

Version:
443 lines (393 loc) 12.8 kB
/** * Copyright 2018 IBM Corp. * * Licensed under the Apache License, Version 2.0 (the 'License'); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an 'AS IS' BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. **/ module.exports = function(RED) { const SERVICE_IDENTIFIER = 'assistant', OLD_SERVICE_IDENTIFIER = 'conversation', SERVICE_VERSION = '2021-11-27', AssistantV2 = require('ibm-watson/assistant/v2'), { IamAuthenticator } = require('ibm-watson/auth'), INVALID_SESSION = 'Invalid Session'; var pkg = require('../../package.json'), serviceutils = require('../../utilities/service-utils'), payloadutils = require('../../utilities/payload-utils'), service = null, sApikey = null; service = serviceutils.getServiceCreds(SERVICE_IDENTIFIER); if (!service) { service = serviceutils.getServiceCreds(OLD_SERVICE_IDENTIFIER); } if (service) { sApikey = service.apikey ? service.apikey : ''; } RED.httpAdmin.get('/watson-assistant-v2/vcap', function(req, res) { res.json(service ? { bound_service: true } : null); }); function Node(config) { var node = this; RED.nodes.createNode(this, config); function setCredentials(msg) { var creds = { apikey : sApikey || node.credentials.apikey || config.apikey, }; if (msg.params) { if (msg.params.apikey) { creds.apikey = msg.params.apikey; } } return creds; } function credentialCheck(k) { if (!k) { return Promise.reject('Missing Watson Assistant service credentials'); } return Promise.resolve(); } function payloadCheck(msg) { if (msg.payload && 'string' != typeof msg.payload) { return Promise.reject('msg.payload must be either empty or a string'); } return Promise.resolve(); } function idCheck(msg) { if (!config.assistant_id && !(msg.params && msg.params.assistant_id)) { return Promise.reject('Missing assistant_id. Check node documentation.'); } return Promise.resolve(); } function setSessionID(msg) { let session_id = null; let id = null; if (msg.params && 'reset_session' in msg.params) { // There is a reset request, therefore no need // to look it up } else { if (!config.multisession) { if (msg.params && 'session_id' in msg.params && !msg.params.session_id) { // Have a session id in single session mode // but its a null string so force a reset of the session. } else { id = node.context().flow.get('session_id'); } } else if (msg.params && msg.params.session_id) { let key = msg.params.session_id; id = node.context().flow.get('session-' + key); } } if (id) { session_id = id; } return session_id; } function checkAndSet(source, target, field) { if (source[field]) { target[field] = source[field]; } } function setContext(msg, params) { let context = null; if (msg.params) { checkAndSet(msg.params, params, 'context'); } return context; } function setAdditionalContext(msg, params) { if (msg.additional_context) { params.context = params.context ? params.context : {'skills' : {'main skill' : {'user_defined': {}}}}; for (var prop in msg.additional_context) { if (msg.additional_context.hasOwnProperty(prop)) { params.context.skills['main skill']['user_defined'][prop] = msg.additional_context[prop]; } } } } function setAssistantID(msg, params) { checkAndSet(config, params, 'assistant_id'); if (msg.params) { checkAndSet(msg.params, params, 'assistant_id'); } if (params && params['assistant_id']) { params.assistantId = params['assistant_id']; delete params['assistant_id']; } } function setInputOptions(msg, params) { // Setting the flags this way works as their default // values are false. ['alternate_intents', 'return_context', 'restart', 'debug'].forEach((f) => { checkAndSet(config, params.input.options, f); if (msg.params) { checkAndSet(msg.params, params.input.options, f); } }); } function setParamInputs(msg, params) { if (msg.params) { ['intents', 'entities'].forEach((f) => { checkAndSet(msg.params, params, f); }); } } function buildInputParams(msg) { let params = { 'input' : { 'message_type': 'text', 'text' : msg.payload, 'options' : {} }, 'sessionId' : setSessionID(msg) }; let context = setContext(msg, params); if (context) { params.context = context; } setAdditionalContext(msg, params); setAssistantID(msg, params); setInputOptions(msg, params); setParamInputs(msg, params); return Promise.resolve(params); } function setServiceSettings(msg, creds) { let authSettings = {}; let serviceSettings = { headers: { 'User-Agent': pkg.name + '-' + pkg.version } }; let endpoint = '', optoutLearning = false, version = SERVICE_VERSION; if (creds.apikey) { authSettings.apikey = creds.apikey; } serviceSettings.authenticator = new IamAuthenticator(authSettings); if (service) { endpoint = service.url; } if (config['service-endpoint']) { endpoint = config['service-endpoint']; } if (config['optout-learning']){ optoutLearning = true; } if (config['timeout'] && config['timeout'] !== '0' && isFinite(config['timeout'])){ serviceSettings.timeout = parseInt(config['timeout']); } // Look for message overrides if (msg.params) { if (msg.params.endpoint) { endpoint = msg.params.endpoint; } if (msg.params.version) { version = msg.params.version; } if ((msg.params['optout_learning'])){ optoutLearning = true; } if (msg.params.timeout !== '0' && isFinite(msg.params.timeout)){ serviceSettings.timeout = parseInt(msg.params.timeout); } if (msg.params.disable_ssl_verification){ serviceSettings.disable_ssl_verification = true; } if (msg.params.customerId) { serviceSettings.headers['X-Watson-Metadata'] = msg.params.customerId; } } serviceSettings.version = version; if (endpoint) { serviceSettings.url = endpoint; } if (optoutLearning) { serviceSettings.headers = serviceSettings.headers || {}; serviceSettings.headers['X-Watson-Learning-Opt-Out'] = '1'; } return Promise.resolve(serviceSettings); } function buildService(settings) { node.service = new AssistantV2(settings); return Promise.resolve(); } function checkSession(msg, params) { return new Promise(function resolver(resolve, reject){ if (params.sessionId) { resolve(); } else { node.service.createSession({assistantId: params.assistantId}) .then((response) => { if (response && response.result && response.result.session_id) { params.sessionId = response.result.session_id; } else if (response && response.session_id) { params.sessionId = response.session_id; } if (params.sessionId) { if (!config.multisession) { node.context().flow.set('session_id', params.sessionId); } else { let key = params.sessionId; if (msg.params && msg.params.session_id){ key = msg.params.session_id; } node.context().flow.set('session-' + key, params.sessionId); } resolve(); } else { reject('Unable to set session'); } }) .catch((err) => { reject(err); }); } }); } function checkForInvalidSession(err) { if (err && err.code && (404 === err.code) && err.message && (INVALID_SESSION === err.message)) { return true; } return false; } function messageTurn(params, repeatedTurn) { return new Promise(function resolver(resolve, reject) { node.service.message(params) .then((response) => { if (response.result) { resolve(response.result); } else { resolve(response); } }) .catch((err) => { if ((!repeatedTurn) && checkForInvalidSession(err)) { resolve({INVALID_SESSION : true}); } else { reject(err); } }); }); } function invalidSessionTurnCheck(body, msg, params) { return new Promise(function resolver(resolve, reject) { if (body && body.INVALID_SESSION) { // Message Turn has returned an invalid session // This time round force a new session, but ensure // that the messageTurn sends a consequental invalid session // as an error. node.status({ fill: 'blue', shape: 'dot', text: 'Resetting Session ...'}); params.sessionId = null; checkSession(msg, params) .then (function(){ return messageTurn(params, true); }) .then ((body) => { return resolve(body); }) .catch ((err) => { reject(err); }) } else { resolve(body); } }); } function setInitialSessionId(msg) { let persistSessionId = false; if (config['persist-session-id']){ persistSessionId = true; } if ( msg.hasOwnProperty('params') && msg.params['persist_session_id'] ){ persistSessionId = true; } return new Promise(function resolver(resolve) { if (persistSessionId) { if (msg.params && msg.params.session_id) { if (msg.payload) { msg.payload.session_id = msg.params.session_id; if (msg.payload.context && msg.payload.context.global) { msg.payload.context.global.session_id = msg.params.session_id; } } } } resolve(msg); }); } this.on('input', function(msg, send, done) { var creds = setCredentials(msg), params = {}; node.status({}); credentialCheck(creds.apikey) .then(function(){ return payloadCheck(msg); }) .then(function(){ return idCheck(msg); }) .then(function(){ return buildInputParams(msg); }) .then(function(p){ params = p; return setServiceSettings(msg, creds); }) .then(function(settings){ return buildService(settings); }) .then(function(){ return checkSession(msg, params); }) .then(function(){ node.status({ fill: 'blue', shape: 'dot', text: 'Calling Assistant service ...'}); return messageTurn(params, false); }) .then(function(body){ return invalidSessionTurnCheck(body, msg, params); }) .then(function(body){ body.session_id = params.sessionId; msg.payload = body; return Promise.resolve(); }) .then(function() { return setInitialSessionId(msg) }) .then(function(){ node.status({}); send(msg); done(); }) .catch(function(err){ let errMsg = payloadutils.reportError(node, msg, err); done(errMsg); }); }); } RED.nodes.registerType('watson-assistant-v2', Node, { credentials: { apikey: {type: 'password'} } }); };