Skip to content

Commit

Permalink
feat: botfuel nlu use trainer api
Browse files Browse the repository at this point in the history
- BREAKING CHANGE: no more intents folder inside bot
- start code with mockup api calls
- intent  is object = {name: .., label:..., }
  • Loading branch information
tcnguyen committed Apr 11, 2018
1 parent 435436f commit 083f578
Show file tree
Hide file tree
Showing 17 changed files with 432 additions and 358 deletions.
4 changes: 2 additions & 2 deletions packages/botfuel-dialog/src/bot.js
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ class Bot {
name: userMessage.payload.value.dialog,
entities: userMessage.payload.value.entities,
};
await this.dm.executeDialogs(this.adapter, userMessage, [dialog]);
await this.dm.executeDialog(this.adapter, userMessage, dialog);
}

/**
Expand All @@ -214,7 +214,7 @@ class Bot {
name: 'image',
entities: [{ url: userMessage.payload.value }],
};
await this.dm.executeDialogs(this.adapter, userMessage, [dialog]);
await this.dm.executeDialog(this.adapter, userMessage, dialog);
}

/**
Expand Down
1 change: 1 addition & 0 deletions packages/botfuel-dialog/src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const defaultConfig = {
intentThreshold: 0.8,
},
multiIntent: false,
trainerApiUri: 'https://trainer-api-staging.herokuapp.com/api/v0',
};

const whitelist = Object.keys(defaultConfig).concat(['spellchecking']);
Expand Down
111 changes: 52 additions & 59 deletions packages/botfuel-dialog/src/dialog-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,31 +47,14 @@ class DialogManager extends Resolver {
}

/**
* Sorts intents
* @param {Object[]} intents - the intent names
* @returns {Object[]} the sorted intents
*/
sortIntents(intents) {
logger.debug('sortIntents', intents);
return intents.sort((intent1, intent2) => {
const reentrant1 = this.resolve(intent1).characteristics.reentrant;
const reentrant2 = this.resolve(intent2).characteristics.reentrant;
if (reentrant1 && !reentrant2) {
return 1;
}
if (!reentrant1 && reentrant2) {
return -1;
}
return 0;
});
}

/**
* Returns the last dialog to execute if no other dialog is found.
* Returns the last "reentrant" dialog to execute if no other dialog is found.
* When the sentence itself does not contain enough information for the DialogManager
* to compute a dialog, the DialogManager recalls the first reentrant dialog from the
* stack of previous dialogs
* @param {Object[]} previousDialogs - the previous dialogs
* @returns {String} a dialog name
*/
getLastDialog(previousDialogs) {
getLastReentrantDialog(previousDialogs) {
for (let i = previousDialogs.length - 1; i >= 0; i--) {
const dialog = previousDialogs[i];
const dialogInstance = this.resolve(dialog.name);
Expand Down Expand Up @@ -113,26 +96,42 @@ class DialogManager extends Resolver {
*/
updateWithIntents(userId, dialogs, intents, entities) {
logger.debug('updateWithIntents', userId, dialogs, intents, entities);
intents = this.sortIntents(intents);
logger.debug('updateWithIntents: intents', intents);
let nb = 0;
const newDialogs = [];
for (let i = 0; i < intents.length; i++) {
const name = intents[i];
const characteristics = this.resolve(name).characteristics;
if (characteristics.reentrant) {
nb++;

let newDialog = null;
if (intents.length > 1) {
newDialog = {
name: 'intent-resolution',
entities: {
entities,
intents,
},
};
} else if (intents.length === 1) {
if (intents[0].isQnA()) {
newDialog = {
name: intents[0].name,
entities: intents[0].answers,
};
} else {
newDialog = {
name: intents[0].name,
entities,
};
}
}

if (newDialog) {
this.updateWithDialog(dialogs, newDialog);
} else {
const lastDialog = dialogs.stack.length > 0 ? dialogs.stack[dialogs.stack.length - 1] : null;
if (lastDialog) {
lastDialog.entities = entities;
}
newDialogs.push({
name,
entities,
blocked: nb > 1,
});
}
this.updateWithDialogs(dialogs, newDialogs);

if (dialogs.stack.length === 0) {
// no intent detected
const lastDialog = this.getLastDialog(dialogs.previous) || {
const lastDialog = this.getLastReentrantDialog(dialogs.previous) || {
name: 'default',
characteristics: {
reentrant: false,
Expand All @@ -143,28 +142,22 @@ class DialogManager extends Resolver {
entities: entities || [],
});
}
if (entities) {
dialogs.stack[dialogs.stack.length - 1].entities = entities;
}
}

/**
* Updates the dialogs.
* @param {Object} dialogs - the dialogs data
* @param {Object[]} newDialogs - new dialogs to be added to the dialog stack
* @param {Object} newDialog - new dialog to be added to the dialog stack
* @returns {void}
*/
updateWithDialogs(dialogs, newDialogs) {
for (let i = newDialogs.length - 1; i >= 0; i--) {
const newDialog = newDialogs[i];
const lastIndex = dialogs.stack.length - 1;
const lastDialog = lastIndex >= 0 ? dialogs.stack[lastIndex] : null;
if (lastDialog && lastDialog.name === newDialog.name) {
lastDialog.entities = newDialog.entities;
} else {
dialogs.stack.push(newDialog);
}
updateWithDialog(dialogs, newDialog) {
const lastDialog = dialogs.stack.length > 0 ? dialogs.stack[dialogs.stack.length - 1] : null;
if (lastDialog && lastDialog.name === newDialog.name) {
lastDialog.entities = newDialog.entities;
} else {
dialogs.stack.push(newDialog);
}
logger.debug('updateWithDialog: updated', dialogs);
}

/**
Expand All @@ -188,7 +181,7 @@ class DialogManager extends Resolver {
previous: [...dialogs.previous, { ...currentDialog, date }],
};
if (newDialog) {
this.updateWithDialogs(dialogs, [newDialog]);
this.updateWithDialog(dialogs, newDialog);
}
return dialogs;

Expand All @@ -205,7 +198,7 @@ class DialogManager extends Resolver {
stack: dialogs.stack.slice(0, -1),
previous: [...dialogs.previous, { ...currentDialog, date }],
};
this.updateWithDialogs(dialogs, [newDialog]);
this.updateWithDialog(dialogs, newDialog);
return dialogs;

case Dialog.ACTION_NEW_CONVERSATION:
Expand Down Expand Up @@ -275,7 +268,7 @@ class DialogManager extends Resolver {
* @returns {void}
*/
async executeIntents(adapter, userMessage, intents, entities) {
logger.debug('execute', userMessage, intents, entities);
logger.debug('executeIntents', userMessage, intents, entities);
const userId = userMessage.user;
const dialogs = await this.getDialogs(userId);
this.updateWithIntents(userId, dialogs, intents, entities);
Expand All @@ -286,14 +279,14 @@ class DialogManager extends Resolver {
* Populates and executes the stack.
* @param {Adapter} adapter - the adapter
* @param {Object} userMessage - the user message
* @param {Object[]} newDialogs - the new dialogs
* @param {Object[]} newDialog - the new dialogs
* @returns {void}
*/
async executeDialogs(adapter, userMessage, newDialogs) {
logger.debug('executeWithDialogs', userMessage, newDialogs);
async executeDialog(adapter, userMessage, newDialog) {
logger.debug('executeDialog', userMessage, newDialog);
const userId = userMessage.user;
const dialogs = await this.getDialogs(userId);
this.updateWithDialogs(dialogs, newDialogs);
this.updateWithDialog(dialogs, newDialog);
await this.setDialogs(userId, await this.execute(adapter, userMessage, dialogs));
}
}
Expand Down
44 changes: 44 additions & 0 deletions packages/botfuel-dialog/src/dialogs/intent-resolution-dialog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* Copyright (c) 2017 - present, Botfuel (https://www.botfuel.io).
*
* 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.
*/
const logger = require('logtown')('IntentResolutionDialog');
const Dialog = require('./dialog');

/**
* The default resolution dialog when
* @extends IntentResolutionDialog
*/
class IntentResolutionDialog extends Dialog {
/**
* @constructor
* @param {Object} config - the bot config
* @param {class} brain - the bot brain
*/
constructor(config, brain) {
super(config, brain, { reentrant: false });
}

/** @inheritDoc */
async execute(adapter, userMessage, messageEntities) {
logger.debug('execute', userMessage, messageEntities);
const { entities, intents } = messageEntities;
const dialogData = { entities, intents };
await this.display(adapter, userMessage, dialogData);

return this.complete();
}
}

module.exports = IntentResolutionDialog;
12 changes: 5 additions & 7 deletions packages/botfuel-dialog/src/dialogs/qnas-dialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,12 @@ class QnasDialog extends Dialog {
async execute(adapter, userMessage, messageEntities) {
logger.debug('execute', userMessage, messageEntities);
const extraData = await this.dialogWillDisplay(userMessage, messageEntities);
const qnas = messageEntities[0].value;
const dialogData = { messageEntities, qnas, extraData };
const answers = messageEntities;
const dialogData = { messageEntities, answers, extraData };
await this.display(adapter, userMessage, dialogData);
if (qnas.length === 1) {
const action = await this.dialogWillComplete(userMessage, dialogData);
return action || this.complete();
}
return this.wait();

const action = await this.dialogWillComplete(userMessage, dialogData);
return action || this.complete();
}
}

Expand Down
2 changes: 2 additions & 0 deletions packages/botfuel-dialog/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const BotTextMessage = require('./messages/bot-text-message');
const BotTableMessage = require('./messages/bot-table-message');
const BotfuelAdapter = require('./adapters/botfuel-adapter');
const BotfuelNlu = require('./nlus/botfuel-nlu');
const BotfuelTrainerNlu = require('./nlus/botfuel-trainer-nlu');
const Brain = require('./brains/brain');
const Card = require('./messages/card');
const CardsMessage = require('./messages/cards-message');
Expand Down Expand Up @@ -67,6 +68,7 @@ module.exports = {
BotTextMessage,
BotfuelAdapter,
BotfuelNlu,
BotfuelTrainerNlu,
Brain,
Card,
CardsMessage,
Expand Down
45 changes: 27 additions & 18 deletions packages/botfuel-dialog/src/nlus/botfuel-nlu.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ const dir = require('node-dir');
const Qna = require('botfuel-qna-sdk');
const logger = require('logtown')('BotfuelNlu');
const AuthenticationError = require('../errors/authentication-error');
const ConfigurationError = require('../errors/configuration-error');
const Classifier = require('../classifier');
const BooleanExtractor = require('../extractors/boolean-extractor');
const LocationExtractor = require('../extractors/location-extractor');
const CompositeExtractor = require('../extractors/composite-extractor');
const Nlu = require('./nlu');
const Intent = require('./intent');

/**
* Sample NLU module using NaturalJS.
Expand All @@ -37,6 +39,10 @@ class BotfuelNlu extends Nlu {
this.extractor = null;
this.qna = null;
this.classifier = null;

if (config.nlu && config.nlu.intentThreshold === undefined) {
throw new ConfigurationError('Missing intentThreshold in nlu configuration');
}
this.intentFilter = async intents =>
intents
.filter(intent => intent.value > config.nlu.intentThreshold)
Expand Down Expand Up @@ -107,19 +113,19 @@ class BotfuelNlu extends Nlu {
if (this.config.nlu.qna) {
logger.debug('compute: qna', this.config.nlu.qna);
if (this.config.nlu.qna.when === 'before') {
const qnaResult = await this.computeWithQna(sentence);
if (qnaResult.intents.length > 0) {
return qnaResult;
const intents = await this.computeWithQna(sentence);
if (intents.length > 0) {
return { intents };
}
return this.computeWithClassifier(sentence, context);
}
const classifierResult = await this.computeWithClassifier(sentence, context);
if (classifierResult.intents.length > 0) {
return classifierResult;
}
const qnaResult = await this.computeWithQna(sentence);
if (qnaResult.intents.length > 0) {
return qnaResult;
const intents = await this.computeWithQna(sentence);
if (intents.length > 0) {
return { intents };
}
return {
intents: [],
Expand All @@ -139,21 +145,21 @@ class BotfuelNlu extends Nlu {
try {
const qnas = await this.qna.getMatchingQnas({ sentence });
logger.debug('computeWithQna: qnas', qnas);

const strict = this.config.nlu.qna.strict;
if ((strict && qnas.length === 1) || (!strict && qnas.length > 0)) {
return {
intents: ['qnas'],
entities: [
{
dim: 'qnas',
value: qnas,
},
],
};
const intents = [
new Intent({
name: 'qnas',
type: 'QnA',
answers: [[{ value: qnas[0].answer }]],
}),
];

return intents;
}
return {
intents: [],
};

return [];
} catch (error) {
logger.error('Could not classify with QnA!');
if (error.statusCode === 403) {
Expand All @@ -178,6 +184,9 @@ class BotfuelNlu extends Nlu {
intents = await this.intentFilter(intents, context);
intents = intents.slice(0, this.config.multiIntent ? 2 : 1);
logger.debug('computeWithClassifier: filtered intents', intents);

intents = intents.map(i => new Intent({ name: i, type: 'Intent' }));
logger.debug('computeWithClassifier: final intents', { intents });
return {
intents,
entities,
Expand Down
Loading

0 comments on commit 083f578

Please sign in to comment.