wingbot
Version:
Enterprise Messaging Bot Conversation Engine
417 lines (376 loc) • 11.2 kB
JavaScript
/*
* @author David Menger
*/
'use strict';
const assert = require('assert');
const deepExtend = require('deep-extend');
const { actionMatches, parseActionPayload } = require('../utils');
/** @typedef {import('../LLM').PromptInfo} PromptInfo */
/** @typedef {import('../LLM').LLMMessage} LLMMessage */
/**
* Format message
*
* @private
* @param {any} text
* @param {any} [actual=null]
* @param {any} [expected=null]
* @returns {string}
*/
function m (text, actual = null, expected = null) {
if (text === false) {
return '';
}
let result = '';
if (expected !== null) {
result = `. Expected: "${expected}", received: ${actual}`;
} else if (actual !== null) {
result = `. ${actual}`;
}
return `${text}${result}`;
}
function ex (message, expected, actual = null) {
let actuals;
if (Array.isArray(actual)) {
actuals = actual;
} else {
actuals = actual === null ? [] : [actual];
}
const msg = `${message}\n + expected: "${expected}"`;
if (actuals.length === 0) {
return msg;
}
return `${msg}\n - actual: ${actuals
.map((a) => `"${a}"`).join('\n ')}`;
}
function getText (response) {
if (!response || !response.message) {
return null;
}
const { message } = response;
if (message.text) {
return message.text;
}
if (message.attachment && message.attachment.payload) {
return message.attachment.payload.title
|| message.attachment.payload.text;
}
return null;
}
function getQuickReplies (response) {
return (response.message && response.message.quick_replies) || [];
}
function getAttachment (response) {
return response.message && response.message.attachment;
}
/**
* Checks attachment type
*
* @param {object} response
* @param {string} type
* @param {string|false} [message='Attachment type does not match'] - use false for no asserts
* @returns {boolean}
*/
function attachmentType (response, type, message = 'Attachment type does not match') {
const attachment = getAttachment(response);
if (message === false && !attachment) {
return false;
}
assert.ok(attachment, m(message, 'there is no attachment'));
const matches = attachment.type === type;
if (message === false) {
return matches;
}
assert.ok(matches, m(message, attachment.type, type));
return true;
}
/**
* Checks, that response is a text
*
* @param {object} response
* @param {string|false} [message='Should be a text'] - use false for no asserts
* @returns {boolean}
*/
function isText (response, message = 'Should be a text') {
const is = typeof getText(response) === 'string' && !response.message.quick_reply;
if (message === false) {
return is;
}
assert(is, message);
return true;
}
function searchMatchesText (search, text) {
let match = false;
if (search instanceof RegExp) {
match = text.match(search);
} else {
match = `${text}`.toLocaleLowerCase().indexOf(search.toLocaleLowerCase()) !== -1;
}
return match;
}
/**
* Checks, that text contain a message
*
* @param {object} response
* @param {string|object} search
* @param {string|false} [message='Should contain a text'] - use false for no asserts
* @returns {boolean}
*/
function contains (response, search, message = 'Should contain a text') {
if (typeof search === 'object') {
try {
assert.deepEqual(
response,
deepExtend({}, response, search),
'Event contains'
);
return true;
} catch (e) {
if (message === false) {
return false;
}
throw e;
}
}
const text = getText(response);
const typeIsText = typeof text === 'string';
if (message === false && !typeIsText) {
return false;
}
assert.ok(typeIsText, m(message, search, 'not a message'));
const match = searchMatchesText(search, text);
if (message === false) {
return match;
}
assert.ok(match, m(message, text, search));
return true;
}
/**
*
* @param {LLMMessage} llmMsg
* @param {string} search
* @param {string|false} message
*/
function llmContains (llmMsg, search, message = 'Should contain a text') {
const text = llmMsg.content;
const typeIsText = typeof text === 'string';
if (message === false && !typeIsText) {
return false;
}
assert.ok(typeIsText, m(message, search, 'not a text message'));
const match = searchMatchesText(search, text);
if (message === false) {
return match;
}
assert.ok(match, m(message, text, search));
return true;
}
/**
* Checks quick response action
*
* @param {object} response
* @param {string} action
* @param {string|false} [message='Should contain the action'] - use false for no asserts
* @returns {boolean}
*/
function quickReplyAction (response, action, message = 'Should contain the action') {
const replies = getQuickReplies(response);
const hasItems = replies.length > 0;
if (message === false && !hasItems) {
return false;
}
assert.ok(hasItems, m(message, action, 'Theres no quick response'));
const has = replies.some((reply) => {
const { action: route } = parseActionPayload(reply, true);
return actionMatches(route, action);
});
if (message === false) {
return has;
}
assert.ok(has, m(message, action));
return true;
}
/**
* Checks quick response action
*
* @param {object} response
* @param {string} search
* @param {string|false} [message='Should contain the action'] - use false for no asserts
* @returns {boolean}
*/
function quickReplyText (response, search, message = 'Should contain the text') {
const replies = getQuickReplies(response);
const hasItems = replies.length > 0;
if (message === false && !hasItems) {
return false;
}
assert.ok(hasItems, m(message, search, 'Theres no quick response'));
const has = replies.some((reply) => {
const { title = '' } = reply;
return searchMatchesText(search, title);
});
if (message === false) {
return has;
}
assert.ok(has, m(message, search));
return true;
}
/**
* Checks template type
*
* @param {object} response
* @param {string} expectedType
* @param {string|false} [message='Template type does not match'] - use false for no asserts
* @returns {boolean}
*/
function templateType (response, expectedType, message = 'Template type does not match') {
if (message === false && !attachmentType(response, 'template', message)) {
return false;
}
const attachment = getAttachment(response);
const actualType = attachment.payload && attachment.payload.template_type;
const typeMatches = actualType === expectedType;
if (message === false) {
return typeMatches;
}
assert.ok(typeMatches, m(message, actualType, expectedType));
return true;
}
/**
* Looks for waiting message
*
* @param {object} response
* @param {string|false} [message='Should be waiting placeholder'] - use false for no asserts
* @returns {boolean}
*/
function waiting (response, message = 'Should be waiting placeholder') {
const is = typeof response.wait === 'number';
if (message === false) {
return is;
}
assert.ok(is, m(message, 'Not a waiting response'));
return true;
}
/**
* Looks for pass thread control
*
* @param {object} response
* @param {string} [appId] - look for specific app id
* @param {string|false} [message='Should be waiting placeholder'] - use false for no asserts
* @returns {boolean}
*/
function passThread (response, appId = null, message = 'Should pass control') {
const is = typeof response === 'object' && response.target_app_id;
const appMatches = appId === null || is === appId;
if (message === false) {
return is && appMatches;
}
assert.ok(is && appMatches, m(message, 'Not a pass thread response'));
return true;
}
/**
*
* @param {object} response
* @param {string|false} message
* @returns {false|object[]}
*/
function buttonTemplateButtons (response, message = 'Button template should contain buttons') {
const buttons = response.message.attachment
&& response.message.attachment.payload
&& response.message.attachment.payload.buttons;
const hasItems = Array.isArray(buttons);
if (!hasItems && message === false) {
return false;
}
assert.ok(hasItems, m(message, 'Is not an array of buttons'));
return buttons;
}
/**
* Validates generic template
*
* @param {object} response
* @param {string} search - look for string
* @param {number} [count]
* @param {string|false} message
* @returns {boolean}
*/
function buttonTemplate (response, search, count = null, message = 'Should contain button template') {
const isGeneric = templateType(response, 'button', message);
if (!isGeneric) {
return false;
}
const buttons = buttonTemplateButtons(response, message);
if (!buttons) {
return false;
}
const { text } = response.message.attachment.payload;
const countMatches = count === null || buttons.length === count;
const textMatches = searchMatchesText(search, text);
if ((!countMatches || !textMatches) && message === false) {
return false;
}
assert.ok(countMatches, m(message, buttons.length, count));
assert.ok(textMatches, m(message, text, search));
return true;
}
/**
*
* @param {object} response
* @param {string|false} message
* @returns {false|object[]}
*/
function genericTemplateItems (response, message = 'Generic template should contain items') {
const elements = response.message.attachment
&& response.message.attachment.payload
&& response.message.attachment.payload.elements;
const hasItems = Array.isArray(elements);
if (!hasItems && message === false) {
return false;
}
assert.ok(hasItems, m(message, 'Is not an array of items'));
return elements;
}
/**
* Validates generic template
*
* @param {object} response
* @param {number} [count]
* @param {string|false} message
* @returns {boolean}
*/
function genericTemplate (response, count = null, message = 'Should contain generic template') {
const isGeneric = templateType(response, 'generic', message);
if (!isGeneric) {
return false;
}
if (count === null) {
return true;
}
const elements = genericTemplateItems(response, message);
if (!elements) {
return false;
}
const countMatches = elements.length === count;
if (!countMatches && message === false) {
return false;
}
assert.ok(countMatches, m(message, elements.length, count));
return true;
}
module.exports = {
contains,
isText,
quickReplyAction,
templateType,
attachmentType,
waiting,
getQuickReplies,
passThread,
genericTemplateItems,
genericTemplate,
buttonTemplate,
quickReplyText,
getText,
parseActionPayload,
ex,
llmContains
};