forked from wit-ai/node-wit
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
378 additions
and
28 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,5 @@ | ||
node_modules | ||
.npmignore | ||
*.un~ | ||
*.js~ | ||
*~ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,290 @@ | ||
'use strict'; | ||
|
||
const ContextClass = require('../../../class/context.class') | ||
|
||
const { | ||
DEFAULT_API_VERSION, | ||
DEFAULT_MAX_STEPS, | ||
DEFAULT_WIT_URL | ||
} = require('./config'); | ||
const fetch = require('isomorphic-fetch'); | ||
const log = require('./log'); | ||
const uuid = require('node-uuid'); | ||
|
||
const learnMore = 'Learn more at https://wit.ai/docs/quickstart'; | ||
|
||
function Wit(opts) { | ||
if (!(this instanceof Wit)) { | ||
return new Wit(opts); | ||
} | ||
|
||
const { | ||
accessToken, apiVersion, actions, headers, logger, witURL | ||
} = this.config = Object.freeze(validate(opts)); | ||
|
||
this._sessions = {}; | ||
|
||
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, reset) => { | ||
//logger.verbose('converse') | ||
let qs = 'session_id=' + encodeURIComponent(sessionId); | ||
if (message) { | ||
qs += '&q=' + encodeURIComponent(message); | ||
} | ||
if (reset) { | ||
qs += '&reset=true'; | ||
} | ||
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, currentRequest, message, prevContext, i) => { | ||
const continueRunActions = (sessionId, currentRequest, message, prevContext, i, confidenceThreshold) => { | ||
confidenceThreshold = confidenceThreshold || 0; | ||
return (json) => { | ||
if (json.confidence < confidenceThreshold) { | ||
logger.debug('Not sure enough to run action'); | ||
return prevContext; | ||
} | ||
if (i < 0) { | ||
logger.warn('Max steps reached, stopping.'); | ||
return prevContext; | ||
} | ||
if (currentRequest !== this._sessions[sessionId]) { | ||
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-compatibility 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), | ||
context: Object.assign(new ContextClass(),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'); | ||
} | ||
if (currentRequest !== this._sessions[sessionId]) { | ||
return ctx; | ||
} | ||
return this.converse(sessionId, null, prevContext).then( | ||
//continueRunActions(sessionId, currentRequest, message, prevContext, i - 1) | ||
continueRunActions(sessionId, currentRequest, message, prevContext, i - 1, confidenceThreshold) | ||
); | ||
}); | ||
} else if (json.type === 'action') { | ||
|
||
//if action is null (usually due to a problem in story) | ||
//then it returns the previous context and also sets an error context | ||
//for handling | ||
|
||
if (json.action===null) { | ||
logger.error('missing action (null)') | ||
prevContext.error = 'missing action' | ||
actions[json.action] = Promise.resolve(prevContext) | ||
} | ||
else { | ||
throwIfActionMissing(actions, json.action); | ||
} | ||
|
||
return actions[json.action](request).then(ctx => { | ||
const nextContext = ctx || {}; | ||
if (currentRequest !== this._sessions[sessionId]) { | ||
return nextContext; | ||
} | ||
return this.converse(sessionId, null, nextContext).then( | ||
continueRunActions(sessionId, currentRequest, message, nextContext, i - 1, confidenceThreshold) | ||
); | ||
}); | ||
} else { | ||
logger.debug('unknown response type ' + json.type); | ||
throw new Error('unknown response type ' + json.type); | ||
} | ||
}; | ||
}; | ||
|
||
this.runActions = function(sessionId, message, context, maxSteps) { | ||
//logger.verbose('runActions') | ||
if (!actions) throwMustHaveActions(); | ||
const steps = maxSteps ? maxSteps : DEFAULT_MAX_STEPS; | ||
|
||
// Figuring out whether we need to reset the last turn. | ||
// Each new call increments an index for the session. | ||
// We only care about the last call to runActions. | ||
// All the previous ones are discarded (preemptive exit). | ||
const currentRequest = (this._sessions[sessionId] || 0) + 1; | ||
this._sessions[sessionId] = currentRequest; | ||
const cleanup = ctx => { | ||
if (currentRequest === this._sessions[sessionId]) { | ||
delete this._sessions[sessionId]; | ||
} | ||
return ctx; | ||
}; | ||
|
||
return this.converse(sessionId, message, context, currentRequest > 1).then( | ||
continueRunActions(sessionId, currentRequest, message, context, steps) | ||
).then(cleanup); | ||
}; | ||
}; | ||
|
||
const makeWitResponseHandler = (logger, endpoint) => { | ||
return rsp => { | ||
const error = err => { | ||
logger.error('[' + endpoint + '] Error: ' + err); | ||
throw err; | ||
}; | ||
|
||
if (rsp instanceof Error) { | ||
return error(rsp); | ||
} | ||
|
||
const [json, status] = rsp; | ||
|
||
if (json instanceof Error) { | ||
return error(json); | ||
} | ||
|
||
const err = json.error || status !== 200 && json.body + ' (' + status + ')'; | ||
|
||
if (err) { | ||
return error(err); | ||
} | ||
|
||
logger.debug('[' + endpoint + '] Response: ' + JSON.stringify(json)); | ||
return json; | ||
} | ||
}; | ||
|
||
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.'); | ||
} | ||
}; | ||
|
||
const validate = (opts) => { | ||
if (!opts.accessToken) { | ||
throw new Error('Could not find access token, learn more at https://wit.ai/docs'); | ||
} | ||
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); | ||
} | ||
|
||
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') { | ||
logger.warn('The \'' + key + '\' action should be a function.'); | ||
} | ||
|
||
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 clone = (obj) => { | ||
if (obj !== null && typeof obj === 'object') { | ||
if (Array.isArray(obj)) { | ||
return obj.map(clone); | ||
} else { | ||
const newObj = {}; | ||
Object.keys(obj).forEach(k => { | ||
newObj[k] = clone(obj[k]); | ||
}); | ||
return newObj; | ||
} | ||
} else { | ||
return obj; | ||
} | ||
}; | ||
|
||
module.exports = Wit; |
Oops, something went wrong.