Skip to content

Commit

Permalink
Merge pull request #186 from internetarchive/feature/init-alexa-skill
Browse files Browse the repository at this point in the history
Feature/init alexa skill
  • Loading branch information
hyzhak authored Apr 5, 2018
2 parents 662ac7b + 20c5a51 commit 68483c2
Show file tree
Hide file tree
Showing 25 changed files with 513 additions and 75 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# deps

functions/node_modules
node_modules


# dev env
Expand Down
19 changes: 19 additions & 0 deletions functions/index-alexa.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
'use strict';

const {defaultActions} = require('./src/actions');
const alexaHandler = require('./src/platform/alexa/handler');
const setup = require('./src/setup');
const logAppStart = require('./src/utils/logger/log-app-start');

const actionsMap = defaultActions();

logAppStart(actionsMap);

setup();

/**
* Alexa Lambda Endpoint
*
* @type {HttpsFunction}
*/
exports.handler = alexaHandler(actionsMap);
42 changes: 2 additions & 40 deletions functions/index.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,9 @@
'use strict';

const DialogflowApp = require('actions-on-google').DialogflowApp;
const functions = require('firebase-functions');
const bst = require('bespoken-tools');
const dashbot = require('dashbot')(
functions.config().dashbot.key, {
printErrors: false,
}).google;

const {defaultActions} = require('./src/actions');
const dialog = require('./src/dialog');
const assistantHandler = require('./src/platform/assistant/handler');
const setup = require('./src/setup');
const {storeAction} = require('./src/state/actions');
const strings = require('./src/strings');
const {debug, warning} = require('./src/utils/logger')('ia:index');
const logAppStart = require('./src/utils/logger/log-app-start');
const logRequest = require('./src/utils/logger/log-request');

const actionsMap = defaultActions();

Expand All @@ -28,30 +16,4 @@ setup();
*
* @type {HttpsFunction}
*/
exports.assistant = functions.https.onRequest(bst.Logless.capture(functions.config().bespoken.key, function (req, res) {
const app = new DialogflowApp({request: req, response: res});

logRequest(app, req);

storeAction(app, app.getIntent());

// it seems pre-flight request from google assistant,
// we shouldn't handle it by actions
if (!req.body || !app.getIntent()) {
debug('we get empty body. so we ignore request');
app.ask('Internet Archive is here!');
return;
}

if (app.hasSurfaceCapability(app.SurfaceCapabilities.MEDIA_RESPONSE_AUDIO)) {
app.handleRequestAsync(actionsMap)
.catch(err => {
warning(`We missed action: "${app.getIntent()}".
And got an error:`, err);
});
} else {
dialog.tell(app, strings.errors.device.mediaResponse);
}

dashbot.configHandler(app);
}));
exports.assistant = assistantHandler(actionsMap);
20 changes: 20 additions & 0 deletions functions/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions functions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
},
"dependencies": {
"actions-on-google": "^1.10.0",
"alexa-sdk": "^1.0.25",
"bespoken-tools": "^1.2.8",
"dashbot": "^9.4.6",
"debug": "^3.1.0",
Expand Down
32 changes: 32 additions & 0 deletions functions/src/actions/launch-request.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const _ = require('lodash');

// const dialog = require('../dialog');
// const query = require('../state/query');
const welcomeStrings = require('../strings').intents.welcome;

/**
* Handle `LaunchRequest` intent from Alexa
* TODO: merge with welcome action handler
*
* @param app
*/
function handler (app) {
let reprompt = welcomeStrings.reprompt || welcomeStrings.speech;

let speech;
if (app.isFirstTry()) {
speech = '<audio src="https://s3.amazonaws.com/gratefulerrorlogs/CrowdNoise.mp3" />' + _.sample(welcomeStrings.acknowledges) + ' ' + welcomeStrings.speech;
} else {
speech = _.sample(welcomeStrings.acknowledges) + ' ' + welcomeStrings.speech;
}

// TODO:
// query.resetSlots(app);
// dialog.ask(app, Object.assign({}, welcomeStrings, {speech, reprompt}));

app.response.ask(Object.assign({}, welcomeStrings, {speech, reprompt}));
}

module.exports = {
handler,
};
28 changes: 28 additions & 0 deletions functions/src/platform/alexa/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const persistance = require('./persistence');
const response = require('./response');

/**
* Facade of Alexa App
*/
class App {
constructor (alexa) {
this.alexa = alexa;

// define interfaces
this.persist = persistance(alexa);
this.response = response(alexa);
}

/**
* is first skill used time
*
* @returns {boolean}
*/
isFirstTry () {
return true;
}
}

module.exports = {
App,
};
31 changes: 31 additions & 0 deletions functions/src/platform/alexa/handler/handlers-builder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
const {App} = require('../app');
const {debug} = require('../../../utils/logger')('ia:platform:alexa:handler');

const kebabToCamel = require('../../../utils/kebab-to-camel');

/**
* Map actions to alexa handlers
* - alexa intenets should be camel styled and Object type
*
* @param actions {Map}
* @returns {Object}
*/
module.exports = (actions) => {
if (!actions) {
return {};
}

return Array
.from(actions.keys())
.reduce((acc, name) => {
const intent = kebabToCamel(name);
return Object.assign({}, acc, {
[intent]: function () {
debug(`begin handle intent "${intent}"`);
actions.get(name)(new App(this));
debug(`end handle intent "${intent}"`);
this.emit(':responseReady');
},
});
}, {});
};
24 changes: 24 additions & 0 deletions functions/src/platform/alexa/handler/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const Alexa = require('alexa-sdk');
const {debug, error} = require('../../../utils/logger')('ia:platform:alexa:handler');

const handlersBuilder = require('./handlers-builder');

module.exports = (actions) => {
const handlers = handlersBuilder(actions);
debug(`We can handle intents: ${Object.keys(handlers).map(name => `"${name}"`).join(', ')}`);
return (event, context, callback) => {
const alexa = Alexa.handler(event, context, callback);

// TODO: get from process.env
// alexa.appId
// alexa.dynamoDB
alexa.registerHandlers(handlers);

try {
alexa.execute();
} catch (err) {
error('Caught Error:', err);
alexa.emit(':tell', 'Sorry, I\'m experiencing some technical difficulties at the moment. Please try again later.');
}
};
};
8 changes: 8 additions & 0 deletions functions/src/platform/alexa/persistence/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const persistance = require('./session');

/**
* Return persistance interface
*
* @param alexa
*/
module.exports = (alexa) => persistance(alexa);
42 changes: 42 additions & 0 deletions functions/src/platform/alexa/persistence/session.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
const _ = require('lodash');

const {debug} = require('../../../utils/logger')('ia:platform:alexa:persistance:device-level');

/**
* Session level persistance
*
* @param alexa
*/
module.exports = (alexa) => {
debug('create');

const deviceId = alexa.event.context.System.device.deviceId;
debug('deviceId:', deviceId);

if (!alexa) {
throw new Error('parameter alexa should be defined');
}

return {
/**
* Get data
*
* @param name
* @returns {{}}
*/
getData: (name) => {
return _.get(alexa.attributes, [deviceId, name]);
},

/**
* Update data
*
* @param name
* @param value
*/
setData: (name, value) => {
debug(`set attribute ${name} to ${JSON.stringify(value)}`);
_.set(alexa.attributes, [deviceId, name], value);
},
};
};
15 changes: 15 additions & 0 deletions functions/src/platform/alexa/response/ask.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* create alexa.ask wrapper
*
* @param alexa
*/
module.exports = (alexa) =>
/**
* Response with question
*
* @param speech {String}
* @param suggestions {Array}
*/
({speech, suggestions}) => {
alexa.response.speak(speech);
};
11 changes: 11 additions & 0 deletions functions/src/platform/alexa/response/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const askBuilder = require('./ask');

/**
* Return response interface
*
* @param alexa
* @returns {Object}
*/
module.exports = (alexa) => ({
ask: askBuilder(alexa),
});
45 changes: 45 additions & 0 deletions functions/src/platform/assistant/handler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
const DialogflowApp = require('actions-on-google').DialogflowApp;
const functions = require('firebase-functions');
const bst = require('bespoken-tools');
const dashbotBuilder = require('dashbot');

const dialog = require('./../../dialog');
const {storeAction} = require('./../../state/actions');
const strings = require('./../../strings');
const {debug, warning} = require('./../../utils/logger')('ia:index');
const logRequest = require('./../../utils/logger/log-request');

module.exports = (actionsMap) => {
const dashbot = dashbotBuilder(
functions.config().dashbot.key, {
printErrors: false,
}).google;

return functions.https.onRequest(bst.Logless.capture(functions.config().bespoken.key, function (req, res) {
const app = new DialogflowApp({request: req, response: res});

logRequest(app, req);

storeAction(app, app.getIntent());

// it seems pre-flight request from google assistant,
// we shouldn't handle it by actions
if (!req.body || !app.getIntent()) {
debug('we get empty body. so we ignore request');
app.ask('Internet Archive is here!');
return;
}

if (app.hasSurfaceCapability(app.SurfaceCapabilities.MEDIA_RESPONSE_AUDIO)) {
app.handleRequestAsync(actionsMap)
.catch(err => {
warning(`We missed action: "${app.getIntent()}".
And got an error:`, err);
});
} else {
dialog.tell(app, strings.errors.device.mediaResponse);
}

dashbot.configHandler(app);
}));
};
8 changes: 8 additions & 0 deletions functions/src/platform/assistant/persistance/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const persistance = require('./session');

/**
* Return persistance interface
*
* @param assistant app
*/
module.exports = (app) => persistance(app);
Loading

0 comments on commit 68483c2

Please sign in to comment.