Botler was developed to let fynd build fyndbot. We wanted a contextual-aware chatbot to be everyone's favorite personal shopper. We found that many chatbots and pre-exisiting chatbot frameworks were fine at simple action => response behavior, but weren't great at using contextual clues to prove a more fluid experience.
Another goal of Botler was to provide as much general out-of-the-box language functionality as possible. We shouldn't keep reiventing the NLP wheel and the more pre-defined behavior everyone adds, the smarter all the bots get.
Botler uses three components to build a bot, intents, actions and the reducer.
- Intents take a text input (and potential the conversation so far) and return the intent of the user, for example "tell me weather in London" maps to
{action:'weather', topic: 'weather, details:{ location: 'London' } }
- The Reducer takes multiple detected intents and reduces it to the correct one
- Skills take intents and the state of the conversation to run an action such as querying an API and sending the results to the user
The User is a simple object the holds the current state, detected intent, and conversation (if intents require rocessing of the entire conversation). It is easily entended by adding more keys to hold application specific info (such as a unique user id to respond to).
Botler comes with a few key intents already installed. Some are
- help for example if the user types "help", "instruction", etc
- yes
- no
- hello
- many more...
$ npm install --save botler
- Weather bot (most simple example) (typescript)
- Buzzfeed news bot (more complex) (typescript)
- Facebook chatbot (javascript)
A weather chatbot in less than 100 lines!
Just make a directory named the topic of the intent. Inside we will have a few JSON files that each are named for the intent action and are an array of strings representing the different forms of the phrasing of the action.
This will add the intent topic 'weather' with the two intents 'weather' and 'rain'.
[ "what's the weather in", "weather", "tell me the forecast"]
[ "is it raining", "will it rain"]
|-- build
|-- lib
|-- nlp
|-- weather
|-- weather.json
|-- rain.json
|-- src
Let's teach botler about some cities too
[ "new york", "nyc", "JFK"]
[ "london", "lhr"]
|-- build
|-- lib
|-- nlp
|-- weather
|-- weather.json
|-- rain.json
|-- location
|-- new_york.json
|-- london.json
|-- src
import Botler, { User, Intent, defaultReducer } from 'botler';
import * as util from 'util';
//teach bot about weather using the previously created intent phrases
const bot = new Botler([`${__dirname}/../nlp`]);
//botler now knows about weather and some cities
function weatherSkill(user: User): Promise<User> {
const weather = ['sunny', 'rainy', 'cloudy']; //there are only three posibilities
// if we've detected a city (new york or london) then let's get a forecast
if (user.intent.topic === 'location') {
//just make the city name pretty new_york => 'new york'
const city = user.intent.action.replace('_', ' ');
user.state = 'none';
return sendToUser(`the weather in ${city} will be ${weather[Math.floor(Math.random()*weather.length)]}`)
.then(() => user);
/// weatherAPI(city)
/// .then(forecast => sendToUser(`forecase is ${forecast}`))
/// .then(() => user);
}
// botler has some helpers that are always running, such as looking for dates and numbers in the users text
// if the user has entered a number that is 5 digits, let's assume it's a zip code
if (user.intent.topic === 'details' && user.intent.details.value.toString().length === 5) {
const zip = user.intent.details.value;
user.state = 'none';
return sendToUser(`the weather at ${zip} will be ${weather[Math.floor(Math.random()*weather.length)]}`)
.then(() => user);
}
return null; //return null if skill can't process this intent;
}
function chatSkill(user: User): Promise<User> {
// decide how to respond based on the users intent
switch(user.intent.action) {
case 'hello': // user said hello
user.state = 'hello';
return sendToUser('Hi there! Would you like to know the weather?')
.then(() => user);
case 'help': // user asked for help
user.state = 'help';
return sendToUser('Hi there! just tell me what city you want to know the weather in...')
.then(() => user);
case 'weather': // user asked about the weather but didn't provide a location
user.state = 'location';
return sendToUser('What city do you want to know the weather in?')
.then(() => user);
case 'yes': // user said yes, check the state of the coversation to figure out what they said yes to
if (user.state === 'hello') { // user responded yes to 'would you like to know the weather?'
user.state = 'city';
return sendToUser('Great, what city?')
.then(() => user);
}
return null; //return null if skill can't process intent;
case 'no':
if (user.state === 'hello') { // user responded no to 'would you like to know the weather?'
user.state = 'none';
return sendToUser('Why not?')
.then(() => user);
}
return null; //return null if skill can't process intent;
default:
return null; //return null if skill can't process intent;
}
}
function confusedSkill(user: User): Promise<User> {
// catch all chat response if Botler couldn't detect a valid intent or the intent wasn't valid
// with the current user state
// console.log(`I'm confused, user intent was ${user.intent.action}`);
return sendToUser('I\'m confused')
.then(() => user);
}
function weatherReducer(intents: Array<Intent>): Promise<Intent> {
if (this && this.debugOn) console.log('intents:', util.inspect(intents, { depth:null }));
//if we detect a location, prioritize that intent and return it
const location = intents.filter(intent => intent.topic === 'location');
if (location.length > 0) {
return Promise.resolve(location[0]);
}
//otherwise just do the normal thing
return defaultReducer(intents);
}
function sendToUser(text: string): Promise<void> {
console.log(`<- ${text}`);
return Promise.resolve();
}
function receiveFromUser(user: User, text: string): Promise<User> {
console.log(`-> ${text}`);
return bot.processText(user, text);
}
// create an empty user and then add some custom info
const emptyUser: User = bot.createEmptyUser({ apiUserID: 'custom_info' });
// this could be an sample conversation
receiveFromUser(emptyUser, 'hi')
.then((user: User) => {
return receiveFromUser(user, 'yes');
})
.then((user: User) => {
return receiveFromUser(user, 'london');
})
.then((user: User) => {
return receiveFromUser(user, 'help');
})
.then((user: User) => {
return receiveFromUser(user, 'what\'s the weather in London?');
})
.then((user: User) => {
//should cause confusion
return receiveFromUser(user, 'you\'re the best!');
})
.then((user: User) => {
return receiveFromUser(user, 'What\'s the weather in New York, NY tomorrow');
})
.then((user: User) => {
return receiveFromUser(user, 'How about at 10004');
});