-
Notifications
You must be signed in to change notification settings - Fork 54
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
29 changed files
with
3,339 additions
and
21 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
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,62 @@ | ||
const R = require('ramda'); | ||
|
||
module.exports = { | ||
validateDialogArgs | ||
}; | ||
|
||
/** | ||
* Validates if a skill is enabled for simulation. Calls Skill Management apis (SMAPI) to achieve this. | ||
* @param {*} dialogMode encapsulates configuration required validate skill information | ||
* @param {*} callback | ||
*/ | ||
function validateDialogArgs(dialogMode, callback) { | ||
const { smapiClient, skillId, stage, locale } = dialogMode; | ||
|
||
smapiClient.skill.manifest.getManifest(skillId, stage, (err, response) => { | ||
if (err) { | ||
return callback(err); | ||
} | ||
if (response.statusCode !== 200) { | ||
return callback(smapiErrorMsg('get-manifest', response)); | ||
} | ||
const apis = R.view(R.lensPath(['body', 'manifest', 'apis']), response); | ||
if (!apis) { | ||
return callback('Ensure "manifest.apis" object exists in the skill manifest.'); | ||
} | ||
|
||
const apisKeys = Object.keys(apis); | ||
if (!apisKeys || apisKeys.length !== 1) { | ||
return callback('Dialog command only supports custom skill type.'); | ||
} | ||
|
||
if (apisKeys[0] !== 'custom') { | ||
return callback(`Dialog command only supports custom skill type, but current skill is a "${apisKeys[0]}" type.`); | ||
} | ||
|
||
const locales = R.view(R.lensPath(['body', 'manifest', 'publishingInformation', 'locales']), response); | ||
if (!locales) { | ||
return callback('Ensure the "manifest.publishingInformation.locales" exists in the skill manifest before simulating your skill.'); | ||
} | ||
|
||
if (!R.view(R.lensProp(locale), locales)) { | ||
return callback( | ||
`Locale ${locale} was not found for your skill. ` | ||
+ 'Ensure the locale you want to simulate exists in your publishingInformation.' | ||
); | ||
} | ||
|
||
smapiClient.skill.getSkillEnablement(skillId, stage, (enableErr, enableResponse) => { | ||
if (enableErr) { | ||
return callback(enableErr); | ||
} | ||
if (enableResponse.statusCode > 300) { | ||
return callback(smapiErrorMsg('get-skill-enablement', enableResponse)); | ||
} | ||
callback(); | ||
}); | ||
}); | ||
} | ||
|
||
function smapiErrorMsg(operation, res) { | ||
return `SMAPI ${operation} request error: ${res.statusCode} - ${res.body.message}`; | ||
} |
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,128 @@ | ||
const path = require('path'); | ||
|
||
const SmapiClient = require('@src/clients/smapi-client'); | ||
const { AbstractCommand } = require('@src/commands/abstract-command'); | ||
const optionModel = require('@src/commands/option-model'); | ||
const DialogReplayFile = require('@src/model/dialog-replay-file'); | ||
const ResourcesConfig = require('@src/model/resources-config'); | ||
const CONSTANTS = require('@src/utils/constants'); | ||
const profileHelper = require('@src/utils/profile-helper'); | ||
const Messenger = require('@src/view/messenger'); | ||
const SpinnerView = require('@src/view/spinner-view'); | ||
const stringUtils = require('@src/utils/string-utils'); | ||
|
||
const InteractiveMode = require('./interactive-mode'); | ||
const ReplayMode = require('./replay-mode'); | ||
|
||
const helper = require('./helper'); | ||
|
||
class DialogCommand extends AbstractCommand { | ||
name() { | ||
return 'dialog'; | ||
} | ||
|
||
description() { | ||
return 'simulate your skill via an interactive dialog with Alexa'; | ||
} | ||
|
||
requiredOptions() { | ||
return []; | ||
} | ||
|
||
optionalOptions() { | ||
return ['skill-id', 'locale', 'stage', 'replay', 'profile', 'debug']; | ||
} | ||
|
||
handle(cmd, cb) { | ||
let dialogMode; | ||
try { | ||
dialogMode = this._dialogModeFactory(this._getDialogConfig(cmd)); | ||
} catch (err) { | ||
Messenger.getInstance().error(err); | ||
return cb(err); | ||
} | ||
|
||
const spinner = new SpinnerView(); | ||
spinner.start('Checking if skill is ready to simulate...'); | ||
helper.validateDialogArgs(dialogMode, (dialogArgsValidationError) => { | ||
if (dialogArgsValidationError) { | ||
spinner.terminate(SpinnerView.TERMINATE_STYLE.FAIL, 'Failed to validate command options'); | ||
Messenger.getInstance().error(dialogArgsValidationError); | ||
return cb(dialogArgsValidationError); | ||
} | ||
spinner.terminate(); | ||
dialogMode.start((controllerError) => { | ||
if (controllerError) { | ||
Messenger.getInstance().error(controllerError); | ||
return cb(controllerError); | ||
} | ||
cb(); | ||
}); | ||
}); | ||
} | ||
|
||
/** | ||
* Function processes dialog arguments and returns a consolidated object. | ||
* @param {Object} cmd encapsulates arguments provided to the dialog command. | ||
* @return { skillId, locale, stage, profile, debug, replayFile, smapiClient, userInputs <for replay mode> } | ||
*/ | ||
_getDialogConfig(cmd) { | ||
const debug = Boolean(cmd.debug); | ||
let { skillId, locale, stage, profile } = cmd; | ||
profile = profileHelper.runtimeProfile(profile); | ||
stage = stage || CONSTANTS.SKILL.STAGE.DEVELOPMENT; | ||
let userInputs; | ||
if (cmd.replay) { | ||
const dialogReplayConfig = new DialogReplayFile(cmd.replay); | ||
skillId = dialogReplayConfig.getSkillId(); | ||
if (!stringUtils.isNonBlankString(skillId)) { | ||
throw 'Replay file must contain skillId'; | ||
} | ||
locale = dialogReplayConfig.getLocale(); | ||
if (!stringUtils.isNonBlankString(locale)) { | ||
throw 'Replay file must contain locale'; | ||
} | ||
userInputs = this._validateUserInputs(dialogReplayConfig.getUserInput()); | ||
} else { | ||
if (!stringUtils.isNonBlankString(skillId)) { | ||
try { | ||
new ResourcesConfig(path.join(process.cwd(), CONSTANTS.FILE_PATH.ASK_RESOURCES_JSON_CONFIG)); | ||
skillId = ResourcesConfig.getInstance().getSkillId(profile); | ||
} catch (err) { | ||
throw 'Failed to read project resource file.'; | ||
} | ||
if (!stringUtils.isNonBlankString(skillId)) { | ||
throw `Failed to obtain skill-id from project resource file ${CONSTANTS.FILE_PATH.ASK_RESOURCES_JSON_CONFIG}`; | ||
} | ||
} | ||
locale = locale || process.env.ASK_DEFAULT_DEVICE_LOCALE; | ||
if (!stringUtils.isNonBlankString(locale)) { | ||
throw 'Locale has not been specified.'; | ||
} | ||
} | ||
const smapiClient = new SmapiClient({ profile, doDebug: debug }); | ||
return { skillId, locale, stage, profile, debug, replay: cmd.replay, smapiClient, userInputs }; | ||
} | ||
|
||
_dialogModeFactory(config) { | ||
if (config.replay) { | ||
return new ReplayMode(config); | ||
} | ||
return new InteractiveMode(config); | ||
} | ||
|
||
_validateUserInputs(userInputs) { | ||
const validatedInputs = []; | ||
userInputs.forEach((input) => { | ||
const trimmedInput = input.trim(); | ||
if (!stringUtils.isNonBlankString(trimmedInput)) { | ||
throw "Replay file's userInput cannot contain empty string."; | ||
} | ||
validatedInputs.push(trimmedInput); | ||
}); | ||
return validatedInputs; | ||
} | ||
} | ||
|
||
module.exports = DialogCommand; | ||
module.exports.createCommand = new DialogCommand(optionModel).createCommand(); |
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,35 @@ | ||
const chalk = require('chalk'); | ||
|
||
const DialogReplView = require('@src/view/dialog-repl-view'); | ||
const DialogController = require('@src/controllers/dialog-controller'); | ||
|
||
module.exports = class InteractiveMode extends DialogController { | ||
constructor(config) { | ||
super(config); | ||
this.header = config.header || 'Welcome to ASK Dialog\n' | ||
+ 'In interactive mode, type your utterance text onto the console and hit enter\n' | ||
+ 'Alexa will then evaluate your input and give a response!'; | ||
} | ||
|
||
start(callback) { | ||
let interactiveReplView; | ||
try { | ||
interactiveReplView = new DialogReplView({ | ||
prompt: chalk.yellow.bold('User > '), | ||
header: this.header, | ||
footer: 'Goodbye!', | ||
evalFunc: (input, replCallback) => { | ||
this.evaluateUtterance(input, interactiveReplView, replCallback); | ||
}, | ||
}); | ||
} catch (error) { | ||
return callback(error); | ||
} | ||
|
||
this.setupSpecialCommands(interactiveReplView, (error) => { | ||
if (error) { | ||
return callback(error); | ||
} | ||
}); | ||
} | ||
}; |
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,62 @@ | ||
const chalk = require('chalk'); | ||
const { Readable } = require('stream'); | ||
|
||
const DialogController = require('@src/controllers/dialog-controller'); | ||
const DialogReplView = require('@src/view/dialog-repl-view'); | ||
const InteractiveMode = require('./interactive-mode'); | ||
|
||
module.exports = class ReplayMode extends DialogController { | ||
constructor(config) { | ||
super(config); | ||
this.config = config; | ||
this.userInputs = config.userInputs; | ||
this.replay = config.replay; | ||
} | ||
|
||
start(callback) { | ||
const replayInputStream = new Readable({ read() {} }); | ||
let replayReplView; | ||
try { | ||
replayReplView = new DialogReplView({ | ||
prompt: chalk.yellow.bold('User > '), | ||
header: this._getHeader(), | ||
footer: 'Goodbye!', | ||
inputStream: replayInputStream, | ||
evalFunc: (replayInput, replayCallback) => { | ||
this._evaluateInput(replayInput, replayReplView, replayInputStream, replayCallback, callback); | ||
} | ||
}); | ||
} catch (error) { | ||
return callback(error); | ||
} | ||
|
||
this.setupSpecialCommands(replayReplView, (error) => { | ||
if (error) { | ||
return callback(error); | ||
} | ||
}); | ||
replayInputStream.push(`${this.userInputs.shift()}\n`, 'utf8'); | ||
} | ||
|
||
_getHeader() { | ||
return 'Welcome to ASK Dialog\n' | ||
+ `Replaying a multi turn conversation with Alexa from ${this.replay}\n` | ||
+ 'Alexa will then evaluate your input and give a response!'; | ||
} | ||
|
||
_evaluateInput(replayInput, replayReplView, replayInputStream, replayCallback, callback) { | ||
this.evaluateUtterance(replayInput, replayReplView, () => { | ||
if (this.userInputs.length > 0) { | ||
replayCallback(); | ||
replayInputStream.push(`${this.userInputs.shift()}\n`, 'utf8'); | ||
} else { | ||
replayReplView.clearSpecialCommands(); | ||
replayReplView.close(); | ||
this.config.header = 'Switching to interactive dialog.\n' | ||
+ 'To automatically quit after replay, append \'.quit\' to the userInput of your replay file.'; | ||
const interactiveReplView = new InteractiveMode(this.config); | ||
interactiveReplView.start(callback); | ||
} | ||
}); | ||
} | ||
}; |
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
Oops, something went wrong.