From 3d7cb6d8a815b4d519b3fa01368a4d9b8c0b5490 Mon Sep 17 00:00:00 2001 From: Fabien Lenoir Date: Tue, 21 Aug 2018 23:44:15 +0200 Subject: [PATCH 1/8] ADD - handling intents and slots prompts confirmation --- index.js | 96 +++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 89 insertions(+), 7 deletions(-) diff --git a/index.js b/index.js index 297d19a..c7035d1 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,13 @@ 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 = slot.type ? slot.samples : []; intentSchema.slots.push({ - "name": key, - "type": intent.slots[key], - "samples": [] + name: key, + type, + samples }); } } @@ -733,9 +735,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 +756,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(); From 3a599d2f41325a2eea0b46420d84c48bdebaf0b8 Mon Sep 17 00:00:00 2001 From: Fabien Lenoir Date: Wed, 22 Aug 2018 00:59:14 +0200 Subject: [PATCH 2/8] UPD - test pass --- test/test_alexa_app_schema.js | 181 ++++++++++++++++++++++++++++++++-- 1 file changed, 171 insertions(+), 10 deletions(-) diff --git a/test/test_alexa_app_schema.js b/test/test_alexa_app_schema.js index 2ce07e2..e2840fe 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": [] } }); }); @@ -619,6 +705,72 @@ 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": false, + "elicitationRequired": false, + "name": "Tubular", + "prompts": {}, + "type": "AMAZON.LITERAL", + }, + { + "confirmationRequired": false, + "elicitationRequired": false, + "name": "Radical", + "prompts": {}, + "type": "AMAZON.US_STATE", + } + ] + }, + { + "confirmationRequired": false, + "name": "testIntent", + "prompts": {}, + "slots": [ + { + "confirmationRequired": false, + "elicitationRequired": false, + "name": "AirportCode", + "prompts": {}, + "type": "FAACODES", + }, + { + "confirmationRequired": false, + "elicitationRequired": false, + "name": "Awesome", + "prompts": {}, + "type": "AMAZON.DATE", + }, + { + "confirmationRequired": false, + "elicitationRequired": false, + "name": "Tubular", + "prompts": {}, + "type": "AMAZON.LITERAL", + } + ] + } + ] + }, "languageModel": { "invocationName": "testApp", "intents": [{ @@ -658,7 +810,8 @@ describe("Alexa", function() { }] }], "types": [] - } + }, + "prompts": [] } }); }); @@ -679,6 +832,9 @@ describe("Alexa", function() { var subject = JSON.parse(testApp.schemas.askcli()); expect(subject).to.eql({ "interactionModel": { + "dialog": { + "intents": [] + }, "languageModel": { "invocationName": "testApp", "intents": [], @@ -698,7 +854,8 @@ describe("Alexa", function() { } }] }] - } + }, + "prompts": [] } }); }); @@ -719,6 +876,9 @@ describe("Alexa", function() { var subject = JSON.parse(testApp.schemas.askcli()); expect(subject).to.eql({ "interactionModel": { + "dialog": { + "intents": [] + }, "languageModel": { "invocationName": "testApp", "intents": [], @@ -753,7 +913,8 @@ describe("Alexa", function() { } }] }] - } + }, + "prompts": [] } }); }); From 958782a436e4c47a220568f2d62482cf7ce8e965 Mon Sep 17 00:00:00 2001 From: Fabien Lenoir Date: Wed, 22 Aug 2018 01:29:24 +0200 Subject: [PATCH 3/8] UPD - tests with intents/slots prompts confirmation --- test/test_alexa_app_schema.js | 99 ++++++++++++++++++++++++++++++----- 1 file changed, 85 insertions(+), 14 deletions(-) diff --git a/test/test_alexa_app_schema.js b/test/test_alexa_app_schema.js index e2840fe..904d029 100644 --- a/test/test_alexa_app_schema.js +++ b/test/test_alexa_app_schema.js @@ -687,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 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} ?'] }); }); @@ -726,11 +737,15 @@ describe("Alexa", function() { "type": "CUSTOMTYPE", }, { - "confirmationRequired": false, - "elicitationRequired": false, + "confirmationRequired": true, + "elicitationRequired": true, "name": "Tubular", - "prompts": {}, + "prompts": { + "confirmation": "Confirm.Slot.1", + "elicitation": "Elicit.Slot.0", + }, "type": "AMAZON.LITERAL", + }, { "confirmationRequired": false, @@ -742,9 +757,9 @@ describe("Alexa", function() { ] }, { - "confirmationRequired": false, + "confirmationRequired": true, "name": "testIntent", - "prompts": {}, + "prompts": { "confirmation": "Confirm.Intent.2" }, "slots": [ { "confirmationRequired": false, @@ -754,10 +769,13 @@ describe("Alexa", function() { "type": "FAACODES", }, { - "confirmationRequired": false, - "elicitationRequired": false, + "confirmationRequired": true, + "elicitationRequired": true, "name": "Awesome", - "prompts": {}, + "prompts": { + "confirmation": "Confirm.Slot.4", + "elicitation": "Elicit.Slot.3", + }, "type": "AMAZON.DATE", }, { @@ -786,7 +804,9 @@ describe("Alexa", function() { }, { "name": "Tubular", "type": "AMAZON.LITERAL", - "samples": [] + "samples": [ + "{Tubular}" + ], }, { "name": "Radical", "type": "AMAZON.US_STATE", @@ -802,7 +822,10 @@ describe("Alexa", function() { }, { "name": "Awesome", "type": "AMAZON.DATE", - "samples": [] + "samples": [ + "I like to do awesome things on {Awesome}", + "{Awesome}", + ], }, { "name": "Tubular", "type": "AMAZON.LITERAL", @@ -811,7 +834,55 @@ describe("Alexa", function() { }], "types": [] }, - "prompts": [] + "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 ?", + } + ] + } + ] + + } }); }); From 45a6d73e007a5bb90472b5a8d1696110d77fc659 Mon Sep 17 00:00:00 2001 From: Fabien Lenoir Date: Wed, 22 Aug 2018 09:56:31 +0200 Subject: [PATCH 4/8] Readme Update --- README.md | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 5e5f36c..de60770 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", + "samples": ['I live in {CITY}', '{CITY}'], + "elicitationPrompts": [ + "Oh I forgot to ask you, in which city do you live ?", + ], + "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 From a24af77c4c563bdf3dfb2af85066c9450aea1805 Mon Sep 17 00:00:00 2001 From: Fabien Lenoir Date: Wed, 22 Aug 2018 09:56:40 +0200 Subject: [PATCH 5/8] Changelog Update --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) 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) From 8ae88183e2911dfb5e3ec6b2d8ef972fab802e33 Mon Sep 17 00:00:00 2001 From: Fabien Lenoir Date: Fri, 24 Aug 2018 17:54:29 +0200 Subject: [PATCH 6/8] UPD - use alexa-uterrance for slot samples syntax --- index.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index c7035d1..ade1454 100644 --- a/index.js +++ b/index.js @@ -687,7 +687,20 @@ alexa.app = function(name) { for (key in intent.slots) { const slot = intent.slots[key]; const type = slot.type ? slot.type : slot; - const samples = slot.type ? slot.samples : []; + 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, From c6444d26edbf01a7d8ad8c9d3a97fb34e526f55c Mon Sep 17 00:00:00 2001 From: Fabien Lenoir Date: Fri, 24 Aug 2018 17:58:44 +0200 Subject: [PATCH 7/8] UPD tests --- test/test_alexa_app_schema.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/test/test_alexa_app_schema.js b/test/test_alexa_app_schema.js index 904d029..fbf23f2 100644 --- a/test/test_alexa_app_schema.js +++ b/test/test_alexa_app_schema.js @@ -688,8 +688,8 @@ describe("Alexa", function() { "slots": { "MyCustomSlotType": "CUSTOMTYPE", "Tubular": { - "type":"AMAZON.LITERAL", - "samples":["{Tubular}"], + "type": "AMAZON.LITERAL", + "samples": ["{-|Tubular}"], "elicitationPrompts": ["which tubular do you use ?"], "confirmationPrompts": ["{Tubular} are you sure ?"] }, @@ -702,7 +702,7 @@ describe("Alexa", function() { "AirportCode": "FAACODES", "Awesome": { "type":"AMAZON.DATE", - "samples":["I like to do awesome things on {Awesome}", "{Awesome}"], + "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 ?"] }, @@ -824,6 +824,9 @@ describe("Alexa", function() { "type": "AMAZON.DATE", "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}", ], }, { From 39abf5e55ad4321646e9321795a857f6e62a1e69 Mon Sep 17 00:00:00 2001 From: Fabien Lenoir Date: Fri, 24 Aug 2018 18:01:08 +0200 Subject: [PATCH 8/8] UPD README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index de60770..b313b1e 100644 --- a/README.md +++ b/README.md @@ -632,10 +632,10 @@ app.intent("sampleIntent", { "AGE": "AMAZON.NUMBER", "CITY": { "type": "AMAZON.US_CITY", - "samples": ['I live in {CITY}', '{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 ?'], } },