node-nlp
Version:
Library for NLU (Natural Language Understanding) done in Node.js
574 lines (566 loc) • 18 kB
JavaScript
/*
* Copyright (c) AXA Shared Services Spain S.A.
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
const { Recognizer } = require('../../lib');
async function fill(recognizer) {
recognizer.nlpManager.addLanguage('en');
const fromEntity = recognizer.nlpManager.addTrimEntity('fromCity');
fromEntity.addBetweenCondition('en', 'from', 'to', { skip: ['travel'] });
fromEntity.addAfterLastCondition('en', 'from', { skip: ['travel'] });
const toEntity = recognizer.nlpManager.addTrimEntity('toCity');
toEntity.addBetweenCondition('en', 'to', 'from', { skip: ['travel'] });
toEntity.addAfterLastCondition('en', 'to', { skip: ['travel'] });
recognizer.nlpManager.slotManager.addSlot('travel', 'toCity', true, {
en: 'Where do you want to go?',
});
recognizer.nlpManager.slotManager.addSlot('travel', 'fromCity', true, {
en: 'From where you are traveling?',
});
recognizer.nlpManager.slotManager.addSlot('travel', 'date', true, {
en: 'When do you want to travel?',
});
recognizer.nlpManager.addDocument(
'en',
'I want to travel from %fromCity% to %toCity% %date%',
'travel'
);
recognizer.nlpManager.addAnswer(
'en',
'travel',
'You want to travel {{ date }} from {{ fromCity }} to {{ toCity }}'
);
await recognizer.nlpManager.train();
}
function mockBot() {
return {
libraries: {
BotBuilder: {
constructor: {
bestRouteResult(result, dialogStack, name) {
return name;
},
},
},
},
recognizer(value) {
this.recognizerValue = value;
},
_onDisambiguateRoute() {
this.defaultDisambiguate = true;
},
};
}
describe('Recognizer', () => {
describe('Constructor', () => {
test('It should create an instance', () => {
const recognizer = new Recognizer();
expect(recognizer).toBeDefined();
});
});
describe('The model can be loaded from an excel file', () => {
test('It should load the excel and train', () => {
const recognizer = new Recognizer();
recognizer.loadExcel('./test/nlp/rules.xls');
expect(recognizer.nlpManager.languages).toEqual(['en', 'es']);
});
});
describe('The model can be loaded from a model.nlp file', () => {
test('It should load the model', () => {
const recognizer = new Recognizer();
recognizer.load('./test/recognizer/model.nlp');
expect(recognizer.nlpManager.languages).toEqual(['en']);
});
});
describe('Process', () => {
test('It should process an utterance', async () => {
const recognizer = new Recognizer();
recognizer.load('./test/recognizer/model.nlp');
const process = await recognizer.process({}, 'en', 'What is your age?');
expect(process.intent).toEqual('agent.age');
expect(process.language).toEqual('English');
expect(process.score).toBeGreaterThan(0.7);
});
test('It should autodetect the language if not provided', async () => {
const recognizer = new Recognizer();
recognizer.load('./test/recognizer/model.nlp');
const process = await recognizer.process(
{},
undefined,
'What is your age?'
);
expect(process.intent).toEqual('agent.age');
expect(process.language).toEqual('English');
expect(process.score).toBeGreaterThan(0.7);
});
test('It should create a new temporal context if not provided', async () => {
const recognizer = new Recognizer();
recognizer.load('./test/recognizer/model.nlp');
const process = await recognizer.process(
undefined,
undefined,
'What is your age?'
);
expect(process.intent).toEqual('agent.age');
expect(process.language).toEqual('English');
expect(process.score).toBeGreaterThan(0.7);
});
test('If the intent is None then the answer should not be calculated', async () => {
const recognizer = new Recognizer();
recognizer.load('./test/recognizer/model.nlp');
const process = await recognizer.process(
undefined,
undefined,
'yupi caramelo?'
);
expect(process.intent).toEqual('None');
expect(process.answer).toBeUndefined();
});
});
describe('Recognize Utterance', () => {
test('It should process providing a locale in the model', done => {
const recognizer = new Recognizer();
recognizer.load('./test/recognizer/model.nlp');
recognizer.recognizeUtterance(
'What is your age?',
{ locale: 'en' },
(error, result) => {
expect(result.intent).toEqual('agent.age');
expect(result.language).toEqual('English');
expect(result.score).toBeGreaterThan(0.7);
done();
}
);
});
test('It should process without providing a model', done => {
const recognizer = new Recognizer();
recognizer.load('./test/recognizer/model.nlp');
recognizer.recognizeUtterance(
'What is your age?',
undefined,
(error, result) => {
expect(result.intent).toEqual('agent.age');
expect(result.language).toEqual('English');
expect(result.score).toBeGreaterThan(0.7);
done();
}
);
});
});
describe('Recognize', () => {
test('It should recognize the intent from the session', done => {
const recognizer = new Recognizer();
recognizer.load('./test/recognizer/model.nlp');
const session = {
locale: 'en',
message: {
text: 'What is your age?',
},
};
recognizer.recognize(session, (err, result) => {
expect(result.intent).toEqual('agent.age');
expect(result.language).toEqual('English');
expect(result.score).toBeGreaterThan(0.7);
done();
});
});
test('It should use context if conversation id is provided', done => {
const recognizer = new Recognizer();
recognizer.loadExcel('./test/nlp/rules.xls').then(() => {
const session1 = {
locale: 'en',
message: {
address: {
conversation: {
id: 'a1b2c3',
},
},
text: 'Who is spiderman?',
},
};
const session2 = {
locale: 'en',
message: {
address: {
conversation: {
id: 'a1b2c3',
},
},
text: 'Where he lives?',
},
};
recognizer.recognize(session1, () => {
recognizer.recognize(session2, (err, result) => {
expect(result.answer).toEqual('Hanging on a web');
done();
});
});
});
});
test('If the utterance cannot be retrieved from the session the intent should be undefined and score 0', done => {
const recognizer = new Recognizer();
recognizer.load('./test/recognizer/model.nlp');
const session = {
locale: 'en',
message: {},
};
recognizer.recognize(session, (err, result) => {
expect(result.intent).toBeUndefined();
expect(result.score).toEqual(0.0);
done();
});
});
});
describe('Slot filling', () => {
test('If all slots are filled return the correct answer', async () => {
const recognizer = new Recognizer();
await fill(recognizer);
const session = {
locale: 'en',
message: {
address: {
conversation: {
id: 'a1b2c3',
},
},
text: 'I want to travel from Barcelona to London tomorrow',
},
};
return new Promise(done =>
recognizer.recognize(session, (err, result) => {
expect(result.answer).toEqual(
'You want to travel tomorrow from Barcelona to London'
);
done();
})
);
});
test('It can chain several slots', async () => {
const recognizer = new Recognizer();
await fill(recognizer);
const session = {
locale: 'en',
message: {
address: {
conversation: {
id: 'a1b2c3',
},
},
text: 'I want to travel to London',
},
};
const session2 = {
locale: 'en',
message: {
address: {
conversation: {
id: 'a1b2c3',
},
},
text: 'Barcelona',
},
};
const session3 = {
locale: 'en',
message: {
address: {
conversation: {
id: 'a1b2c3',
},
},
text: 'tomorrow',
},
};
return new Promise(done =>
recognizer.recognize(session, (err, result) => {
expect(result.answer).toEqual('From where you are traveling?');
recognizer.recognize(session2, (err2, result2) => {
expect(result2.answer).toEqual('When do you want to travel?');
recognizer.recognize(session3, (err3, result3) => {
expect(result3.answer).toEqual(
'You want to travel tomorrow from Barcelona to London'
);
done();
});
});
})
);
});
});
describe('Recognize Twice', () => {
test('It should not change result if context has last recognized', async () => {
const recognizer = new Recognizer();
await fill(recognizer);
const session = {
locale: 'en',
message: {
address: {
conversation: {
id: 'a1b2c3',
},
},
text: 'I want to travel to London',
},
};
const session2 = {
locale: 'en',
message: {
address: {
conversation: {
id: 'a1b2c3',
},
},
text: 'Barcelona',
},
};
const session3 = {
locale: 'en',
message: {
address: {
conversation: {
id: 'a1b2c3',
},
},
text: 'tomorrow',
},
};
return new Promise(done =>
recognizer.recognize(session, (err, result) => {
expect(result.answer).toEqual('From where you are traveling?');
recognizer.recognizeTwice(session, () => {
recognizer.recognize(session2, (err2, result2) => {
expect(result2.answer).toEqual('When do you want to travel?');
recognizer.recognize(session3, (err3, result3) => {
expect(result3.answer).toEqual(
'You want to travel tomorrow from Barcelona to London'
);
done();
});
});
});
})
);
});
test('It should recognize again if the context has not last recognized', async () => {
const recognizer = new Recognizer();
await fill(recognizer);
const session = {
locale: 'en',
message: {
address: {
conversation: {
id: 'a1b2c3',
},
},
text: 'I want to travel to London',
},
};
const session2 = {
locale: 'en',
message: {
address: {
conversation: {
id: 'a1b2c4',
},
},
text: 'I want to travel to London tomorrow',
},
};
return new Promise(done =>
recognizer.recognize(session, (err, result) => {
expect(result.answer).toEqual('From where you are traveling?');
recognizer.recognizeTwice(session2, (err2, result2) => {
expect(result2).not.toEqual(result);
done();
});
})
);
});
});
describe('Get dialog id', () => {
test('Given a session with dialog stack, get the first developer dialog', () => {
const session = {
dialogStack: () => ['not this', 'neither this', '*:/dialog'],
};
const recognizer = new Recognizer();
const dialogId = recognizer.getDialogId(session);
expect(dialogId).toEqual('/dialog');
});
test('If no developer dialog found, return empty string', () => {
const session = { dialogStack: () => ['not this', 'neither this'] };
const recognizer = new Recognizer();
const dialogId = recognizer.getDialogId(session);
expect(dialogId).toEqual('');
});
test('If dialogStack method does not exists, return empty string', () => {
const session = {};
const recognizer = new Recognizer();
const dialogId = recognizer.getDialogId(session);
expect(dialogId).toEqual('');
});
});
describe('Default routing', () => {
test('If a route is given, it should select the route at the bot', () => {
let routeToActiveDialogCalls = 0;
let selectRouteCalls = 0;
const route = {
libraryName: 'library',
};
const library = {
selectRoute: () => {
selectRouteCalls += 1;
},
};
const bot = {
name: 'bot',
library: () => library,
libraries: {
BotBuilder: {
constructor: {
bestRouteResult: () => route,
},
},
},
};
const session = {
dialogStack: () => [],
routeToActiveDialog: () => {
routeToActiveDialogCalls += 1;
},
};
const recognizer = new Recognizer();
recognizer.defaultRouting(bot, session, []);
expect(selectRouteCalls).toEqual(1);
expect(routeToActiveDialogCalls).toEqual(0);
});
test('If no route is given, it should route to active dialog', () => {
let routeToActiveDialogCalls = 0;
let selectRouteCalls = 0;
const route = undefined;
const library = {
selectRoute: () => {
selectRouteCalls += 1;
},
};
const bot = {
name: 'bot',
library: () => library,
libraries: {
BotBuilder: {
constructor: {
bestRouteResult: () => route,
},
},
},
};
const session = {
dialogStack: () => [],
routeToActiveDialog: () => {
routeToActiveDialogCalls += 1;
},
};
const recognizer = new Recognizer();
recognizer.defaultRouting(bot, session, []);
expect(selectRouteCalls).toEqual(0);
expect(routeToActiveDialogCalls).toEqual(1);
});
});
describe('Process Answer', () => {
test('If the answer starts with / then it is a dialog', () => {
let beginDialogCalls = 0;
let sendCalls = 0;
const session = {
beginDialog: () => {
beginDialogCalls += 1;
},
send: () => {
sendCalls += 1;
},
};
const recognizer = new Recognizer();
recognizer.processAnswer(session, '/dialog');
expect(beginDialogCalls).toEqual(1);
expect(sendCalls).toEqual(0);
});
test('If the answer does not starts with / then it is a message', () => {
let beginDialogCalls = 0;
let sendCalls = 0;
const session = {
beginDialog: () => {
beginDialogCalls += 1;
},
send: () => {
sendCalls += 1;
},
};
const recognizer = new Recognizer();
recognizer.processAnswer(session, 'text');
expect(beginDialogCalls).toEqual(0);
expect(sendCalls).toEqual(1);
});
});
describe('Set Bot', () => {
test('If not activate routing default disambiguate route is still there', () => {
const bot = mockBot();
const recognizer = new Recognizer();
recognizer.setBot(bot);
/* eslint-disable no-underscore-dangle */
bot._onDisambiguateRoute();
expect(bot.defaultDisambiguate).toBeTruthy();
});
test('If activate routing disambiguate route is changed', () => {
const bot = mockBot();
const session = {
dialogStack() {
return ['/'];
},
routeToActiveDialog() {
return 'active';
},
};
const recognizer = new Recognizer();
recognizer.setBot(bot, true);
/* eslint-disable no-underscore-dangle */
bot._onDisambiguateRoute(session);
expect(bot.defaultDisambiguate).toBeFalsy();
});
test('If session contains message', () => {
const bot = mockBot();
const session = {
dialogStack() {
return ['/'];
},
routeToActiveDialog() {
return 'active';
},
message: {
text: 'I want to travel from Barcelona to London tomorrow',
},
};
const recognizer = new Recognizer();
fill(recognizer);
recognizer.setBot(bot, true);
/* eslint-disable no-underscore-dangle */
bot._onDisambiguateRoute(session);
expect(bot.defaultDisambiguate).toBeFalsy();
});
});
});