talkify
Version:
Framework for developing chat bot applications.
390 lines (311 loc) • 14 kB
JavaScript
/**
* Created by manthanhd on 17/10/2016.
*/
function DefaultSkillResolutionStrategyFn() {
const util = require('util');
const extend = util._extend;
const skillsMap = {};
const undefinedSkills = [];
this.addSkill = function(skill, options) {
var minConfidence = 0;
if(options instanceof Number) {
minConfidence = options;
} else if (options && options.minConfidence !== undefined) {
minConfidence = options.minConfidence;
}
var skillClone = extend({}, skill);
var botSkillObject = {skill: skillClone, minConfidence: minConfidence};
if (skillClone.topic === undefined || skillClone.topic === 'undefined') {
skillClone.topic = undefined;
undefinedSkills.push(botSkillObject);
}
if(skillClone.topic instanceof Array) {
skillClone.topic.forEach(function(topic) {
if(skillsMap[topic] === undefined) skillsMap[topic] = [];
skillsMap[topic].push(skillClone);
});
} else {
if (skillsMap[skillClone.topic] === undefined) {
skillsMap[skillClone.topic] = [];
}
skillsMap[skillClone.topic].push(botSkillObject);
}
};
this.getSkills = function() {
var topics = Object.keys(skillsMap);
var skills = [];
topics.forEach(function (topic) {
skills.push(skillsMap[topic]);
});
return skills;
};
var _resolveSkillFromTopicWithConfidence = function ResolveSkillFromTopicWithConfidenceFn(topic) {
if (topic.name === undefined) {
if (undefinedSkills && undefinedSkills.length > 0) {
return undefinedSkills[0];
}
return;
}
var botSkills = skillsMap[topic.name];
var minConfidenceSkill = undefinedSkills[0];
var minDistance = 999;
if (botSkills !== undefined && botSkills.length > 0) {
for (var i = 0; i < botSkills.length; i++) {
var botSkill = botSkills[i];
var distance = topic.confidence - botSkill.minConfidence;
if (distance < 0) continue;
if (distance < minDistance) {
minDistance = distance;
minConfidenceSkill = botSkill;
}
if (distance === 0) break;
}
}
return minConfidenceSkill;
};
this.resolve = function(err, resolutionContext, callback) {
var topics = resolutionContext.topics;
var sentence = resolutionContext.sentence;
var mostConfidentTopic = topics[0];
var resolvedBotSkill = _resolveSkillFromTopicWithConfidence(mostConfidentTopic);
var skill = resolvedBotSkill ? resolvedBotSkill.skill : undefined;
if (skill === undefined) {
return callback(new Error("could not resolve any skills"));
}
var clonedSkill = extend({}, skill);
clonedSkill.sentence = sentence;
clonedSkill.topic = mostConfidentTopic;
return callback(undefined, clonedSkill);
};
return this;
}
function DefaultTopicResolutionStrategyFn() {
const classifications = [];
this.collect = function(classification, classificationContext, callback) {
classifications.push(classification);
return callback();
};
this.resolve = function(callback) {
var topics = [];
classifications.forEach(function(classification) {
topics.push({name: classification.label, confidence: classification.value});
});
return callback(undefined, topics);
};
return this;
}
function Bot(config) {
config = config || {};
const async = require('async');
const util = require('util');
const extend = util._extend;
const LogisticRegressionClassifier = require('talkify-natural-classifier').LogisticRegressionClassifier;
var classifiers = config.classifier || config.classifiers;
if (!classifiers) classifiers = [new LogisticRegressionClassifier()];
if (!(classifiers instanceof Array)) classifiers = [classifiers];
/**
* Defines how a skill should be resolved from a given set of topics
* @type {*}
*/
const skillResolutionStrategy = config.skillResolutionStrategy || DefaultSkillResolutionStrategyFn();
const topicResolutionStrategy = config.topicResolutionStrategy || DefaultTopicResolutionStrategyFn;
const BotTypes = require('./BotTypes');
const Skill = BotTypes.Skill;
const SingleLineMessage = BotTypes.SingleLineMessage;
const Correspondence = BotTypes.Correspondence;
const Context = BotTypes.Context;
const BuiltInContextStore = require('./ContextStore');
const contextStore = config.contextStore || new BuiltInContextStore();
var botObject = this;
this.train = function TrainBotFn(topic, text, callback) {
var classifier = botObject.getClassifier();
if (!topic || (typeof topic !== 'string' && !(topic instanceof String))) throw new TypeError('topic must be a String and cannot be undefined');
if (!text || (!(text instanceof Array) && typeof text !== 'string' && !(text instanceof String))) throw new TypeError('text must be a String and cannot be undefined');
classifier.trainDocument({topic: topic, text: text}, function (err) {
if (err) return callback(err);
return classifier.initialize(callback);
});
return botObject;
};
this.trainAll = function TrainAllBotFn(documents, finished) {
finished = (finished) ? finished : function (err) {
throw err;
};
var classifier = botObject.getClassifier();
if (!documents || !(documents instanceof Array)) return finished(new TypeError('expected documents to exist and be of type Array'));
classifier.trainDocument(documents, function (err) {
if (err) return callback(err);
return classifier.initialize(finished);
});
return botObject;
};
this.addSkill = function AddSkillFn(skill, options) {
if (!skill || !(skill instanceof Skill)) throw new TypeError('skill parameter is of invalid type. Must be defined and be of type Skill');
// Allows us to keep backwards compatibility with people who still use 1 with skill as confidence option
var evaluatedOptions = options instanceof Object ? options : {minConfidence: options};
skillResolutionStrategy.addSkill(skill, evaluatedOptions);
return botObject;
};
this.getSkills = function GetSkillsFn() {
return skillResolutionStrategy.getSkills();
};
var _resolveContext = function _ResolveContext(id, callback) {
var retrieved = function (err, contextRecord) {
if (err) return callback(err);
if (contextRecord === undefined) return callback(err, new Context(id));
return callback(err, contextRecord.context);
};
return contextStore.get(id, retrieved);
};
var _classifyToTopics = function ClassifyToTopicFn(sentence, done) {
return async.map(classifiers, function (classifier, classificationDone) {
return process.nextTick(function() {
var classified = function (err, classifications) {
var topicResolver = new topicResolutionStrategy();
async.each(classifications, function(classification, callback) {
var context = {};
topicResolver.collect(classification, context, callback);
}, function(err) {
topicResolver.resolve(classificationDone);
});
};
return classifier.getClassifications(sentence, classified);
});
}, function (err, classificationArrays) {
if (err) return done(err, classificationArrays);
var classifications = [];
classificationArrays.forEach(function(classificationArray) {
classificationArray.forEach(function(classification) {
classifications.push(classification);
});
});
var indeterminates = [];
return async.filter(classifications, function (classification, mapped) {
if (classification.name === undefined) {
indeterminates.push(classification);
return mapped(undefined, false);
}
return mapped(undefined, true);
}, function (err, allMapped) {
return async.sortBy(allMapped, function (classification, sorted) {
return sorted(undefined, classification.confidence * -1);
}, function (err, sortedClassifications) {
var allClassifications = sortedClassifications.concat(indeterminates);
return done(err, allClassifications);
});
});
});
};
var _getSkillByName = function (name, skillList) {
var skills = skillList || botObject.getSkills();
for (var i = 0; i < skills.length; i++) {
var skill = skills[i];
if (skill instanceof Array) {
var returnSkill = _getSkillByName(name, skill);
if (returnSkill) return returnSkill;
continue;
}
if (skill.name === name) return skill;
if (skill.skill.name === name) return skill.skill;
}
};
var _resolveSkills = function _ResolveSkillsFn(sentences, doneCallback) {
return async.mapSeries(sentences, function (sentence, callback) {
var classified = function (err, topics) {
return process.nextTick(function() {
return skillResolutionStrategy.resolve(err, {
sentence: sentence,
topics: topics
}, callback);
});
};
return _classifyToTopics(sentence, classified);
}, function (err, resolvedSkills) {
return doneCallback(err, resolvedSkills);
});
};
var _applySkills = function _ApplySkillsFn(context, request, response, skills, skillsApplied) {
return async.mapSeries(skills, function (skill, callback) {
return process.nextTick(function() {
var next = function () {
var responseMessage = response.message;
if (responseMessage) {
delete response.message;
}
if (context.lock) {
response.isFinal = true;
}
if (response.isFinal === true) {
return callback('break', responseMessage);
}
return callback(undefined, responseMessage);
};
response.lockConversationForNext = function LockConversationForNextFn() {
context.lock = skill.name;
return response;
};
response.final = function FinalFn() {
response.isFinal = true;
return response;
};
response.send = function SendFn(message) {
response.message = message;
return next();
};
request.sentence.current = skill.sentence;
request.sentence.index = skills.indexOf(skill);
request.skill.current = skill;
request.skill.index = request.sentence.index;
skill.apply(context.data, request, response, next);
});
}, function (err, messages) {
if (err === 'break') err = undefined;
return skillsApplied(err, messages);
});
};
this.resolve = function ResolveFn(id, content, callback) {
var sentences = content.replace(/([.?!])\s*(?=[A-Z])/g, "$1|").split("|");
var request = new Correspondence(id, new SingleLineMessage(content));
request.sentence = {all: sentences};
var response = new Correspondence(id);
var context;
var messages;
var contextMemorized = function (err, memorizedContext) {
if (err) return callback(err, messages);
return callback(err, messages);
};
var skillsApplied = function (err, responseMessages) {
messages = responseMessages;
return contextStore.put(id, context, contextMemorized);
};
var skillsResolved = function (err, skills) {
if (err) return callback(err);
request.skill = {all: skills};
return _applySkills(context, request, response, skills, skillsApplied);
};
var contextResolved = function (err, resolvedContext) {
context = resolvedContext;
if (context.lock) {
var skill = _getSkillByName(context.lock);
skill = extend({}, skill);
delete context.lock;
request.skill = {all: [skill]};
return _applySkills(context, request, response, [skill], skillsApplied);
}
return _resolveSkills(sentences, skillsResolved);
};
_resolveContext(id, contextResolved);
return botObject;
};
this.getContextStore = function GetContextStoreFn() {
return contextStore;
};
this.getClassifiers = function GetClassifiersFn() {
return classifiers;
};
this.getClassifier = function GetClassifierFn() {
return classifiers[classifiers.length - 1];
};
return this;
}
module.exports = Bot;