node-wit
Advanced tools
Comparing version 3.3.2 to 4.0.0
@@ -0,4 +1,22 @@ | ||
## v4.0.0 | ||
After a lot of internal dogfooding and bot building, we decided to change the API in a backwards-incompatible way. The changes are described below and aim to simplify user code and accommodate upcoming features. | ||
We moved to a Promise-based API, instead of callbacks. This makes the code simpler and the error-handling more straight-forward. It's also inline with where JS is going with standards like `fetch()` and `async/await` that are based on Promises. | ||
See `./examples` to see how to use the new API. | ||
### Breaking changes | ||
- `say` renamed to `send` to reflect that it deals with more than just text | ||
- Removed built-in actions `merge` and `error` | ||
- Actions signature simplified with `request` and `response` arguments | ||
- Actions need to return promises and do not receive the `cb` parameter anymore | ||
- INFO level replaces LOG level | ||
- configuration is now done when instantiating the `Wit` object, instead of using env vars | ||
## v3.3.2 | ||
- allows for targetting a specific API version, by setting `WIT_API_VERSION` | ||
- allows for overriding API version, by setting `WIT_API_VERSION` | ||
@@ -5,0 +23,0 @@ ## v3.3.1 |
'use strict'; | ||
// Joke example | ||
// See https://wit.ai/patapizza/example-joke | ||
let Wit = null; | ||
try { | ||
// if running from repo | ||
Wit = require('../').Wit; | ||
} catch (e) { | ||
Wit = require('node-wit').Wit; | ||
} | ||
// When not cloning the `node-wit` repo, replace the `require` like so: | ||
// const Wit = require('node-wit').Wit; | ||
const Wit = require('../').Wit; | ||
const token = (() => { | ||
const accessToken = (() => { | ||
if (process.argv.length !== 3) { | ||
console.log('usage: node examples/joke.js <wit-token>'); | ||
console.log('usage: node examples/joke.js <wit-access-token>'); | ||
process.exit(1); | ||
@@ -18,2 +19,5 @@ } | ||
// Joke example | ||
// See https://wit.ai/patapizza/example-joke | ||
const allJokes = { | ||
@@ -46,31 +50,40 @@ chuck: [ | ||
const actions = { | ||
say(sessionId, context, message, cb) { | ||
console.log(message); | ||
cb(); | ||
send(request, response) { | ||
console.log('sending...', JSON.stringify(response)); | ||
return Promise.resolve(); | ||
}, | ||
merge(sessionId, context, entities, message, cb) { | ||
delete context.joke; | ||
const category = firstEntityValue(entities, 'category'); | ||
if (category) { | ||
context.cat = category; | ||
} | ||
const sentiment = firstEntityValue(entities, 'sentiment'); | ||
if (sentiment) { | ||
context.ack = sentiment === 'positive' ? 'Glad you liked it.' : 'Hmm.'; | ||
} else { | ||
delete context.ack; | ||
} | ||
cb(context); | ||
merge({entities, context, message, sessionId}) { | ||
return new Promise(function(resolve, reject) { | ||
delete context.joke; | ||
const category = firstEntityValue(entities, 'category'); | ||
if (category) { | ||
context.cat = category; | ||
} | ||
const sentiment = firstEntityValue(entities, 'sentiment'); | ||
if (sentiment) { | ||
context.ack = sentiment === 'positive' ? 'Glad you liked it.' : 'Hmm.'; | ||
} else { | ||
delete context.ack; | ||
} | ||
return resolve(context); | ||
}); | ||
}, | ||
error(sessionId, context, error) { | ||
console.log(error.message); | ||
['select-joke']({entities, context}) { | ||
return new Promise(function(resolve, reject) { | ||
// const category = firstEntityValue(entities, 'category') || 'default'; | ||
// const sentiment = firstEntityValue(entities, 'sentiment'); | ||
// if (sentiment) { | ||
// context.ack = sentiment === 'positive' ? 'Glad you liked it.' : 'Hmm.'; | ||
// } else { | ||
// delete context.ack; | ||
// } | ||
const jokes = allJokes[context.cat || 'default']; | ||
context.joke = jokes[Math.floor(Math.random() * jokes.length)]; | ||
return resolve(context); | ||
}); | ||
}, | ||
['select-joke'](sessionId, context, cb) { | ||
const jokes = allJokes[context.cat || 'default']; | ||
context.joke = jokes[Math.floor(Math.random() * jokes.length)]; | ||
cb(context); | ||
}, | ||
}; | ||
const client = new Wit(token, actions); | ||
const client = new Wit({accessToken, actions}); | ||
client.interactive(); |
@@ -9,16 +9,25 @@ 'use strict'; | ||
// | ||
// 1. npm install body-parser express request | ||
// 1. npm install body-parser express request | ||
// 2. Download and install ngrok from https://ngrok.com/download | ||
// 3. ./ngrok http 8445 | ||
// 4. WIT_TOKEN=your_access_token FB_PAGE_ID=your_page_id FB_PAGE_TOKEN=your_page_token FB_VERIFY_TOKEN=verify_token node examples/messenger.js | ||
// 5. Subscribe your page to the Webhooks using verify_token and `https://<your_ngrok_io>/fb` as callback URL. | ||
// 4. WIT_TOKEN=your_access_token FB_APP_SECRET=your_app_secret FB_PAGE_TOKEN=your_page_token node examples/messenger.js | ||
// 5. Subscribe your page to the Webhooks using verify_token and `https://<your_ngrok_io>/webhook` as callback URL. | ||
// 6. Talk to your bot on Messenger! | ||
const bodyParser = require('body-parser'); | ||
const crypto = require('crypto'); | ||
const express = require('express'); | ||
const fetch = require('node-fetch'); | ||
const request = require('request'); | ||
// When not cloning the `node-wit` repo, replace the `require` like so: | ||
// const Wit = require('node-wit').Wit; | ||
const Wit = require('../').Wit; | ||
let Wit = null; | ||
let log = null; | ||
try { | ||
// if running from repo | ||
Wit = require('../').Wit; | ||
log = require('../').log; | ||
} catch (e) { | ||
Wit = require('node-wit').Wit; | ||
log = require('node-wit').log; | ||
} | ||
@@ -32,12 +41,17 @@ // Webserver parameter | ||
// Messenger API parameters | ||
const FB_PAGE_ID = process.env.FB_PAGE_ID && Number(process.env.FB_PAGE_ID); | ||
if (!FB_PAGE_ID) { | ||
throw new Error('missing FB_PAGE_ID'); | ||
} | ||
const FB_PAGE_ID = process.env.FB_PAGE_ID; | ||
if (!FB_PAGE_ID) { throw new Error('missing FB_PAGE_ID') } | ||
const FB_PAGE_TOKEN = process.env.FB_PAGE_TOKEN; | ||
if (!FB_PAGE_TOKEN) { | ||
throw new Error('missing FB_PAGE_TOKEN'); | ||
} | ||
const FB_VERIFY_TOKEN = process.env.FB_VERIFY_TOKEN; | ||
if (!FB_PAGE_TOKEN) { throw new Error('missing FB_PAGE_TOKEN') } | ||
const FB_APP_SECRET = process.env.FB_APP_SECRET; | ||
if (!FB_APP_SECRET) { throw new Error('missing FB_APP_SECRET') } | ||
let FB_VERIFY_TOKEN = null; | ||
crypto.randomBytes(8, (err, buff) => { | ||
if (err) throw err; | ||
FB_VERIFY_TOKEN = buff.toString('hex'); | ||
console.log(`/webhook will accept the Verify Token "${FB_VERIFY_TOKEN}"`); | ||
}); | ||
// ---------------------------------------------------------------------------- | ||
// Messenger API specific code | ||
@@ -47,45 +61,24 @@ | ||
// https://developers.facebook.com/docs/messenger-platform/send-api-reference | ||
const fbReq = request.defaults({ | ||
uri: 'https://graph.facebook.com/me/messages', | ||
method: 'POST', | ||
json: true, | ||
qs: { access_token: FB_PAGE_TOKEN }, | ||
headers: {'Content-Type': 'application/json'}, | ||
}); | ||
const fbMessage = (recipientId, msg, cb) => { | ||
const opts = { | ||
form: { | ||
recipient: { | ||
id: recipientId, | ||
}, | ||
message: { | ||
text: msg, | ||
}, | ||
}, | ||
}; | ||
fbReq(opts, (err, resp, data) => { | ||
if (cb) { | ||
cb(err || data.error && data.error.message, data); | ||
const fbMessage = (id, text) => { | ||
const body = JSON.stringify({ | ||
recipient: { id }, | ||
message: { text }, | ||
}); | ||
const qs = 'access_token=' + encodeURIComponent(FB_PAGE_TOKEN); | ||
return fetch('https://graph.facebook.com/me/messages?' + qs, { | ||
method: 'POST', | ||
headers: {'Content-Type': 'application/json'}, | ||
body, | ||
}) | ||
.then(rsp => rsp.json()) | ||
.then(json => { | ||
if (json.error && json.error.message) { | ||
throw new Error(json.error.message); | ||
} | ||
return json; | ||
}); | ||
}; | ||
// See the Webhook reference | ||
// https://developers.facebook.com/docs/messenger-platform/webhook-reference | ||
const getFirstMessagingEntry = (body) => { | ||
const val = body.object == 'page' && | ||
body.entry && | ||
Array.isArray(body.entry) && | ||
body.entry.length > 0 && | ||
body.entry[0] && | ||
body.entry[0].id == FB_PAGE_ID && | ||
body.entry[0].messaging && | ||
Array.isArray(body.entry[0].messaging) && | ||
body.entry[0].messaging.length > 0 && | ||
body.entry[0].messaging[0] | ||
; | ||
return val || null; | ||
}; | ||
// ---------------------------------------------------------------------------- | ||
// Wit.ai bot specific code | ||
@@ -117,3 +110,3 @@ | ||
const actions = { | ||
say(sessionId, context, message, cb) { | ||
send({sessionId}, {text}) { | ||
// Our bot has something to say! | ||
@@ -125,27 +118,19 @@ // Let's retrieve the Facebook user whose session belongs to | ||
// Let's forward our bot response to her. | ||
fbMessage(recipientId, message, (err, data) => { | ||
if (err) { | ||
console.log( | ||
'Oops! An error occurred while forwarding the response to', | ||
recipientId, | ||
':', | ||
err | ||
); | ||
} | ||
// Let's give the wheel back to our bot | ||
cb(); | ||
// We return a promise to let our bot know when we're done sending | ||
return fbMessage(recipientId, text) | ||
.then(() => null) | ||
.catch((err) => { | ||
console.error( | ||
'Oops! An error occurred while forwarding the response to', | ||
recipientId, | ||
':', | ||
err.stack || err | ||
); | ||
}); | ||
} else { | ||
console.log('Oops! Couldn\'t find user for session:', sessionId); | ||
console.error('Oops! Couldn\'t find user for session:', sessionId); | ||
// Giving the wheel back to our bot | ||
cb(); | ||
return Promise.resolve() | ||
} | ||
}, | ||
merge(sessionId, context, entities, message, cb) { | ||
cb(context); | ||
}, | ||
error(sessionId, context, error) { | ||
console.log(error.message); | ||
}, | ||
// You should implement your custom actions here | ||
@@ -156,15 +141,20 @@ // See https://wit.ai/docs/quickstart | ||
// Setting up our bot | ||
const wit = new Wit(WIT_TOKEN, actions); | ||
const wit = new Wit({ | ||
accessToken: WIT_TOKEN, | ||
actions, | ||
logger: new log.Logger(log.INFO) | ||
}); | ||
// Starting our webserver and putting it all together | ||
const app = express(); | ||
app.set('port', PORT); | ||
app.listen(app.get('port')); | ||
app.use(bodyParser.json()); | ||
app.use(({method, url}, rsp, next) => { | ||
rsp.on('finish', () => { | ||
console.log(`${rsp.statusCode} ${method} ${url}`); | ||
}); | ||
next(); | ||
}); | ||
app.use(bodyParser.json({ verify: verifyRequestSignature })); | ||
// Webhook setup | ||
app.get('/fb', (req, res) => { | ||
if (!FB_VERIFY_TOKEN) { | ||
throw new Error('missing FB_VERIFY_TOKEN'); | ||
} | ||
app.get('/webhook', (req, res) => { | ||
if (req.query['hub.mode'] === 'subscribe' && | ||
@@ -179,57 +169,61 @@ req.query['hub.verify_token'] === FB_VERIFY_TOKEN) { | ||
// Message handler | ||
app.post('/fb', (req, res) => { | ||
// Parsing the Messenger API response | ||
const messaging = getFirstMessagingEntry(req.body); | ||
if (messaging && messaging.message && messaging.recipient.id === FB_PAGE_ID) { | ||
// Yay! We got a new message! | ||
app.post('/webhook', (req, res) => { | ||
// Parse the Messenger payload | ||
// See the Webhook reference | ||
// https://developers.facebook.com/docs/messenger-platform/webhook-reference | ||
const data = req.body; | ||
// We retrieve the Facebook user ID of the sender | ||
const sender = messaging.sender.id; | ||
if (data.object === 'page') { | ||
data.entry.forEach(entry => { | ||
entry.messaging.forEach(event => { | ||
if (event.message) { | ||
// Yay! We got a new message! | ||
// We retrieve the Facebook user ID of the sender | ||
const sender = event.sender.id; | ||
// We retrieve the user's current session, or create one if it doesn't exist | ||
// This is needed for our bot to figure out the conversation history | ||
const sessionId = findOrCreateSession(sender); | ||
// We retrieve the user's current session, or create one if it doesn't exist | ||
// This is needed for our bot to figure out the conversation history | ||
const sessionId = findOrCreateSession(sender); | ||
// We retrieve the message content | ||
const msg = messaging.message.text; | ||
const atts = messaging.message.attachments; | ||
// We retrieve the message content | ||
const {text, attachments} = event.message; | ||
if (atts) { | ||
// We received an attachment | ||
if (attachments) { | ||
// We received an attachment | ||
// Let's reply with an automatic message | ||
fbMessage(sender, 'Sorry I can only process text messages for now.') | ||
.catch(console.error); | ||
} else if (text) { | ||
// We received a text message | ||
// Let's reply with an automatic message | ||
fbMessage( | ||
sender, | ||
'Sorry I can only process text messages for now.' | ||
); | ||
} else if (msg) { | ||
// We received a text message | ||
// Let's forward the message to the Wit.ai Bot Engine | ||
// This will run all actions until our bot has nothing left to do | ||
wit.runActions( | ||
sessionId, // the user's current session | ||
text, // the user's message | ||
sessions[sessionId].context // the user's current session state | ||
).then((context) => { | ||
// Our bot did everything it has to do. | ||
// Now it's waiting for further messages to proceed. | ||
console.log('Waiting for next user messages'); | ||
// Let's forward the message to the Wit.ai Bot Engine | ||
// This will run all actions until our bot has nothing left to do | ||
wit.runActions( | ||
sessionId, // the user's current session | ||
msg, // the user's message | ||
sessions[sessionId].context, // the user's current session state | ||
(error, context) => { | ||
if (error) { | ||
console.log('Oops! Got an error from Wit:', error); | ||
} else { | ||
// Our bot did everything it has to do. | ||
// Now it's waiting for further messages to proceed. | ||
console.log('Waiting for futher messages.'); | ||
// Based on the session state, you might want to reset the session. | ||
// This depends heavily on the business logic of your bot. | ||
// Example: | ||
// if (context['done']) { | ||
// delete sessions[sessionId]; | ||
// } | ||
// Based on the session state, you might want to reset the session. | ||
// This depends heavily on the business logic of your bot. | ||
// Example: | ||
// if (context['done']) { | ||
// delete sessions[sessionId]; | ||
// } | ||
// Updating the user's current session state | ||
sessions[sessionId].context = context; | ||
// Updating the user's current session state | ||
sessions[sessionId].context = context; | ||
}) | ||
.catch((err) => { | ||
console.error('Oops! Got an error from Wit: ', err.stack || err); | ||
}) | ||
} | ||
} else { | ||
console.log('received event', JSON.stringify(event)); | ||
} | ||
); | ||
} | ||
}); | ||
}); | ||
} | ||
@@ -239,1 +233,33 @@ res.sendStatus(200); | ||
/* | ||
* Verify that the callback came from Facebook. Using the App Secret from | ||
* the App Dashboard, we can verify the signature that is sent with each | ||
* callback in the x-hub-signature field, located in the header. | ||
* | ||
* https://developers.facebook.com/docs/graph-api/webhooks#setup | ||
* | ||
*/ | ||
function verifyRequestSignature(req, res, buf) { | ||
var signature = req.headers["x-hub-signature"]; | ||
if (!signature) { | ||
// For testing, let's log an error. In production, you should throw an | ||
// error. | ||
console.error("Couldn't validate the signature."); | ||
} else { | ||
var elements = signature.split('='); | ||
var method = elements[0]; | ||
var signatureHash = elements[1]; | ||
var expectedHash = crypto.createHmac('sha1', FB_APP_SECRET) | ||
.update(buf) | ||
.digest('hex'); | ||
if (signatureHash != expectedHash) { | ||
throw new Error("Couldn't validate the request signature."); | ||
} | ||
} | ||
} | ||
app.listen(PORT); | ||
console.log('Listening on :' + PORT + '...'); |
'use strict'; | ||
// Quickstart example | ||
// See https://wit.ai/l5t/Quickstart | ||
let Wit = null; | ||
try { | ||
// if running from repo | ||
Wit = require('../').Wit; | ||
} catch (e) { | ||
Wit = require('node-wit').Wit; | ||
} | ||
// When not cloning the `node-wit` repo, replace the `require` like so: | ||
// const Wit = require('node-wit').Wit; | ||
const Wit = require('../').Wit; | ||
const token = (() => { | ||
const accessToken = (() => { | ||
if (process.argv.length !== 3) { | ||
console.log('usage: node examples/quickstart.js <wit-token>'); | ||
console.log('usage: node examples/quickstart.js <wit-access-token>'); | ||
process.exit(1); | ||
@@ -18,2 +19,5 @@ } | ||
// Quickstart example | ||
// See https://wit.ai/ar7hur/quickstart | ||
const firstEntityValue = (entities, entity) => { | ||
@@ -32,26 +36,25 @@ const val = entities && entities[entity] && | ||
const actions = { | ||
say(sessionId, context, message, cb) { | ||
console.log(message); | ||
cb(); | ||
send(request, response) { | ||
const {sessionId, context, entities} = request; | ||
const {text, quickreplies} = response; | ||
return new Promise(function(resolve, reject) { | ||
console.log('sending...', JSON.stringify(response)); | ||
return resolve(); | ||
}); | ||
}, | ||
merge(sessionId, context, entities, message, cb) { | ||
// Retrieve the location entity and store it into a context field | ||
const loc = firstEntityValue(entities, 'location'); | ||
if (loc) { | ||
context.loc = loc; | ||
} | ||
cb(context); | ||
getForecast({context, entities}) { | ||
return new Promise(function(resolve, reject) { | ||
var location = firstEntityValue(entities, 'location') | ||
if (location) { | ||
context.forecast = 'sunny in ' + location; // we should call a weather API here | ||
} else { | ||
context.missingLocation = true; | ||
delete context.forecast; | ||
} | ||
return resolve(context); | ||
}); | ||
}, | ||
error(sessionId, context, error) { | ||
console.log(error.message); | ||
}, | ||
['fetch-weather'](sessionId, context, cb) { | ||
// Here should go the api call, e.g.: | ||
// context.forecast = apiCall(context.loc) | ||
context.forecast = 'sunny'; | ||
cb(context); | ||
}, | ||
}; | ||
const client = new Wit(token, actions); | ||
const client = new Wit({accessToken, actions}); | ||
client.interactive(); |
module.exports = { | ||
Logger: require('./lib/logger.js').Logger, | ||
logLevels: require('./lib/logger.js').logLevels, | ||
log: require('./lib/log.js'), | ||
Wit: require('./lib/wit.js').Wit, | ||
} |
459
lib/wit.js
@@ -6,20 +6,164 @@ 'use strict'; | ||
const uuid = require('node-uuid'); | ||
const Logger = require('./logger').Logger; | ||
const logLevels = require('./logger').logLevels; | ||
const log = require('./log'); | ||
const DEFAULT_API_VERSION = '20160516'; | ||
const DEFAULT_MAX_STEPS = 5; | ||
const CALLBACK_TIMEOUT_MS = 10000; | ||
const DEFAULT_WIT_URL = 'https://api.wit.ai'; | ||
const learnMore = 'Learn more at https://wit.ai/docs/quickstart'; | ||
let l = new Logger(logLevels.LOG); | ||
function Wit(opts) { | ||
if (!(this instanceof Wit)) { | ||
return new Wit(opts); | ||
} | ||
const makeWitResponseHandler = (endpoint, l, cb) => { | ||
const error = err => { | ||
l.error('[' + endpoint + '] Error: ' + err); | ||
if (cb) { | ||
cb(err); | ||
const { | ||
accessToken, apiVersion, actions, headers, logger, witURL | ||
} = this.config = Object.freeze(validate(opts)); | ||
this.message = (message, context) => { | ||
let qs = 'q=' + encodeURIComponent(message); | ||
if (context) { | ||
qs += '&context=' + encodeURIComponent(JSON.stringify(context)); | ||
} | ||
const method = 'GET'; | ||
const fullURL = witURL + '/message?' + qs | ||
const handler = makeWitResponseHandler(logger, 'message'); | ||
logger.debug(method, fullURL); | ||
return fetch(fullURL, { | ||
method, | ||
headers, | ||
}) | ||
.then(response => Promise.all([response.json(), response.status])) | ||
.then(handler) | ||
; | ||
}; | ||
this.converse = (sessionId, message, context) => { | ||
let qs = 'session_id=' + encodeURIComponent(sessionId); | ||
if (message) { | ||
qs += '&q=' + encodeURIComponent(message); | ||
} | ||
const method = 'POST'; | ||
const fullURL = witURL + '/converse?' + qs; | ||
const handler = makeWitResponseHandler(logger, 'converse'); | ||
logger.debug(method, fullURL); | ||
return fetch(fullURL, { | ||
method, | ||
headers, | ||
body: JSON.stringify(context), | ||
}) | ||
.then(response => Promise.all([response.json(), response.status])) | ||
.then(handler) | ||
; | ||
}; | ||
const continueRunActions = (sessionId, message, prevContext, i) => { | ||
return (json) => { | ||
if (i < 0) { | ||
logger.warn('Max steps reached, stopping.'); | ||
return prevContext; | ||
} | ||
if (!json.type) { | ||
throw new Error('Couldn\'t find type in Wit response'); | ||
} | ||
logger.debug('Context: ' + JSON.stringify(prevContext)); | ||
logger.debug('Response type: ' + json.type); | ||
// backwards-cpmpatibility with API version 20160516 | ||
if (json.type === 'merge') { | ||
json.type = 'action'; | ||
json.action = 'merge'; | ||
} | ||
if (json.type === 'error') { | ||
throw new Error('Oops, I don\'t know what to do.'); | ||
} | ||
if (json.type === 'stop') { | ||
return prevContext; | ||
} | ||
const request = { | ||
sessionId, | ||
context: clone(prevContext), | ||
text: message, | ||
entities: json.entities, | ||
}; | ||
if (json.type === 'msg') { | ||
throwIfActionMissing(actions, 'send'); | ||
const response = { | ||
text: json.msg, | ||
quickreplies: json.quickreplies, | ||
}; | ||
return actions.send(request, response).then((ctx) => { | ||
if (ctx) { | ||
throw new Error('Cannot update context after \'send\' action'); | ||
} | ||
return this.converse(sessionId, null, prevContext).then( | ||
continueRunActions(sessionId, message, prevContext, i - 1) | ||
); | ||
}); | ||
} else if (json.type === 'action') { | ||
const action = json.action; | ||
throwIfActionMissing(actions, action); | ||
return actions[action](request).then((ctx) => { | ||
const nextContext = ctx || {}; | ||
return this.converse(sessionId, null, nextContext).then( | ||
continueRunActions(sessionId, message, nextContext, i - 1) | ||
); | ||
}); | ||
} else { | ||
logger.debug('unknown response type', json); | ||
throw new Error('unknown response type ' + json.type); | ||
} | ||
} | ||
}; | ||
this.runActions = (sessionId, message, context, maxSteps) => { | ||
if (!actions) throwMustHaveActions(); | ||
const steps = maxSteps ? maxSteps : DEFAULT_MAX_STEPS; | ||
return this.converse(sessionId, message, context).then( | ||
continueRunActions(sessionId, message, context, steps) | ||
); | ||
}; | ||
this.interactive = (initContext, maxSteps) => { | ||
if (!actions) throwMustHaveActions(); | ||
let context = typeof initContext === 'object' ? initContext : {}; | ||
const sessionId = uuid.v1(); | ||
const steps = maxSteps ? maxSteps : DEFAULT_MAX_STEPS; | ||
const rl = readline.createInterface({ | ||
input: process.stdin, | ||
output: process.stdout, | ||
}); | ||
rl.setPrompt('> '); | ||
const prompt = () => { | ||
rl.prompt(); | ||
rl.write(null, {ctrl: true, name: 'e'}); | ||
}; | ||
prompt(); | ||
rl.on('line', (line) => { | ||
line = line.trim(); | ||
if (!line) { | ||
return prompt(); | ||
} | ||
this.runActions(sessionId, line, context, steps) | ||
.then((ctx) => { | ||
context = ctx; | ||
prompt(); | ||
}) | ||
.catch(logger.error) | ||
}); | ||
}; | ||
}; | ||
const makeWitResponseHandler = (logger, endpoint) => { | ||
return rsp => { | ||
const error = err => { | ||
logger.error('[' + endpoint + '] Error: ' + err); | ||
throw err; | ||
}; | ||
if (rsp instanceof Error) { | ||
@@ -29,4 +173,3 @@ return error(rsp); | ||
const json = rsp[0]; | ||
const status = rsp[1]; | ||
const [json, status] = rsp; | ||
@@ -40,60 +183,71 @@ if (json instanceof Error) { | ||
if (err) { | ||
return error(err) | ||
return error(err); | ||
} | ||
l.debug('[' + endpoint + '] Response: ' + JSON.stringify(json)); | ||
if (cb) { | ||
cb(null, json); | ||
} | ||
logger.debug('[' + endpoint + '] Response: ' + JSON.stringify(json)); | ||
return json; | ||
} | ||
}; | ||
const validateActions = (actions) => { | ||
const learnMore = 'Learn more at https://wit.ai/docs/quickstart'; | ||
if (typeof actions !== 'object') { | ||
throw new Error('The second parameter should be an Object.'); | ||
const throwMustHaveActions = () => { | ||
throw new Error('You must provide the `actions` parameter to be able to use runActions. ' + learnMore) | ||
}; | ||
const throwIfActionMissing = (actions, action) => { | ||
if (!actions[action]) { | ||
throw new Error('No \'' + action + '\' action found.'); | ||
} | ||
if (!actions.say) { | ||
throw new Error('The \'say\' action is missing. ' + learnMore); | ||
}; | ||
const validate = (opts) => { | ||
if (!opts.accessToken) { | ||
throw new Error('Could not find access token, learn more at https://wit.ai/docs'); | ||
} | ||
if (!actions.merge) { | ||
throw new Error('The \'merge\' action is missing. ' + learnMore); | ||
opts.witURL = opts.witURL || DEFAULT_WIT_URL; | ||
opts.apiVersion = opts.apiVersion || DEFAULT_API_VERSION; | ||
opts.headers = opts.headers || { | ||
'Authorization': 'Bearer ' + opts.accessToken, | ||
'Accept': 'application/vnd.wit.' + opts.apiVersion + '+json', | ||
'Content-Type': 'application/json', | ||
}; | ||
opts.logger = opts.logger || new log.Logger(log.INFO); | ||
if (opts.actions) { | ||
opts.actions = validateActions(opts.logger, opts.actions); | ||
} | ||
if (!actions.error) { | ||
throw new Error('The \'error\' action is missing. ' + learnMore); | ||
return opts; | ||
}; | ||
const validateActions = (logger, actions) => { | ||
if (typeof actions !== 'object') { | ||
throw new Error('Actions should be an object. ' + learnMore); | ||
} | ||
if (!actions.send) { | ||
throw new Error('The \'send\' action is missing. ' + learnMore); | ||
} | ||
Object.keys(actions).forEach(key => { | ||
if (typeof actions[key] !== 'function') { | ||
l.warn('The \'' + key + '\' action should be a function.'); | ||
logger.warn('The \'' + key + '\' action should be a function.'); | ||
} | ||
if (key === 'say' && actions.say.length !== 4) { | ||
l.warn('The \'say\' action should accept 4 arguments: sessionId, context, message, callback. ' + learnMore); | ||
} else if (key === 'merge' && actions.merge.length !== 5) { | ||
l.warn('The \'merge\' action should accept 5 arguments: sessionId, context, entities, message, callback. ' + learnMore); | ||
} else if (key === 'error' && actions.error.length !== 3) { | ||
l.warn('The \'error\' action should accept 3 arguments: sessionId, context, error. ' + learnMore); | ||
} else if (key !== 'say' && key !== 'merge' && key !== 'error' && actions[key].length !== 3) { | ||
l.warn('The \'' + key + '\' action should accept 3 arguments: sessionId, context, callback. ' + learnMore); | ||
if (key === 'say' && actions[key].length > 2 || | ||
key === 'merge' && actions[key].length > 2 || | ||
key === 'error' && actions[key].length > 2 | ||
) { | ||
logger.warn('The \'' + key + '\' action has been deprecated. ' + learnMore); | ||
} | ||
if (key === 'send') { | ||
if (actions[key].length !== 2) { | ||
logger.warn('The \'send\' action should accept 2 arguments: request and response. ' + learnMore); | ||
} | ||
} else if (actions[key].length !== 1) { | ||
logger.warn('The \'' + key + '\' action should accept 1 argument: request. ' + learnMore); | ||
} | ||
}); | ||
return actions; | ||
}; | ||
const makeCallbackTimeout = (ms) => { | ||
return setTimeout(() => { | ||
l.warn('I didn\'t get the callback after ' + (ms / 1000) + ' seconds. Did you forget to call me back?'); | ||
}, ms); | ||
}; | ||
const cbIfActionMissing = (actions, action, cb) => { | ||
if (!actions.hasOwnProperty(action)) { | ||
if (cb) { | ||
cb('No \'' + action + '\' action found.'); | ||
} | ||
return true; | ||
} | ||
return false; | ||
}; | ||
const clone = (obj) => { | ||
@@ -115,203 +269,4 @@ if (obj !== null && typeof obj === 'object') { | ||
const Wit = function(token, actions, logger) { | ||
const baseURL = process.env.WIT_URL || 'https://api.wit.ai'; | ||
const version = process.env.WIT_API_VERSION || DEFAULT_API_VERSION; | ||
const headers = { | ||
'Authorization': 'Bearer ' + token, | ||
'Accept': 'application/vnd.wit.' + version + '+json', | ||
'Content-Type': 'application/json', | ||
}; | ||
if (logger) { | ||
l = logger; | ||
} | ||
this.actions = validateActions(actions); | ||
this.message = (message, context, cb) => { | ||
if (typeof context === 'function') { | ||
cb = context; | ||
context = undefined; | ||
} | ||
let qs = 'q=' + encodeURIComponent(message); | ||
if (context) { | ||
qs += '&context=' + encodeURIComponent(JSON.stringify(context)); | ||
} | ||
const handler = makeWitResponseHandler('message', l, cb); | ||
fetch(baseURL + '/message?' + qs, { | ||
method: 'GET', | ||
headers: headers, | ||
}) | ||
.then(response => Promise.all([response.json(), response.status])) | ||
.then(handler) | ||
.catch(handler) | ||
; | ||
}; | ||
this.converse = (sessionId, message, context, cb) => { | ||
const handler = makeWitResponseHandler('converse', l, cb); | ||
let qs = 'session_id=' + sessionId; | ||
if (message) { | ||
qs += '&q=' + encodeURIComponent(message); | ||
} | ||
fetch(baseURL + '/converse?' + qs, { | ||
method: 'POST', | ||
headers: headers, | ||
body: JSON.stringify(context), | ||
}) | ||
.then(response => Promise.all([response.json(), response.status])) | ||
.then(handler) | ||
.catch(handler) | ||
; | ||
}; | ||
const makeCallback = (i, sessionId, message, context, cb) => { | ||
let timeoutID; | ||
const makeActionCallback = () => { | ||
timeoutID = makeCallbackTimeout(CALLBACK_TIMEOUT_MS); | ||
return (newContext) => { | ||
if (timeoutID) { | ||
clearTimeout(timeoutID); | ||
timeoutID = null; | ||
} | ||
const context = newContext || {}; | ||
l.debug('Context\': ' + JSON.stringify(context)); | ||
if (i <= 0) { | ||
l.warn('Max steps reached, halting.'); | ||
if (cb) { | ||
cb(null, context); | ||
} | ||
return; | ||
} | ||
// Retrieving action sequence | ||
this.converse( | ||
sessionId, | ||
null, | ||
context, | ||
makeCallback(--i, sessionId, message, context, cb).bind(this) | ||
); | ||
}; | ||
}; | ||
const makeSayCallback = () => { | ||
timeoutID = makeCallbackTimeout(CALLBACK_TIMEOUT_MS); | ||
return function() { | ||
if (arguments.length > 0) { | ||
throw new Error('The \'say\' callback should not have any arguments!'); | ||
} | ||
if (timeoutID) { | ||
clearTimeout(timeoutID); | ||
timeoutID = null; | ||
} | ||
if (i <= 0) { | ||
l.warn('Max steps reached, halting.'); | ||
if (cb) { | ||
cb(null, context); | ||
} | ||
return; | ||
} | ||
// Retrieving action sequence | ||
this.converse( | ||
sessionId, | ||
null, | ||
context, | ||
makeCallback(--i, sessionId, message, context, cb).bind(this) | ||
); | ||
}; | ||
}; | ||
return (error, json) => { | ||
l.debug('Context: ' + JSON.stringify(context)); | ||
error = error || !json.type && 'Couldn\'t find type in Wit response'; | ||
if (error) { | ||
if (cb) { | ||
cb(error); | ||
} | ||
return; | ||
} | ||
var clonedContext = clone(context); | ||
if (json.type === 'stop') { | ||
// End of turn | ||
if (cb) { | ||
cb(null, context); | ||
} | ||
return; | ||
} else if (json.type === 'msg') { | ||
if (cbIfActionMissing(this.actions, 'say', cb)) { | ||
return; | ||
} | ||
l.log('Executing say with message: ' + json.msg); | ||
this.actions.say(sessionId, clonedContext, json.msg, makeSayCallback().bind(this)); | ||
} else if (json.type === 'merge') { | ||
if (cbIfActionMissing(this.actions, 'merge', cb)) { | ||
return; | ||
} | ||
l.log('Executing merge action'); | ||
this.actions.merge(sessionId, clonedContext, json.entities, message, makeActionCallback()); | ||
} else if (json.type === 'action') { | ||
const action = json.action; | ||
if (cbIfActionMissing(this.actions, action, cb)) { | ||
return; | ||
} | ||
l.log('Executing action: ' + action); | ||
this.actions[action](sessionId, clonedContext, makeActionCallback()); | ||
} else { // error | ||
if (cbIfActionMissing(this.actions, 'error', cb)) { | ||
return; | ||
} | ||
l.log('Executing error action'); | ||
this.actions.error(sessionId, clonedContext, new Error('Oops, I don\'t know what to do.')); | ||
return; | ||
} | ||
}; | ||
}; | ||
this.runActions = (sessionId, message, context, cb, maxSteps) => { | ||
const steps = maxSteps ? maxSteps : DEFAULT_MAX_STEPS; | ||
this.converse( | ||
sessionId, | ||
message, | ||
context, | ||
makeCallback(steps, sessionId, message, context, cb).bind(this) | ||
); | ||
}; | ||
this.interactive = (initContext, maxSteps) => { | ||
const sessionId = uuid.v1(); | ||
this.context = typeof initContext === 'object' ? initContext : {}; | ||
const steps = maxSteps ? maxSteps : DEFAULT_MAX_STEPS; | ||
this.rl = readline.createInterface({ | ||
input: process.stdin, | ||
output: process.stdout, | ||
}); | ||
this.rl.setPrompt('> '); | ||
this.rl.prompt(); | ||
this.rl.write(null, {ctrl: true, name: 'e'}); | ||
this.rl.on('line', ((line) => { | ||
const msg = line.trim(); | ||
this.runActions( | ||
sessionId, | ||
msg, | ||
this.context, | ||
(error, context) => { | ||
if (error) { | ||
l.error(error); | ||
} else { | ||
this.context = context; | ||
} | ||
this.rl.prompt(); | ||
this.rl.write(null, {ctrl: true, name: 'e'}); | ||
}, | ||
steps | ||
); | ||
}).bind(this)); | ||
}; | ||
}; | ||
module.exports = { | ||
Wit: Wit, | ||
Wit, | ||
}; |
{ | ||
"name": "node-wit", | ||
"version": "3.3.2", | ||
"version": "4.0.0", | ||
"description": "Wit.ai Node.js SDK", | ||
@@ -19,7 +19,10 @@ "keywords": [ | ||
"repository": "https://github.com/wit-ai/node-wit", | ||
"author": "Julien Odent <julien@wit.ai>", | ||
"author": "The Wit Team <help@wit.ai>", | ||
"dependencies": { | ||
"node-fetch": "^1.5.1", | ||
"node-uuid": "^1.4.7" | ||
}, | ||
"engines": { | ||
"node" : ">=4.0.0" | ||
} | ||
} |
246
README.md
@@ -18,3 +18,6 @@ # Wit Node.js SDK [![npm](https://img.shields.io/npm/v/node-wit.svg)](https://www.npmjs.com/package/node-wit) | ||
```bash | ||
node examples/template.js <your_token> | ||
# Node.js <= 6.x.x, add the flag --harmony_destructuring | ||
node --harmony_destructuring examples/basic.js <MY_TOKEN> | ||
# Node.js >= v6.x.x | ||
node examples/basic.js <MY_TOKEN> | ||
``` | ||
@@ -24,44 +27,6 @@ | ||
## API | ||
### Messenger integration example | ||
### Version change | ||
See `examples/messenger.js` for a thoroughly documented tutorial. | ||
On 2016, May 11th, the /message API was updated to reflect the new Bot Engine model: intent are now entities. | ||
We updated the SDK to the latest version: 20160516. | ||
You can target a specific version by setting the env variable `WIT_API_VERSION`. | ||
```json | ||
{ | ||
"msg_id" : "e86468e5-b9e8-4645-95ce-b41a66fda88d", | ||
"_text" : "hello", | ||
"entities" : { | ||
"intent" : [ { | ||
"confidence" : 0.9753469589149633, | ||
"value" : "greetings" | ||
} ] | ||
} | ||
} | ||
``` | ||
Version prior to 20160511 will return the old format: | ||
```json | ||
{ | ||
"msg_id" : "722fc79b-725c-4ca1-8029-b7f57ff88f54", | ||
"_text" : "hello", | ||
"outcomes" : [ { | ||
"_text" : "hello", | ||
"confidence" : null, | ||
"intent" : "default_intent", | ||
"entities" : { | ||
"intent" : [ { | ||
"confidence" : 0.9753469589149633, | ||
"value" : "greetings" | ||
} ] | ||
} | ||
} ], | ||
"WARNING" : "DEPRECATED" | ||
} | ||
``` | ||
### Overview | ||
@@ -78,50 +43,50 @@ | ||
The Wit constructor takes the following parameters: | ||
* `token` - the access token of your Wit instance | ||
* `actions` - the object with your actions | ||
* `accessToken` - the access token of your Wit instance | ||
* `actions` - (optional if only using `.message()`) the object with your actions | ||
* `logger` - (optional) the object handling the logging. | ||
* `apiVersion` - (optional) the API version to use instead of the recommended one | ||
The `actions` object has action names as properties, and action implementations as values. | ||
You need to provide at least an implementation for the special actions `say`, `merge` and `error`. | ||
The `actions` object has action names as properties, and action functions as values. | ||
Action implementations must return Promises (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) | ||
You must provide at least an implementation for the special action `send`. | ||
A minimal `actions` object looks like this: | ||
```js | ||
const actions = { | ||
say(sessionId, context, message, cb) { | ||
console.log(message); | ||
cb(); | ||
}, | ||
merge(sessionId, context, entities, message, cb) { | ||
cb(context); | ||
}, | ||
error(sessionId, context, error) { | ||
console.log(error.message); | ||
}, | ||
}; | ||
``` | ||
* `send` takes 2 parameters: `request` and `response` | ||
* custom actions take 1 parameter: `request` | ||
A custom action takes the following parameters: | ||
* `sessionId` - a unique identifier describing the user session | ||
* `context` - the object representing the session state | ||
* `cb(context)` - a callback function to fire at the end of your action with the updated context. | ||
#### Request | ||
* `sessionId` (string) - a unique identifier describing the user session | ||
* `context` (object) - the object representing the session state | ||
* `text` (string) - the text message sent by your end-user | ||
* `entities` (object) - the entities extracted by Wit's NLU | ||
Example: | ||
```js | ||
const Wit = require('node-wit').Wit; | ||
const client = new Wit(token, actions); | ||
``` | ||
#### Response | ||
* `text` (string) - The text your bot needs to send to the user (as described in your Wit.ai Stories) | ||
* `quickreplies` | ||
The `logger` object should implement the methods `debug`, `log`, `warn` and `error`. | ||
All methods take a single parameter `message`. | ||
The `logger` object should implement the methods `debug`, `info`, `warn` and `error`. | ||
They can receive an arbitrary number of parameters to log. | ||
For convenience, we provide a `Logger` class, taking a log level parameter | ||
For convenience, we provide a `Logger`, taking a log level parameter (provided as `logLevels`). | ||
The following levels are defined: `DEBUG`, `LOG`, `WARN`, `ERROR`. | ||
Example: | ||
```js | ||
const Logger = require('node-wit').Logger; | ||
const levels = require('node-wit').logLevels; | ||
const Wit = require('node-wit').Wit; | ||
const {Wit, log} = require('node-wit'); | ||
const logger = new Logger(levels.DEBUG); | ||
const client = new Wit(token, actions, logger); | ||
const client = new Wit({ | ||
accessToken: MY_TOKEN, | ||
actions: { | ||
send(request, response) { | ||
return new Promise(function(resolve, reject) { | ||
console.log(JSON.stringify(response)); | ||
return resolve(); | ||
}); | ||
}, | ||
myAction({sessionId, context, text, entities}) { | ||
console.log(`Session ${sessionId} received ${text}`); | ||
console.log(`The current context is ${JSON.stringify(context)}`); | ||
console.log(`Wit extracted ${JSON.stringify(entities)}`); | ||
return Promise.resolve(context); | ||
} | ||
}, | ||
logger: new log.Logger(log.DEBUG) // optional | ||
}); | ||
``` | ||
@@ -136,14 +101,11 @@ | ||
* `context` - (optional) the object representing the session state | ||
* `cb(error, data)` - a callback function with the JSON response | ||
Example: | ||
```js | ||
const context = {}; | ||
client.message('what is the weather in London?', context, (error, data) => { | ||
if (error) { | ||
console.log('Oops! Got an error: ' + error); | ||
} else { | ||
console.log('Yay, got Wit.ai response: ' + JSON.stringify(data)); | ||
} | ||
}); | ||
const client = new Wit({accessToken: 'MY_TOKEN'}); | ||
client.message('what is the weather in London?', {}) | ||
.then((data) => { | ||
console.log('Yay, got Wit.ai response: ' + JSON.stringify(data)); | ||
}) | ||
.catch(console.error); | ||
``` | ||
@@ -159,25 +121,24 @@ | ||
* `context` - the object representing the session state | ||
* `cb(error, context)` - a callback function with the updated context | ||
* `maxSteps` - (optional) the maximum number of actions to execute (defaults to 5) | ||
Example: | ||
```js | ||
const session = 'my-user-session-42'; | ||
const sessionId = 'my-user-session-42'; | ||
const context0 = {}; | ||
client.runActions(session, 'what is the weather in London?', context0, (e, context1) => { | ||
if (e) { | ||
console.log('Oops! Got an error: ' + e); | ||
return; | ||
} | ||
client.runActions(sessionId, 'what is the weather in London?', context0) | ||
.then((context1) => { | ||
console.log('The session state is now: ' + JSON.stringify(context1)); | ||
client.runActions(session, 'and in Brussels?', context1, (e, context2) => { | ||
if (e) { | ||
console.log('Oops! Got an error: ' + e); | ||
return; | ||
} | ||
console.log('The session state is now: ' + JSON.stringify(context2)); | ||
}); | ||
return client.runActions(sessionId, 'and in Brussels?', context1); | ||
}) | ||
.then((context2) => { | ||
console.log('The session state is now: ' + JSON.stringify(context2)); | ||
}) | ||
.catch((e) => { | ||
console.log('Oops! Got an error: ' + e); | ||
}); | ||
``` | ||
See `./examples/messenger.js` for a full-fledged example | ||
### converse | ||
@@ -191,13 +152,10 @@ | ||
* `context` - the object representing the session state | ||
* `cb(error, data)` - a callback function with the JSON response | ||
Example: | ||
```js | ||
client.converse('my-user-session-42', 'what is the weather in London?', {}, (error, data) => { | ||
if (error) { | ||
console.log('Oops! Got an error: ' + error); | ||
} else { | ||
console.log('Yay, got Wit.ai response: ' + JSON.stringify(data)); | ||
} | ||
}); | ||
client.converse('my-user-session-42', 'what is the weather in London?', {}) | ||
.then((data) => { | ||
console.log('Yay, got Wit.ai response: ' + JSON.stringify(data)); | ||
}) | ||
.catch(console.error); | ||
``` | ||
@@ -217,42 +175,40 @@ | ||
## Messenger integration example | ||
## Changing the API version | ||
This quickstart assumes that you have: | ||
* a [Wit.ai bot setup](https://wit.ai/docs/quickstart); | ||
* a [Messenger Platform setup](https://developers.facebook.com/docs/messenger-platform/quickstart). | ||
On 2016, May 11th, the /message API was updated to reflect the new Bot Engine model: intent are now entities. | ||
We updated the SDK to the latest version: 20160516. | ||
You can target a specific version by passing the `apiVersion` parameter when creating the `Wit` object. | ||
### Install dependencies | ||
```bash | ||
npm install body-parser express node-fetch | ||
```json | ||
{ | ||
"msg_id" : "e86468e5-b9e8-4645-95ce-b41a66fda88d", | ||
"_text" : "hello", | ||
"entities" : { | ||
"intent" : [ { | ||
"confidence" : 0.9753469589149633, | ||
"value" : "greetings" | ||
} ] | ||
} | ||
} | ||
``` | ||
### Download and install ngrok | ||
Version prior to 20160511 will return the old format: | ||
From [here](https://ngrok.com/download). | ||
### Run ngrok | ||
```bash | ||
./ngrok http 8445 | ||
```json | ||
{ | ||
"msg_id" : "722fc79b-725c-4ca1-8029-b7f57ff88f54", | ||
"_text" : "hello", | ||
"outcomes" : [ { | ||
"_text" : "hello", | ||
"confidence" : null, | ||
"intent" : "default_intent", | ||
"entities" : { | ||
"intent" : [ { | ||
"confidence" : 0.9753469589149633, | ||
"value" : "greetings" | ||
} ] | ||
} | ||
} ], | ||
"WARNING" : "DEPRECATED" | ||
} | ||
``` | ||
This will provide `your_ngrok_domain` (the `Forwarding` line). | ||
### Run the example | ||
```bash | ||
export WIT_TOKEN=your_access_token | ||
export FB_PAGE_ID=your_page_id | ||
export FB_PAGE_TOKEN=your_page_token | ||
export FB_VERIFY_TOKEN=any_token | ||
node examples/messenger.js | ||
``` | ||
### Subscribe your page to Messenger Webhooks | ||
Using your `FB_VERIFY_TOKEN` and `https://<your_ngrok_domain>/fb` as callback URL. | ||
See the [Messenger Platform docs](https://developers.facebook.com/docs/messenger-platform/quickstart). | ||
### Talk to your bot on Messenger! |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 3 instances in 1 package
30990
6
646
208
5