Skip to content

Commit

Permalink
refactor: botfuel-nlu uses Botfuel Trainer
Browse files Browse the repository at this point in the history
BREAKING CHANGE: the nlu botfuel (default) will use Botfuel Trainer. Bots must be migrated to use Botfuel Trainer. No more intents folder and intents must be created inside the Botfuel Trainer.
  • Loading branch information
tcnguyen committed May 16, 2018
1 parent d772e07 commit c9cb226
Show file tree
Hide file tree
Showing 3 changed files with 46 additions and 253 deletions.
157 changes: 44 additions & 113 deletions packages/botfuel-dialog/src/nlus/botfuel-nlu.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,41 +15,36 @@
*/

const fs = require('fs');
const fsExtra = require('fs-extra');
const rp = require('request-promise-native');
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 logger = require('logtown')('BotfuelTrainerNlu');
const BooleanExtractor = require('../extractors/boolean-extractor');
const LocationExtractor = require('../extractors/location-extractor');
const CompositeExtractor = require('../extractors/composite-extractor');
const Nlu = require('./nlu');
const SdkError = require('../errors/sdk-error');
const ClassificationResult = require('./classification-result');
const Nlu = require('./nlu');

/**
* Sample NLU module using NaturalJS.
* NLU using Botfuel Trainer API
*/
class BotfuelNlu extends Nlu {
class BotfuelTrainerNlu extends Nlu {
/** @inheritdoc */
constructor(config) {
logger.debug('constructor', config);
super(config);
this.extractor = null;
this.qna = null;
this.classifier = null;

if (config.nlu && config.nlu.intentThreshold === undefined) {
throw new ConfigurationError('Missing intentThreshold in nlu configuration');
if (!process.env.BOTFUEL_APP_TOKEN) {
throw new SdkError('BOTFUEL_APP_TOKEN is required for using the nlu service');
}
this.intentFilter = async intents =>
intents
.filter(intent => intent.value > config.nlu.intentThreshold)
.map(intent => intent.name);
const intentFilterPath = `${this.config.path}/src/intent-filter.js`;
if (fsExtra.pathExistsSync(intentFilterPath)) {
this.intentFilter = require(intentFilterPath);

if (!process.env.BOTFUEL_APP_ID) {
throw new SdkError('BOTFUEL_APP_ID is required for using the nlu service');
}

if (!process.env.BOTFUEL_APP_KEY) {
throw new SdkError('BOTFUEL_APP_KEY is required for using the nlu service');
}
}

Expand Down Expand Up @@ -92,116 +87,52 @@ class BotfuelNlu extends Nlu {
this.extractor = new CompositeExtractor({
extractors: this.getExtractors(`${this.config.path}/src/extractors`),
});
// Classifier
this.classifier = new Classifier(this.config);
await this.classifier.init();
// QnA
if (this.config.nlu.qna) {
if (!process.env.BOTFUEL_APP_ID || !process.env.BOTFUEL_APP_KEY) {
logger.error('BOTFUEL_APP_ID and BOTFUEL_APP_KEY are required for using the QnA service!');
}
this.qna = new Qna({
appId: process.env.BOTFUEL_APP_ID,
appKey: process.env.BOTFUEL_APP_KEY,
});
}
}

/** @inheritdoc */
async compute(sentence, context) {
async compute(sentence /* context */) {
logger.debug('compute', sentence);

if (this.config.nlu.qna) {
logger.debug('compute: qna', this.config.nlu.qna);
// compute entities
const messageEntities = await this.computeEntities(sentence);

if (this.config.nlu.qna.when === 'before') {
const qnaResults = await this.computeWithQna(sentence);
if (qnaResults.length > 0) {
return { classificationResults: qnaResults };
}
return this.computeWithClassifier(sentence, context);
}
// compute intents
let trainerUrl = process.env.BOTFUEL_TRAINER_API_URL || 'https://api.botfuel.io/trainer/api/v0';

// qna is after
const { classificationResults, messageEntities } = await this.computeWithClassifier(
sentence,
context,
);
if (trainerUrl.slice(-1) !== '/') {
trainerUrl += '/';
}

if (classificationResults.length > 0) {
return { classificationResults, messageEntities };
}
const options = {
uri: `${trainerUrl}classify`,
qs: {
sentence,
},
headers: {
'Botfuel-Bot-Id': process.env.BOTFUEL_APP_TOKEN,
'App-Id': process.env.BOTFUEL_APP_ID,
'App-Key': process.env.BOTFUEL_APP_KEY,
},
json: true,
};

const qnaResults = await this.computeWithQna(sentence);
if (qnaResults.length > 0) {
return { classificationResults: qnaResults };
}
}
const res = await rp(options);

return this.computeWithClassifier(sentence, context);
}
const classificationResults = res.map(data => new ClassificationResult(data));

/**
* Computes QnA.
* @param {String} sentence - the user sentence
* @returns {Promise.<Object>}
*/
async computeWithQna(sentence) {
logger.debug('computeWithQna', sentence);
try {
const qnas = await this.qna.getMatchingQnas({ sentence });
logger.debug('computeWithQna: qnas', qnas);

const { nlu } = this.config;
const { qna } = nlu;
const { strict } = qna;
if ((strict && qnas.length === 1) || (!strict && qnas.length > 0)) {
const qnaResults = [
new ClassificationResult({
name: 'qnas',
type: ClassificationResult.TYPE_QNA,
answers: [[{ value: qnas[0].answer }]],
}),
];

return qnaResults;
}

return [];
} catch (error) {
logger.error('Could not classify with QnA!');
if (error.statusCode === 403) {
throw new AuthenticationError();
}
throw error;
}
return { messageEntities, classificationResults };
}

/**
* Computes intents and entities using the classifier.
* Computes entities using the classifier.
* @param {String} sentence - the user sentence
* @param {Object} [context] - an optional context (brain and userMessage)
* @returns {Promise.<Object>}
* @returns {Object} entities
*/
async computeWithClassifier(sentence, context) {
logger.debug('computeWithClassifier', sentence);
async computeEntities(sentence) {
logger.debug('computeEntities', sentence);
const entities = await this.extractor.compute(sentence);
logger.debug('computeWithClassifier: entities', entities);
let intents = await this.classifier.compute(sentence, entities);
logger.debug('computeWithClassifier: non filtered intents', intents);
intents = await this.intentFilter(intents, context);
intents = intents.slice(0, this.config.multiIntent ? 2 : 1);
logger.debug('computeWithClassifier: filtered intents', intents);

const classificationResults = intents.map(
i => new ClassificationResult({ name: i, type: ClassificationResult.TYPE_INTENT }),
);
logger.debug('computeWithClassifier: final classficationResults', { classificationResults });
return {
classificationResults: classificationResults || [],
messageEntities: entities,
};
return entities;
}
}

module.exports = BotfuelNlu;
module.exports = BotfuelTrainerNlu;
138 changes: 0 additions & 138 deletions packages/botfuel-dialog/src/nlus/botfuel-trainer-nlu.js

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@
const sinon = require('sinon');
const CompositeExtractor = require('../../src/extractors/composite-extractor');
const ClassificationResult = require('../../src/nlus/classification-result');
const BotfuelTrainerNlu = require('../../src/nlus/botfuel-trainer-nlu');
const BotfuelTrainerNlu = require('../../src/nlus/botfuel-nlu');
const SdkError = require('../../src/errors/sdk-error');

describe('Botfuel Trainer Nlu', () => {
describe('Botfuel Nlu', () => {
describe('check credentials', () => {
const sandbox = sinon.sandbox.create();

Expand Down

0 comments on commit c9cb226

Please sign in to comment.