diff --git a/CHANGELOG.md b/CHANGELOG.md index ca26ad9..13155b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * [#352](https://github.com/alexa-js/alexa-app/pull/352): Allow utterance expansion in custom slot type synonyms - [@daanzu](https://github.com/daanzu). * [#361](https://github.com/alexa-js/alexa-app/pull/361): Fix object reference for dialogState in in doc - [@Sephtenen](https://github.com/Sephtenen). * [#364](https://github.com/alexa-js/alexa-app/pull/364): Fix reprompt() to concatenate multiple SSML prompts - [@andrewjhunt](https://github.com/andrewjhunt). +* [#366](https://github.com/alexa-js/alexa-app/pull/358): Add support for including samples in slots and intents for confirmations - [@fabien88](https://github.com/fabien88). * Your contribution here. ### 4.2.2 (April 7, 2018) diff --git a/README.md b/README.md index 5e5f36c..b313b1e 100644 --- a/README.md +++ b/README.md @@ -622,16 +622,27 @@ The alexa-app module makes it easy to define your intent schema and generate man ### Schema Syntax Pass an object with two properties: slots and utterances. +A third (optional) property : prompts. To confirm intent on alexa side with dialog delegate. +slot prompts and intents prompts are working only with askcli command. ```javascript app.intent("sampleIntent", { "slots": { "NAME": "AMAZON.US_FIRST_NAME", - "AGE": "AMAZON.NUMBER" + "AGE": "AMAZON.NUMBER", + "CITY": { + "type": "AMAZON.US_CITY", + "elicitationPrompts": [ + "Oh I forgot to ask you, in which city do you live ?", + ], + "samples": ['I live in {-|CITY}', '{in|at|near|} {-|CITY}'], + "confirmationPrompts": ['Ok so you live in {CITY} right ?'], + } }, "utterances": [ "my {name is|name's} {NAME} and {I am|I'm} {-|AGE}{ years old|}" - ] + ], + "prompts": ['Ok do you confirm your name is {NAME}, your age is {AGE} and that you live in {CITY} ?'], }, function(request, response) { ... } ); @@ -747,7 +758,7 @@ WhatsMyColorIntent tell me what my favorite color is #### Skill Builder Syntax -If you are using the Skill Builder Beta, the `schemas.skillBuilder()` function will generate a single schema JSON string +If you are using the Skill Builder Beta, the `schemas.skillBuilder()` function will generate a single schema JSON string that includes your intents with all of their utterances ```javascript @@ -1056,4 +1067,4 @@ All named apps can be found in the `alexa.apps` object, keyed by name. The value Copyright (c) 2016-2017 Matt Kruse -MIT License, see [LICENSE](LICENSE.md) for details. +MIT License, see [LICENSE](LICENSE.md) for details. \ No newline at end of file diff --git a/index.js b/index.js index 297d19a..ade1454 100644 --- a/index.js +++ b/index.js @@ -312,6 +312,7 @@ alexa.intent = function(name, schema, handler) { this.dialog = (schema && typeof schema.dialog !== "undefined") ? schema.dialog : {}; this.slots = (schema && typeof schema["slots"] !== "undefined") ? schema["slots"] : null; this.utterances = (schema && typeof schema["utterances"] !== "undefined") ? schema["utterances"] : null; + this.prompts = schema && typeof schema["prompts"] !== "undefined" ? schema["prompts"] : null; this.isDelegatedDialog = function() { return this.dialog.type === "delegate"; @@ -684,12 +685,26 @@ alexa.app = function(name) { if (intent.slots && Object.keys(intent.slots).length > 0) { intentSchema["slots"] = []; for (key in intent.slots) { - // It's unclear whether `samples` is actually used for slots, - // but the interaction model will not build without an (empty) array + const slot = intent.slots[key]; + const type = slot.type ? slot.type : slot; + const samples = []; + if (slot.samples) { + slot.samples.forEach(function(sample) { + var list = AlexaUtterances( + sample, + intent.slots, + self.dictionary, + self.exhaustiveUtterances + ); + list.forEach(function(utterance) { + samples.push(utterance); + }); + }); + } intentSchema.slots.push({ - "name": key, - "type": intent.slots[key], - "samples": [] + name: key, + type, + samples }); } } @@ -733,9 +748,13 @@ alexa.app = function(name) { if (intent.slots && Object.keys(intent.slots).length > 0) { intentSchema["slots"] = []; for (key in intent.slots) { + const slot = intent.slots[key]; + const type = slot.type ? slot.type : slot; + const samples = slot.type ? slot.samples : []; intentSchema.slots.push({ "name": key, - "type": intent.slots[key] + "type": type, + "samples": samples }); } } @@ -750,16 +769,92 @@ alexa.app = function(name) { }, askcli: function(invocationName) { var model = skillBuilderSchema(); + var { prompts, dialog } = skillBuilderDialog(); model.invocationName = invocationName || self.invocationName || self.name; var schema = { interactionModel: { - languageModel: model + languageModel: model, + dialog, + prompts } }; return JSON.stringify(schema, null, 3); } }; + var skillBuilderDialog = function() { + var schema = { + dialog: { + intents: [] + }, + prompts: [] + }, + intentName, + intent, + key; + + var creatPrompt = function(promptId, variations) { + return { + id: promptId, + variations: variations.map(prompt => ({ + type: "SSML", + value: "" + prompt + "" + })) + }; + }; + + for (intentName in self.intents) { + intent = self.intents[intentName]; + var intentSchema = { + name: intent.name, + confirmationRequired: false, + prompts: {}, + slots: [] + }; + + if (intent.prompts) { + var intentConfirmPromptId = "Confirm.Intent." + schema.prompts.length; + schema.prompts.push(creatPrompt(intentConfirmPromptId, intent.prompts)); + intentSchema.prompts.confirmation = intentConfirmPromptId; + intentSchema.confirmationRequired = true; + } + + if (intent.slots && Object.keys(intent.slots).length > 0) { + for (key in intent.slots) { + var slot = intent.slots[key]; + var dialogIntentSlot = { + name: key, + type: slot.type || slot, + prompts: {}, + elicitationRequired: false, + confirmationRequired: false + }; + if (slot.elicitationPrompts) { + var elicitPromptId = "Elicit.Slot." + schema.prompts.length; + schema.prompts.push( + creatPrompt(elicitPromptId, slot.elicitationPrompts) + ); + dialogIntentSlot.elicitationRequired = true; + dialogIntentSlot.prompts.elicitation = elicitPromptId; + } + if (slot.confirmationPrompts) { + var confirmPromptId = "Confirm.Slot." + schema.prompts.length; + schema.prompts.push( + creatPrompt(confirmPromptId, slot.confirmationPrompts) + ); + dialogIntentSlot.confirmationRequired = true; + dialogIntentSlot.prompts.confirmation = confirmPromptId; + } + + intentSchema.slots.push(dialogIntentSlot); + } + } + schema.dialog.intents.push(intentSchema); + } + + return schema; + }; + // extract the schema and generate a schema JSON object this.schema = function() { return this.schemas.intent(); diff --git a/test/test_alexa_app_schema.js b/test/test_alexa_app_schema.js index 2ce07e2..fbf23f2 100644 --- a/test/test_alexa_app_schema.js +++ b/test/test_alexa_app_schema.js @@ -46,24 +46,30 @@ describe("Alexa", function() { "intent": "testIntentTwo", "slots": [{ "name": "MyCustomSlotType", + "samples": [], "type": "CUSTOMTYPE" }, { "name": "Tubular", + "samples": [], "type": "AMAZON.LITERAL" }, { "name": "Radical", + "samples": [], "type": "AMAZON.US_STATE" }] }, { "intent": "testIntent", "slots": [{ "name": "AirportCode", + "samples": [], "type": "FAACODES" }, { "name": "Awesome", + "samples": [], "type": "AMAZON.DATE" }, { "name": "Tubular", + "samples": [], "type": "AMAZON.LITERAL" }] }] @@ -122,12 +128,15 @@ describe("Alexa", function() { "intent": "testIntent", "slots": [{ "name": "MyCustomSlotType", + "samples": [], "type": "CUSTOMTYPE" }, { "name": "Tubular", + "samples": [], "type": "AMAZON.LITERAL" }, { "name": "Radical", + "samples": [], "type": "AMAZON.US_STATE" }] }] @@ -165,24 +174,30 @@ describe("Alexa", function() { "intent": "testIntentTwo", "slots": [{ "name": "MyCustomSlotType", + "samples": [], "type": "CUSTOMTYPE" }, { "name": "Tubular", + "samples": [], "type": "AMAZON.LITERAL" }, { "name": "Radical", + "samples": [], "type": "AMAZON.US_STATE" }] }, { "intent": "testIntent", "slots": [{ "name": "AirportCode", + "samples": [], "type": "FAACODES" }, { "name": "Awesome", + "samples": [], "type": "AMAZON.DATE" }, { "name": "Tubular", + "samples": [], "type": "AMAZON.LITERAL" }] }] @@ -441,11 +456,15 @@ describe("Alexa", function() { var subject = JSON.parse(testApp.schemas.askcli()); expect(subject).to.eql({ "interactionModel": { + "dialog": { + "intents": [] + }, "languageModel": { "invocationName": "testApp", "intents": [], "types": [] - } + }, + "prompts": [] } }); }) @@ -460,11 +479,15 @@ describe("Alexa", function() { var subject = JSON.parse(testApp.schemas.askcli()); expect(subject).to.eql({ "interactionModel": { + "dialog": { + "intents": [] + }, "languageModel": { "invocationName": "my cool skill", "intents": [], "types": [] - } + }, + "prompts": [] } }); }) @@ -474,11 +497,15 @@ describe("Alexa", function() { var subject = JSON.parse(testApp.schemas.askcli("my okay skill")); expect(subject).to.eql({ "interactionModel": { + "dialog": { + "intents": [] + }, "languageModel": { "invocationName": "my okay skill", "intents": [], "types": [] - } + }, + "prompts": [] } }); }) @@ -494,6 +521,16 @@ describe("Alexa", function() { var subject = JSON.parse(testApp.schemas.askcli()); expect(subject).to.eql({ "interactionModel": { + "dialog": { + "intents": [ + { + "confirmationRequired": false, + "name": "AMAZON.PauseIntent", + "prompts": {}, + "slots": [], + } + ] + }, "languageModel": { "invocationName": "testApp", "intents": [{ @@ -501,7 +538,8 @@ describe("Alexa", function() { "samples": [] }], "types": [] - } + }, + "prompts": [] } }); }); @@ -518,6 +556,16 @@ describe("Alexa", function() { var subject = JSON.parse(testApp.schemas.askcli()); expect(subject).to.eql({ "interactionModel": { + "dialog": { + "intents": [ + { + "confirmationRequired": false, + "name": "AMAZON.PauseIntent", + "prompts": {}, + "slots": [] + } + ] + }, "languageModel": { "invocationName": "testApp", "intents": [{ @@ -525,7 +573,8 @@ describe("Alexa", function() { "samples": [] }], "types": [] - } + }, + "prompts": [] } }); }); @@ -545,6 +594,31 @@ describe("Alexa", function() { var subject = JSON.parse(testApp.schemas.askcli()); expect(subject).to.eql({ "interactionModel": { + "dialog": { + "intents": [ + { + "confirmationRequired": false, + "name": "testIntent", + "prompts": {}, + "slots": [ + { + "confirmationRequired": false, + "elicitationRequired": false, + "name": "Tubular", + "prompts": {}, + "type": "AMAZON.LITERAL" + }, + { + "confirmationRequired": false, + "elicitationRequired": false, + "name": "Radical", + "prompts": {}, + "type": "AMAZON.US_STATE" + } + ] + } + ] + }, "languageModel": { "invocationName": "testApp", "intents": [{ @@ -561,7 +635,8 @@ describe("Alexa", function() { }] }], "types": [] - } + }, + "prompts": [] } }); }); @@ -578,6 +653,16 @@ describe("Alexa", function() { var subject = JSON.parse(testApp.schemas.askcli()); expect(subject).to.eql({ "interactionModel": { + "dialog": { + "intents": [ + { + "confirmationRequired": false, + "name": "testIntent", + "prompts": {}, + "slots": [] + } + ] + }, "languageModel": { "invocationName": "testApp", "intents": [{ @@ -588,7 +673,8 @@ describe("Alexa", function() { ] }], "types": [] - } + }, + "prompts": [] } }); }); @@ -601,17 +687,28 @@ describe("Alexa", function() { testApp.intent("testIntentTwo", { "slots": { "MyCustomSlotType": "CUSTOMTYPE", - "Tubular": "AMAZON.LITERAL", - "Radical": "AMAZON.US_STATE" + "Tubular": { + "type": "AMAZON.LITERAL", + "samples": ["{-|Tubular}"], + "elicitationPrompts": ["which tubular do you use ?"], + "confirmationPrompts": ["{Tubular} are you sure ?"] + }, + "Radical": "AMAZON.US_STATE", }, }); testApp.intent("testIntent", { "slots": { "AirportCode": "FAACODES", - "Awesome": "AMAZON.DATE", + "Awesome": { + "type":"AMAZON.DATE", + "samples":["I {like to|} do awesome {things|stuff} on {-|Awesome}", "{-|Awesome}"], + "elicitationPrompts": ["When do you do awesome things ?"], + "confirmationPrompts": ["I never though you could do awesome things that date of :{Awesome} ! Are you sure ?"] + }, "Tubular": "AMAZON.LITERAL" }, + prompts: ['are you sure about {AirportCode} and {Tubular} ?'] }); }); @@ -619,6 +716,79 @@ describe("Alexa", function() { var subject = JSON.parse(testApp.schemas.askcli()); expect(subject).to.eql({ "interactionModel": { + "dialog": { + "intents": [ + { + "confirmationRequired": false, + "name": "AMAZON.PauseIntent", + "prompts": {}, + "slots": [], + }, + { + "confirmationRequired": false, + "name": "testIntentTwo", + "prompts": {}, + "slots": [ + { + "confirmationRequired": false, + "elicitationRequired": false, + "name": "MyCustomSlotType", + "prompts": {}, + "type": "CUSTOMTYPE", + }, + { + "confirmationRequired": true, + "elicitationRequired": true, + "name": "Tubular", + "prompts": { + "confirmation": "Confirm.Slot.1", + "elicitation": "Elicit.Slot.0", + }, + "type": "AMAZON.LITERAL", + + }, + { + "confirmationRequired": false, + "elicitationRequired": false, + "name": "Radical", + "prompts": {}, + "type": "AMAZON.US_STATE", + } + ] + }, + { + "confirmationRequired": true, + "name": "testIntent", + "prompts": { "confirmation": "Confirm.Intent.2" }, + "slots": [ + { + "confirmationRequired": false, + "elicitationRequired": false, + "name": "AirportCode", + "prompts": {}, + "type": "FAACODES", + }, + { + "confirmationRequired": true, + "elicitationRequired": true, + "name": "Awesome", + "prompts": { + "confirmation": "Confirm.Slot.4", + "elicitation": "Elicit.Slot.3", + }, + "type": "AMAZON.DATE", + }, + { + "confirmationRequired": false, + "elicitationRequired": false, + "name": "Tubular", + "prompts": {}, + "type": "AMAZON.LITERAL", + } + ] + } + ] + }, "languageModel": { "invocationName": "testApp", "intents": [{ @@ -634,7 +804,9 @@ describe("Alexa", function() { }, { "name": "Tubular", "type": "AMAZON.LITERAL", - "samples": [] + "samples": [ + "{Tubular}" + ], }, { "name": "Radical", "type": "AMAZON.US_STATE", @@ -650,7 +822,13 @@ describe("Alexa", function() { }, { "name": "Awesome", "type": "AMAZON.DATE", - "samples": [] + "samples": [ + "I like to do awesome things on {Awesome}", + "I do awesome things on {Awesome}", + "I like to do awesome stuff on {Awesome}", + "I do awesome stuff on {Awesome}", + "{Awesome}", + ], }, { "name": "Tubular", "type": "AMAZON.LITERAL", @@ -658,7 +836,56 @@ describe("Alexa", function() { }] }], "types": [] - } + }, + "prompts": [ + { + "id": "Elicit.Slot.0", + "variations": [ + { + "type": "SSML", + "value": "which tubular do you use ?", + } + ] + }, + { + "id": "Confirm.Slot.1", + "variations": [ + { + "type": "SSML", + "value": "{Tubular} are you sure ?", + } + ] + }, + { + "id": "Confirm.Intent.2", + "variations": [ + { + "type": "SSML", + "value": "are you sure about {AirportCode} and {Tubular} ?" + } + ] + }, + { + "id": "Elicit.Slot.3", + "variations": [ + { + "type": "SSML", + "value": "When do you do awesome things ?", + } + ] + }, + { + "id": "Confirm.Slot.4", + "variations": [ + { + "type": "SSML", + "value": "I never though you could do awesome things that date of :{Awesome} ! Are you sure ?", + } + ] + } + ] + + } }); }); @@ -679,6 +906,9 @@ describe("Alexa", function() { var subject = JSON.parse(testApp.schemas.askcli()); expect(subject).to.eql({ "interactionModel": { + "dialog": { + "intents": [] + }, "languageModel": { "invocationName": "testApp", "intents": [], @@ -698,7 +928,8 @@ describe("Alexa", function() { } }] }] - } + }, + "prompts": [] } }); }); @@ -719,6 +950,9 @@ describe("Alexa", function() { var subject = JSON.parse(testApp.schemas.askcli()); expect(subject).to.eql({ "interactionModel": { + "dialog": { + "intents": [] + }, "languageModel": { "invocationName": "testApp", "intents": [], @@ -753,7 +987,8 @@ describe("Alexa", function() { } }] }] - } + }, + "prompts": [] } }); });