├── lambda └── custom │ ├── package.json │ └── index.js ├── README.md └── models └── en-US.json /lambda/custom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-world", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "ask-sdk-core": "^2.1.0-beta.1", 13 | "ask-sdk-model": "^1.4.0-beta.1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Can Fulfill Intent Request 2 | Sample related to: "Help Customers Find the Right Skill When No Invocation Name is Used. You can make it easier for customers to find and engage with your US English skills by adding support for the CanFulfillIntentRequest (beta) interface. Using CanFulfillIntentRequest, your skill provides information about its ability to fulfill a given customer request at runtime. Alexa combines this information with a machine-learning model to choose the right skill to use when a customer makes a request without an invocation name. As a result, customers find the right skill faster, using the search terms they say most naturally. " 3 | 4 | To use this, you'll need the alexa node SDK. Specifically the beta version. Here are instructions 5 | https://github.com/alexa/alexa-skills-kit-sdk-for-nodejs/tree/2.x_public-beta 6 | 7 | 8 | 9 | 10 | [launch blog post](https://developer.amazon.com/blogs/alexa/post/352e9834-0a98-4868-8d94-c2746b794ce9/improve-alexa-skill-discovery-and-name-free-use-of-your-skill-with-canfulfillintentrequest-beta) 11 | 12 | https://developer.amazon.com/docs/custom-skills/name-free-interaction.html 13 | 14 | -------------------------------------------------------------------------------- /models/en-US.json: -------------------------------------------------------------------------------- 1 | { 2 | "interactionModel": { 3 | "languageModel": { 4 | "invocationName": "voice experts", 5 | "intents": [ 6 | { 7 | "name": "AMAZON.CancelIntent", 8 | "samples": [] 9 | }, 10 | { 11 | "name": "AMAZON.HelpIntent", 12 | "samples": [] 13 | }, 14 | { 15 | "name": "AMAZON.StopIntent", 16 | "samples": [] 17 | }, 18 | { 19 | "name": "aboutIntent", 20 | "slots": [ 21 | { 22 | "name": "voiceExpert", 23 | "type": "VOICE_EXPERTS" 24 | } 25 | ], 26 | "samples": [ 27 | "tell me about {voiceExpert}" 28 | ] 29 | } 30 | ], 31 | "types": [ 32 | { 33 | "values": [ 34 | { 35 | "name": { 36 | "value": "Anna Van Brookhoven", 37 | "synonyms": [ 38 | "Anna" 39 | ] 40 | } 41 | }, 42 | { 43 | "name": { 44 | "value": "Rob McCauley", 45 | "synonyms": [ 46 | "Rob", 47 | "McCauley" 48 | ] 49 | } 50 | }, 51 | { 52 | "name": { 53 | "value": "Jedidiah Esposito", 54 | "synonyms": [ 55 | "Jed Esposito", 56 | "Jed" 57 | ] 58 | } 59 | }, 60 | { 61 | "name": { 62 | "value": "Amit Jotwani", 63 | "synonyms": [ 64 | "Amit" 65 | ] 66 | } 67 | }, 68 | { 69 | "name": { 70 | "value": "Azi Frajar", 71 | "synonyms": [ 72 | "Azi" 73 | ] 74 | } 75 | }, 76 | { 77 | "name": { 78 | "value": "Sohan Maheshwar" 79 | } 80 | }, 81 | { 82 | "name": { 83 | "value": "Ankit Kala", 84 | "synonyms": [ 85 | "Ankit" 86 | ] 87 | } 88 | }, 89 | { 90 | "name": { 91 | "value": "Max Amordeluso" 92 | } 93 | }, 94 | { 95 | "name": { 96 | "value": "Andrea Muttoni" 97 | } 98 | }, 99 | { 100 | "name": { 101 | "value": "Rob Pulciani" 102 | } 103 | }, 104 | { 105 | "name": { 106 | "value": "Eric King" 107 | } 108 | }, 109 | { 110 | "name": { 111 | "value": "Kenny Mathers" 112 | } 113 | }, 114 | { 115 | "name": { 116 | "value": "Franklin Lobb", 117 | "synonyms": [ 118 | "Lobb", 119 | "Franklin" 120 | ] 121 | } 122 | }, 123 | { 124 | "name": { 125 | "value": "Akersh Srivastava", 126 | "synonyms": [ 127 | "Akersh", 128 | "the only Akersh" 129 | ] 130 | } 131 | }, 132 | { 133 | "name": { 134 | "value": "Glenn Cameron" 135 | } 136 | }, 137 | { 138 | "name": { 139 | "value": "Mike Maas", 140 | "synonyms": [ 141 | "mike mass", 142 | "mass", 143 | "Maas", 144 | "Mike" 145 | ] 146 | } 147 | }, 148 | { 149 | "name": { 150 | "value": "cassidy Williams", 151 | "synonyms": [ 152 | "cassy doo", 153 | "cassidoo", 154 | "williams", 155 | "Cassidy" 156 | ] 157 | } 158 | }, 159 | { 160 | "name": { 161 | "value": "cami williams", 162 | "synonyms": [ 163 | "c willy c s", 164 | "cwillcs", 165 | "williams", 166 | "cami" 167 | ] 168 | } 169 | }, 170 | { 171 | "name": { 172 | "value": "Justin Jeffress", 173 | "synonyms": [ 174 | "sleepy developer", 175 | "the sleepy developer", 176 | "jeffress", 177 | "justin" 178 | ] 179 | } 180 | }, 181 | { 182 | "name": { 183 | "value": "Jeff Blankenburg", 184 | "synonyms": [ 185 | "Blankenburg", 186 | "Jeff" 187 | ] 188 | } 189 | }, 190 | { 191 | "name": { 192 | "value": "Memo Doring", 193 | "synonyms": [ 194 | "Doring", 195 | "Memo" 196 | ] 197 | } 198 | }, 199 | { 200 | "name": { 201 | "value": "Paul Cutsinger", 202 | "synonyms": [ 203 | "Cutsinger", 204 | "Paul" 205 | ] 206 | } 207 | } 208 | ], 209 | "name": "VOICE_EXPERTS" 210 | } 211 | ] 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /lambda/custom/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable func-names */ 2 | /* eslint-disable no-console */ 3 | /*jshint esversion: 6 */ 4 | 5 | const Alexa = require('ask-sdk-core'); 6 | 7 | const CFIRAboutIntentHandler ={ 8 | canHandle(handlerInput){ 9 | return handlerInput.requestEnvelope.request.type === `CanFulfillIntentRequest` && 10 | handlerInput.requestEnvelope.request.intent.name === 'aboutIntent'; 11 | }, 12 | handle(handlerInput){ 13 | const intentName = handlerInput.requestEnvelope.request.intent.name; 14 | const filledSlots = handlerInput.requestEnvelope.request.intent.slots; 15 | const slotValues = getSlotValues(filledSlots); 16 | console.log ("in CFIR AboutIntentHandler " + JSON.stringify(slotValues)); 17 | 18 | if (slotValues.voiceExpert.isValidated) { 19 | console.log ("in CFIR AboutIntentHandler YES"); 20 | return handlerInput.responseBuilder 21 | .withCanFulfillIntent( 22 | { 23 | "canFulfill": "YES", 24 | "slots":{ 25 | "voiceExpert": { 26 | "canUnderstand": "YES", 27 | "canFulfill": "YES" 28 | } 29 | } 30 | }) 31 | .getResponse(); 32 | } else { 33 | console.log ("in CFIR AboutIntentHandler canFulfill == NO"); 34 | return handlerInput.responseBuilder 35 | .withCanFulfillIntent( 36 | { 37 | "canFulfill": "YES", 38 | "slots":{ 39 | "voiceExpert": { 40 | "canUnderstand": "YES", 41 | "canFulfill": "NO" 42 | } 43 | } 44 | }) 45 | .getResponse(); 46 | } 47 | 48 | return handlerInput.responseBuilder 49 | .speak(speechoutput) 50 | .reprompt(speechoutput) 51 | .getResponse(); 52 | } 53 | }; 54 | 55 | const AboutIntentHandler ={ 56 | canHandle(handlerInput){ 57 | return handlerInput.requestEnvelope.request.type === `IntentRequest` && 58 | handlerInput.requestEnvelope.request.intent.name === 'aboutIntent'; 59 | }, 60 | handle(handlerInput){ 61 | const intentName = handlerInput.requestEnvelope.request.intent.name; 62 | let speechoutput = intentName; //default response 63 | const filledSlots = handlerInput.requestEnvelope.request.intent.slots; 64 | const slotValues = getSlotValues(filledSlots); 65 | console.log ("in AboutIntentHandler " ); 66 | console.log (" slotValues " + JSON.stringify(slotValues)); 67 | console.log (" bio " + voiceExpertBios[slotValues.voiceExpert.value.toLowerCase()]); 68 | 69 | //TODO disambiguate if needed 70 | 71 | if (slotValues.voiceExpert.isValidated) { 72 | speechoutput = slotValues.voiceExpert.value + " " + voiceExpertBios[slotValues.voiceExpert.value.toLowerCase()]; 73 | } 74 | 75 | return handlerInput.responseBuilder 76 | .speak(speechoutput) 77 | .reprompt(speechoutput) 78 | .getResponse(); 79 | } 80 | }; 81 | 82 | 83 | const LaunchHandler ={ 84 | canHandle(handlerInput){ 85 | return handlerInput.requestEnvelope.request.type === `LaunchRequest` || 86 | (handlerInput.requestEnvelope.request.type === `IntentRequest` && 87 | handlerInput.requestEnvelope.request.intent.name === 'AMAZON.HelpIntent'); 88 | }, 89 | handle(handlerInput){ 90 | //const requestType = handlerInput.requestEnvelope.request.type; 91 | //bios = getRandomItem(voiceExpertBios); 92 | //console.log(voiceExpertBios); 93 | 94 | let speech = "We have a long list of voice experts like, "+ Object.keys(getRandomItem(voiceExpertBios))+ " or "+ Object.keys(getRandomItem(voiceExpertBios))+ ". Who would you like to hear about?"; 95 | let repromt = "Would you prefer to hear about "+ Object.keys(getRandomItem(voiceExpertBios))+ " or "+ Object.keys(getRandomItem(voiceExpertBios))+"?"; 96 | return handlerInput.responseBuilder 97 | .speak(speech) 98 | .reprompt(repromt) 99 | .getResponse(); 100 | } 101 | }; 102 | 103 | const StopHandler ={ 104 | canHandle(handlerInput){ 105 | return handlerInput.requestEnvelope.request.type === `IntentRequest` && 106 | (handlerInput.requestEnvelope.request.intent.name === 'AMAZON.StopIntent' || 107 | handlerInput.requestEnvelope.request.intent.name === 'AMAZON.CancelIntent' ) 108 | ; 109 | }, 110 | handle(handlerInput){ 111 | //const requestType = handlerInput.requestEnvelope.request.type; 112 | //bios = getRandomItem(voiceExpertBios); 113 | //console.log(voiceExpertBios); 114 | 115 | let speech = getRandomItem(["goodbye","see you next time", "", "bye", ""]); 116 | return handlerInput.responseBuilder 117 | .speak(speech) 118 | .getResponse(); 119 | } 120 | }; 121 | 122 | const IntentReflectorHandler ={ 123 | canHandle(handlerInput){ 124 | return handlerInput.requestEnvelope.request.type === `IntentRequest`; 125 | }, 126 | handle(handlerInput){ 127 | const intentName = handlerInput.requestEnvelope.request.intent.name; 128 | return handlerInput.responseBuilder 129 | .speak(intentName) 130 | .getResponse(); 131 | } 132 | }; 133 | const SessionEndedReflectorHandler ={ 134 | canHandle(handlerInput){ 135 | return handlerInput.requestEnvelope.request.type === `SessionEndedRequest`; 136 | }, 137 | handle(handlerInput){ 138 | const requestType = handlerInput.requestEnvelope.request.type; 139 | const sessionEndedReason = handlerInput.requestEnvelope.request.reason; 140 | console.log(`~~~~~~~~~~~~~~~~~~~`); 141 | console.log(requestType+ " "+sessionEndedReason); 142 | console.log(`~~~~~~~~~~~~~~~~~~~`); 143 | } 144 | }; 145 | 146 | const RequestHandlerChainErrorHandler = { 147 | canHandle(handlerInput, error) { 148 | console.log(`~~~~~~~~~`); 149 | console.log(error.message); 150 | console.log(`~~~~~~~~~`); 151 | return error.message === `RequestHandlerChain not found!`; 152 | }, 153 | handle(handlerInput, error) { 154 | console.log(`Error handled: ${error.message}`); 155 | 156 | return handlerInput.responseBuilder 157 | .speak('Oops! Looks like you forgot to register a handler again') 158 | .reprompt('Sorry, an error occurred.') 159 | .getResponse(); 160 | }, 161 | }; 162 | 163 | const CFIRErrorHandler = { 164 | canHandle(handlerInput) { 165 | return handlerInput.requestEnvelope.request.type === `CanFulfillIntentRequest`; 166 | }, 167 | handle(handlerInput, error) { 168 | console.log(`CFIR Error handled: ${error.message}`); 169 | 170 | return handlerInput.responseBuilder 171 | .withCanFulfillIntent( 172 | { 173 | "canFulfill": "NO", 174 | "slots":{ 175 | "voiceExpert": { 176 | "canUnderstand": "NO", 177 | "canFulfill": "NO" 178 | } 179 | } 180 | }) 181 | .getResponse(); 182 | }, 183 | }; 184 | 185 | 186 | const ErrorHandler = { 187 | canHandle() { 188 | return true; 189 | }, 190 | handle(handlerInput, error) { 191 | console.log(`Error handled: ${error.message}`); 192 | 193 | return handlerInput.responseBuilder 194 | .speak('Sorry, an error occurred.') 195 | .reprompt('Sorry, an error occurred.') 196 | .getResponse(); 197 | }, 198 | }; 199 | 200 | function getSlotValues(filledSlots) { 201 | const slotValues = {}; 202 | 203 | console.log(`The filled slots: ${JSON.stringify(filledSlots)}`); 204 | Object.keys(filledSlots).forEach((item) => { 205 | const name = filledSlots[item].name; 206 | 207 | if (filledSlots[item] && 208 | filledSlots[item].resolutions && 209 | filledSlots[item].resolutions.resolutionsPerAuthority[0] && 210 | filledSlots[item].resolutions.resolutionsPerAuthority[0].status && 211 | filledSlots[item].resolutions.resolutionsPerAuthority[0].status.code) { 212 | switch (filledSlots[item].resolutions.resolutionsPerAuthority[0].status.code) { 213 | case 'ER_SUCCESS_MATCH': 214 | slotValues[name] = { 215 | synonym: filledSlots[item].value, 216 | value: filledSlots[item].resolutions.resolutionsPerAuthority[0].values[0].value.name, 217 | id: filledSlots[item].resolutions.resolutionsPerAuthority[0].values[0].value.id, 218 | isValidated: true 219 | }; 220 | break; 221 | case 'ER_SUCCESS_NO_MATCH': 222 | slotValues[name] = { 223 | synonym: filledSlots[item].value, 224 | value: filledSlots[item].value, 225 | id: null, 226 | isValidated: false, 227 | }; 228 | break; 229 | default: 230 | break; 231 | } 232 | } else { 233 | slotValues[name] = { 234 | synonym: filledSlots[item].value, 235 | value: filledSlots[item].value, 236 | id: filledSlots[item].id, 237 | isValidated: false 238 | }; 239 | } 240 | }, this); 241 | 242 | return slotValues; 243 | } 244 | 245 | function getRandomItem(arrayOfItems) { 246 | // can take an array, or a dictionary 247 | if (Array.isArray(arrayOfItems)) { 248 | // the argument is an array [] 249 | let i = 0; 250 | i = Math.floor(Math.random() * arrayOfItems.length); 251 | return (arrayOfItems[i]); 252 | } 253 | if (typeof arrayOfItems === 'object') { 254 | // argument is object, treat as dictionary 255 | const result = {}; 256 | const key = getRandomItem(Object.keys(arrayOfItems)); 257 | result[key] = arrayOfItems[key]; 258 | return result; 259 | } 260 | // not an array or object, so just return the input 261 | return arrayOfItems; 262 | } 263 | 264 | const voiceExpertBios = { 265 | "anna van brookhoven":"works on alexa.design slash guide.", 266 | "jedidiah esposito":"is our instructional designer for voice user experience training.", 267 | "amit jotwani":"leads online training.", 268 | "azi frajar":"leads australia evangelism.", 269 | "sohan maheshwar":"leads India evangelism.", 270 | "ankit kala":"is in charge of in person training in India.", 271 | "max amordeluso":"leads the E.U. evangelism.", 272 | "andrea muttoni":"leads U.K evangelism.", 273 | "rob pulciani":"leads the Alexa Skills Kit team.", 274 | "rob mccauley":"leads our in person events.", 275 | "eric king":"leads developer and consumer programs for the Alexa Skills Kit.", 276 | "kenny mathers":"leads in person and online training.", 277 | "franklin lobb":"is in charge of our training content.", 278 | "akersh srivastava":"runs in person training and can be found on twitter as, the only Akersh.", 279 | "glenn cameron":"runs the Alexa Champions program.", 280 | "mike maas":"leads smart home evangelism.", 281 | "cassidy williams":"can be found on twitter as, cassidoo", 282 | "cami williams":"leads gaming evangelism and can be found on twitter as, c willy c.s.", 283 | "justin jeffress":"runs in person training and focuses on dialog management.", 284 | "jeff blankenburg":"leads in skill purchasing evangelism.", 285 | "memo doring":"and his beard are regularly seen on twitch.tv slash Amazon Alexa.", 286 | "paul cutsinger":"can be found on twitter at Paul Cutsinger." 287 | }; 288 | 289 | const skillBuilder = Alexa.SkillBuilders.custom(); 290 | 291 | exports.handler = skillBuilder 292 | .addRequestHandlers( 293 | CFIRAboutIntentHandler, 294 | AboutIntentHandler, 295 | LaunchHandler, 296 | StopHandler, 297 | IntentReflectorHandler 298 | ) 299 | .addErrorHandlers( 300 | CFIRErrorHandler, 301 | RequestHandlerChainErrorHandler, 302 | ErrorHandler 303 | ) 304 | .lambda(); 305 | --------------------------------------------------------------------------------