Skip to content

Commit

Permalink
added ContextClass handling
Browse files Browse the repository at this point in the history
  • Loading branch information
umairda committed Sep 9, 2016
1 parent c0277f8 commit b2b6c6c
Show file tree
Hide file tree
Showing 4 changed files with 378 additions and 28 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
node_modules
.npmignore
*.un~
*.js~
*~
38 changes: 21 additions & 17 deletions lib/wit.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ function Wit(opts) {
}

const {
accessToken, apiVersion, actions, headers, logger, witURL
accessToken, apiVersion, actions, headers, logger, witURL, ContextClass
} = this.config = Object.freeze(validate(opts));

this._sessions = {};
Expand All @@ -30,7 +30,7 @@ function Wit(opts) {
const method = 'GET';
const fullURL = witURL + '/message?' + qs
const handler = makeWitResponseHandler(logger, 'message');
logger.debug(method, fullURL);
//logger.debug(method, fullURL);
return fetch(fullURL, {
method,
headers,
Expand All @@ -51,7 +51,7 @@ function Wit(opts) {
const method = 'POST';
const fullURL = witURL + '/converse?' + qs;
const handler = makeWitResponseHandler(logger, 'converse');
logger.debug(method, fullURL);
//logger.debug(method, fullURL);
return fetch(fullURL, {
method,
headers,
Expand All @@ -75,7 +75,7 @@ function Wit(opts) {
throw new Error('Couldn\'t find type in Wit response');
}

logger.debug('Context: ' + JSON.stringify(prevContext));
logger.debug('Context: ' + JSON.stringify(prevContext,null,1));
logger.debug('Response type: ' + json.type);

// backwards-compatibility with API version 20160516
Expand All @@ -94,7 +94,8 @@ function Wit(opts) {

const request = {
sessionId,
context: clone(prevContext),
//context: clone(prevContext),
context: (typeof ContextClass === 'undefined')?clone(prevContext):Object.assign(new ContextClass(),prevContext),
text: message,
entities: json.entities,
};
Expand Down Expand Up @@ -125,21 +126,22 @@ function Wit(opts) {
if (json.action===null) {
logger.error('missing action (null)')
prevContext.error = 'missing action'
actions[json.action] = Promise.resolve(prevContext)
return this.converse(sessionId, null, prevContext).then(
continueRunActions(sessionId, currentRequest, message, prevContext, i)
);
}
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)
);
});
}

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)
);
});
} else {
logger.debug('unknown response type ' + json.type);
throw new Error('unknown response type ' + json.type);
Expand All @@ -148,6 +150,7 @@ function Wit(opts) {
};

this.runActions = function(sessionId, message, context, maxSteps) {
//logger.verbose('runActions')
if (!actions) throwMustHaveActions();
const steps = maxSteps ? maxSteps : DEFAULT_MAX_STEPS;

Expand Down Expand Up @@ -193,7 +196,8 @@ const makeWitResponseHandler = (logger, endpoint) => {
return error(err);
}

logger.debug('[' + endpoint + '] Response: ' + JSON.stringify(json));
logger.debug('[' + endpoint + '] Response: ' + JSON.stringify(json,null,1));
//logger.debug('[' + endpoint + '] Response: ' + json);
return json;
}
};
Expand Down
290 changes: 290 additions & 0 deletions lib/witWConfidenceThreshold.js
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;
Loading

0 comments on commit b2b6c6c

Please sign in to comment.