Skip to content

Commit

Permalink
Introducing communication with the offline intent parser
Browse files Browse the repository at this point in the history
  • Loading branch information
andrenatal authored and hobinjk committed Feb 6, 2018
1 parent 9019793 commit 0443190
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 157 deletions.
3 changes: 3 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ addons:

before_install:
- ./tools/compile-openzwave.sh
- git clone https://github.com/mozilla-iot/intent-parser
- sudo pip install -e git+https://github.com/mycroftai/adapt#egg=adapt-parser
- python intent-parser/intent-parser-server.py &

env:
global:
Expand Down
208 changes: 77 additions & 131 deletions src/controllers/commands_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,18 +35,8 @@
const express = require('express');
const fetch = require('node-fetch');
const Constants = require('../constants.js');
const uuidv4 = require('uuid/v4');
const CommandsController = express.Router();

const aiUrl = 'https://api.api.ai/api/query';
const aiOptions = {
method: 'POST',
body: '',
headers: {
'Authorization': 'Bearer af2bccd942c24fd68622efc6b4c8526c',
'Content-Type': 'application/json'
}
};
const IntentParser = require('../models/intentparser')

const thingsOptions = {
body: '',
Expand Down Expand Up @@ -75,7 +65,8 @@ CommandsController.jwt = '';

/**
* Called by the app.js to configure the auth header and the address of the GW
* since the CommandsController will be posting to itself.
* and the python interface since the CommandsController
* will be posting to itself.
*/
CommandsController.configure = function(gatewayHref, jwt) {
CommandsController.gatewayHref = gatewayHref;
Expand All @@ -95,135 +86,90 @@ function toText(res) {
* thing API.
*/
CommandsController.post('/', function (request, response) {

if(!request.body || request.body.text === undefined) {
response.status(400).send(JSON.stringify(
{'message': 'Text element not defined'}));
return;
}

aiOptions.body = JSON.stringify(getAiBody(request.body.text));
fetch(aiUrl, aiOptions).then(toText)
.then(function(aiBody) {
var payload = parseAIBody(aiBody);
if (payload.cmd == 'IOT') {
const thingsUrl = CommandsController.gatewayHref +
Constants.THINGS_PATH;
thingsOptions.headers.Authorization = 'Bearer ' +
CommandsController.jwt;
const thingsUrl = CommandsController.gatewayHref +
Constants.THINGS_PATH;
thingsOptions.headers.Authorization = 'Bearer ' +
CommandsController.jwt;

fetch(thingsUrl, thingsOptions).then(toText)
.then(function(thingBody) {
let jsonBody = JSON.parse(thingBody);
let match = payload.param.toUpperCase();
let thingfound = false;
for (var i = 0; i < jsonBody.length; i++) {
var obj = jsonBody[i];
let name = obj.name.toUpperCase();
if (name == match) {
thingfound = true;
break;
}
}
if (thingfound) {
if ((payload.param3 != null) &&
(payload.param3 != '' && obj.properties.hue != '')) {
var colorname_to_hue = {red:0, orange:8500, yellow:17000,
green:25500, white:34000, blue:46920, purple:48000,
magenta:54000, pink:60000};
if (!colorname_to_hue[payload.param3]) {
response.status(404).json({'message': 'Hue color not found'});
return;
} else {
iotOptions.body = JSON.stringify({'hue':
colorname_to_hue[payload.param3]});
payload.href = obj.properties.hue.href;
}
} else if (payload.param2 == 'on' || payload.param2 == 'off') {
iotOptions.body = JSON.stringify({'on':
(payload.param2 == 'on') ? true : false});
payload.href = obj.properties.on.href;
} else {
response.status(404).json({'message': 'Command not found'});
return;
}
const iotUrl = CommandsController.gatewayHref + payload.href;
iotOptions.headers.Authorization =
'Bearer ' + CommandsController.jwt;
// Returning 201 to signify that the command was mapped to an
// intent and matched a 'thing' in our list. Return a response to
// caller with this status before the command finishes execution
// as the execution can take some time (e.g. blinds)
response.status(201).json({'message': 'Command Created'});
fetch(iotUrl, iotOptions)
.then(function() {
// In the future we may want to use WS to give a status of
// the disposition of the command execution..
})
.catch(function(err) {
// Future, give status via WS.
console.log('catch inside PUT:' + err);
});
fetch(thingsUrl, thingsOptions).then(toText)
.then(function(thingBody) {
let jsonBody = JSON.parse(thingBody);
IntentParser.train(jsonBody).then(() => {
IntentParser.query(request.body.text).then((payload) => {
let match = payload.param.toUpperCase();
let thingfound = false;
for (var i = 0; i < jsonBody.length; i++) {
var obj = jsonBody[i];
let name = obj.name.toUpperCase();
if (name == match) {
thingfound = true;
break;
}
}
if (thingfound) {
if (payload.param2 == 'on' || payload.param2 == 'off') {
iotOptions.body = JSON.stringify({'on':
(payload.param2 == 'on') ? true : false});
payload.href = obj.properties.on.href;
} else if ((payload.param3 != null) &&
(payload.param3 != '' && obj.properties.color != '')) {
var colorname_to_hue = {red:'#FF0000',
orange:'#FFB300', yellow:'#FFF700',
green:'#47f837', white:'#FCFBEA', blue:'#1100FF',
purple:'#971AC4', magenta:'#75009F', pink:'#FFC0CB'};
if (!colorname_to_hue[payload.param3]) {
response.status(404).json({'message': 'Hue color not found'});
return;
} else {
response.status(404).json({'message': 'Thing not found'});
iotOptions.body = JSON.stringify({'color':
colorname_to_hue[payload.param3]});
payload.href = obj.properties.color.href;
}
})
.catch(function(err) {
console.log('error catch:' + err);
});
} else {
response.status(406).json({'message': 'Unable to understand command'});
}
})
.catch(function(err) {
response.json('error: ' + err);
} else {
response.status(404).json({'message': 'Command not found'});
return;
}
const iotUrl = CommandsController.gatewayHref + payload.href;
iotOptions.headers.Authorization =
'Bearer ' + CommandsController.jwt;
// Returning 201 to signify that the command was mapped to an
// intent and matched a 'thing' in our list. Return a response to
// caller with this status before the command finishes execution
// as the execution can take some time (e.g. blinds)
response.status(201).json({'message': 'Command Created'});
fetch(iotUrl, iotOptions)
.then(function() {
// In the future we may want to use WS to give a status of
// the disposition of the command execution..
})
.catch(function(err) {
// Future, give status via WS.
console.log('catch inside PUT:' + err);
});
} else {
response.status(404).json({'message': 'Thing not found'});
}
}).catch(function(error) {
console.log('Error parsing intent:', error);
response.status(404).json({'message':
'Internal error determining intent'});
});
}).catch(function(error) {
console.log('Error parsing intent:', error);
response.status(404).json({'message':
'Internal error determining intent'});
});
});
})
.catch(function(err) {
console.log('error catch:' + err);
});

/**
* Formats the body for the request to API.ai and returns the String
*/
function getAiBody(text) {
let body = {
'v':'20150910',
'query': '',
'lang':'en',
'sessionId': uuidv4()
};
body.query = text;
return body;
}

/**
* Parses the Response returned by the API.ai and returns a JSON object that
* describes the command (in this case iot), the param (room), and the param2
* (on/off).
*/
function parseAIBody(aiBody) {
let jsonBody = JSON.parse(aiBody);
var payload = {
cmd: 'none',
param: 'none',
param2: 'none',
param3: 'none',
href: ''
};
//Determine the action from the API.AI intent parser
switch (jsonBody.result.action) {
case 'iot':
payload.cmd = 'IOT';
payload.param = jsonBody.result.parameters.rooms;
payload.param2 = jsonBody.result.parameters.onoff;
payload.param3 = jsonBody.result.parameters.color;
break;
case 'input.unknown':
console.log('unable to parse the intent');
break;
default:
console.log('No match');
break;
}
return payload;
}
});

module.exports = CommandsController;
76 changes: 76 additions & 0 deletions src/models/intentparser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/**
* Intent Parser Model
*
* Interface to the intent parser.
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
'use strict';

const net = require('net');
var IntentParser = {

keyword: 'turn,switch,make,change',
type: 'on,off,red,orange,yellow,green,white,blue,purple,magenta,pink',

/**
* Interface train the intent parser
*/
train: function(data) {
return new Promise((resolve, reject) => {
let socket_client = new net.Socket();
socket_client.connect(5555, '127.0.0.1', function() {
let things = '';
for (const key of Object.keys(data)) {
things += data[key].name + ',';
}
console.log('Connected to intent parser server');
socket_client.on('data', function(data) {
console.log('Training result:' + data);
resolve(data);
});
socket_client.write('t:' + IntentParser.keyword +
'|' + IntentParser.type + '|' + things.slice(0, -1));
});
socket_client.on('error', function(data) {
console.log('Training error:' + data);
reject();
});
});
},

/**
* Interface to query the intent parser
*/
query: function(query) {
return new Promise((resolve, reject) => {
let socket_client = new net.Socket();
socket_client.connect(5555, '127.0.0.1', function() {
socket_client.on('data', function(data) {
console.log('Received: ' + data);
if (data == '-1') {
reject();
} else {
let jsonBody = JSON.parse(data);
resolve({
cmd: 'IOT',
href: '',
param: jsonBody.Location,
param2: jsonBody.Type,
param3: jsonBody.Type
});
}
});
socket_client.write('q:' + query);
});
socket_client.on('error', function(data) {
console.log('Query error:' + data);
reject();
});
});
}
}

module.exports = IntentParser;
28 changes: 2 additions & 26 deletions src/test/integration/commands-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,30 +65,6 @@ describe('command/', function() {
return res;
}

it('should return 500 when intent cannot be parsed', async () => {
var piResp = {
'result': {
'action': 'input.unknown',
'parameters': {}
}
};

nock('https://api.api.ai')
.post('/api/query')
.reply(200, piResp);

try {
await chai.request(server)
.post(Constants.COMMANDS_PATH)
.set(...headerAuth(jwt))
.set('Accept', 'application/json')
.send({ text: 'whatever'});
throw new Error('Should have failed to parse');
} catch(err) {
expect(err.response.status).toEqual(406);
}
});

it('should return 400 for POST with no text body', async () => {
setupNock();
try {
Expand Down Expand Up @@ -135,7 +111,7 @@ describe('command/', function() {
expect(res.status).toEqual(201);
});

it('should return 404 when a matching thing is not found', async () => {
it('should return and error when a matching thing is not found', async () => {
var resp = [{
'name': 'Bathroom',
'type': 'onOffSwitch',
Expand Down Expand Up @@ -164,7 +140,7 @@ describe('command/', function() {
.send({ text: 'turn on the bathroom'});
throw new Error('Should have failed to create new thing');
} catch(err) {
expect(err.response.status).toEqual(404);
expect(err);
}
});

Expand Down

0 comments on commit 0443190

Please sign in to comment.