1 | ;
|
2 | const _ = require('lodash');
|
3 | const nodeDebug = require('debug');
|
4 | const glob = require('glob');
|
5 | const path = require('path');
|
6 |
|
7 | const handleRequest = require('./handle-request');
|
8 | const createIntent = require('./create-intent');
|
9 | const createCustomSlot = require('./create-custom-slot');
|
10 | const generateSpeechAssets = require('./generate-speech-assets');
|
11 | const saveSpeechAssets = require('./save-speech-assets');
|
12 | const builtInIntentsMap = require('./built-in-intents-map');
|
13 | const createServer = require('./create-server');
|
14 | const parseError = require('./error-handler').parseError;
|
15 |
|
16 | const builtInIntentsList = _.keys(builtInIntentsMap).join(', ');
|
17 | const debug = nodeDebug('alexia:debug');
|
18 |
|
19 | /**
|
20 | * Create new app
|
21 | * @param {string} name - App name
|
22 | * @param {Object} [options] - Additional app options
|
23 | * @param {string} [options.version] - App version
|
24 | * @param {string[]} [options.ids] - Array of app ids. Only requests with supported app ids will be handled
|
25 | */
|
26 | module.exports = (name, options) => {
|
27 | let app = {
|
28 | name: name,
|
29 | options: options,
|
30 | intents: {},
|
31 | customSlots: {},
|
32 | actions: []
|
33 | };
|
34 |
|
35 | let handlers = {
|
36 | onStart: () => 'Welcome',
|
37 | onEnd: () => 'Bye',
|
38 | defaultActionFail: () => 'Sorry, your command is invalid'
|
39 | };
|
40 |
|
41 | /**
|
42 | * Sets handler to be called on application start
|
43 | * @param {function} handler - Handler to be called when app is started without intent
|
44 | */
|
45 | app.onStart = (handler) => {
|
46 | handlers.onStart = handler;
|
47 | };
|
48 |
|
49 | /**
|
50 | * Sets handler to be called on application end
|
51 | * @param {function} handler - Handler to be called when application is unexpectedly terminated
|
52 | */
|
53 | app.onEnd = (handler) => {
|
54 | handlers.onEnd = handler;
|
55 | };
|
56 |
|
57 | /**
|
58 | * Sets handler to be called on default action fail
|
59 | * @param {function} handler - Default handler to be called when action can not be invoked
|
60 | */
|
61 | app.defaultActionFail = (handler) => {
|
62 | handlers.defaultActionFail = handler;
|
63 | };
|
64 |
|
65 | /**
|
66 | * Creates intent
|
67 | * @param {string} name - Intent name. Should not be equal to built-in intent name. It is possible to use this function to create built-in intents but utterances are required argument and you need to specify full built-in intent name f.e. `AMAZON.StopIntent`. See `{@link app.builtInIntent}`. If not specified (null, undefined or empty string), automatically generated intent name is used but we recommend to name each intent
|
68 | * @param {(string|string[])} richUtterances - one or more utterances. Utterances contain utterance description with slots types. Example: `My age is {age:Number}`
|
69 | * @param {function} handler - Function to be called when intent is invoked
|
70 | */
|
71 | app.intent = (name, richUtterances, handler) => {
|
72 | const intent = createIntent(app.intents, name, richUtterances, handler);
|
73 | app.intents[intent.name] = intent;
|
74 |
|
75 | return intent;
|
76 | };
|
77 |
|
78 | /**
|
79 | * Creates built-int intent.
|
80 | * Essentialy the same as `intent` but with optional `utterances` since we need to specify each built-in intent has its own set of default utterances you are not required to extend
|
81 | * @param {string} name - Built-in Intent name. Must be one of: `cancel`, `help`, `next`, `no`, `pause`, `previous`, `repeat`, `resume`, `startOver`, `stop`, `yes`
|
82 | * @param {(string|string[]|function)} [utterances] - one or more utterances without slots. Could be ommited and handler could be 2nd parameter instead
|
83 | * @param {function} handler - Function to be called when intent is invoked
|
84 | */
|
85 | app.builtInIntent = (name, utterances, handler) => {
|
86 | // Validate built-in intent name
|
87 | if (!builtInIntentsMap[name]) {
|
88 | const e = parseError(new Error(`Built-in Intent name ${name} is invalid. Please use one of: ${builtInIntentsList}`));
|
89 | throw e;
|
90 | }
|
91 |
|
92 | // Shift ommited arguments (utternaces are optional)
|
93 | if (!handler) {
|
94 | handler = utterances;
|
95 | utterances = undefined;
|
96 | }
|
97 |
|
98 | app.intent(name, utterances, handler);
|
99 | };
|
100 |
|
101 | /**
|
102 | * Handles request and calls done when finished
|
103 | * @param {Object} data - Request JSON to be handled.
|
104 | * @param {Function} done - Callback to be called when request is handled. Callback is called with one argument - response JSON
|
105 | */
|
106 | app.handle = (data, done) => {
|
107 | handleRequest(app, data, handlers, done);
|
108 | };
|
109 |
|
110 | /**
|
111 | * Creates custom slot
|
112 | * @param {string} name - Name of the custom slot
|
113 | * @param {string[]} samples - Array of custom slot samples
|
114 | */
|
115 | app.customSlot = (name, samples) => {
|
116 | const customSlot = createCustomSlot(app.customSlots, name, samples);
|
117 | app.customSlots[name] = customSlot;
|
118 | };
|
119 |
|
120 | /**
|
121 | * Creates action
|
122 | * @param {string} action - Action object
|
123 | * @param {string} action.from - Name of the intent to allow transition from
|
124 | * @param {string} action.to - Name of th eintent to allow transition to
|
125 | * @param {function} action.if - Function returning boolean whether this transition should be handled.
|
126 | * @param {function} action.fail - Handler to be called if `action.if` returned `false`
|
127 | */
|
128 | app.action = (action) => {
|
129 | app.actions.push({
|
130 | from: typeof (action.from) === 'string' ? action.from : action.from.name,
|
131 | to: typeof (action.to) === 'string' ? action.to : action.to.name,
|
132 | if: action.if,
|
133 | fail: action.fail
|
134 | });
|
135 | };
|
136 |
|
137 | /**
|
138 | * Generate speech assets object: {schema, utterances, customSlots}
|
139 | */
|
140 | app.speechAssets = () => {
|
141 | return generateSpeechAssets(app);
|
142 | };
|
143 |
|
144 | /**
|
145 | * Save speech assets to their respective files: intentSchema.json, utterances.txt, customSlots.txt
|
146 | * @param {string} [directory] - directory folder name, defaults to '/speechAssets'
|
147 | */
|
148 | app.saveSpeechAssets = (directory) => {
|
149 | const dir = directory || 'speechAssets';
|
150 | const assets = generateSpeechAssets(app);
|
151 | saveSpeechAssets(assets, dir);
|
152 | };
|
153 |
|
154 | /**
|
155 | * Creates Hapi server with one route that handles all request for this app. Server must be started using `server.start()`
|
156 | * @param {number} [options] - Server options
|
157 | * @property {number} [options.path] - Path to run server route on. Defaults to `/`
|
158 | * @property {number} [options.port] - Port to run server on. If not specified then `process.env.PORT` is used. Defaults to `8888`
|
159 | * @returns {object} server
|
160 | */
|
161 | app.createServer = (options) => {
|
162 | return createServer(app, options);
|
163 | };
|
164 |
|
165 | /**
|
166 | * Registers all intents matching specified pattern
|
167 | * @param {string} pattern - Pattern used for intent matching. Must be relative to project root. Example: 'src/intents/**.js'
|
168 | */
|
169 | app.registerIntents = (pattern) => {
|
170 |
|
171 | const files = glob.sync(pattern);
|
172 |
|
173 | if (files.length === 0) {
|
174 | console.warn(`No intents found using pattern '${pattern}'`);
|
175 | }
|
176 |
|
177 | files.forEach(intentFile => {
|
178 | debug(`Registering intent '${intentFile}'`);
|
179 | require(path.relative(__dirname, intentFile))(app);
|
180 | });
|
181 |
|
182 | };
|
183 |
|
184 | return app;
|
185 | };
|