├── nodemon.json ├── .gitignore ├── lambda ├── custom │ ├── package.json │ ├── package-lock.json │ └── index.ts └── local │ └── index.ts ├── gulpfile.js ├── tsconfig.json ├── models └── en-US.json ├── .ask └── config ├── skill.json ├── package.json ├── LICENSE.md └── README.md /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": [ 3 | "lambda" 4 | ], 5 | "ext": "ts", 6 | "ignore": [ 7 | "lambda/**/*.spec.ts" 8 | ], 9 | "exec": "ts-node ./lambda/local/index.ts" 10 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build directories 2 | dist 3 | 4 | # Dependency directories 5 | /**/node_modules 6 | 7 | # Logs 8 | logs 9 | *.log 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* -------------------------------------------------------------------------------- /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.0.7", 13 | "ask-sdk-model": "^1.3.1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require("gulp"); 2 | var ts = require("gulp-typescript"); 3 | var tsProject = ts.createProject("tsconfig.json"); 4 | 5 | var OUT_DIR = "dist"; 6 | var IN_DIR = "lambda"; 7 | 8 | // compile typescript 9 | gulp.task("compile", function () { 10 | return tsProject.src() 11 | .pipe(tsProject()) 12 | .js.pipe(gulp.dest(OUT_DIR)); 13 | }); 14 | 15 | // copy json files (e.g. localization json) 16 | gulp.task("json", function () { 17 | return gulp.src(IN_DIR + "/**/*.json").pipe(gulp.dest(OUT_DIR)); 18 | }); 19 | 20 | gulp.task("default", ["compile", "json"]); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "lib": [ 6 | "es6" 7 | ], 8 | "moduleResolution": "node", 9 | "rootDir": "lambda", 10 | "outDir": "dist", 11 | "sourceMap": false, 12 | "allowJs": false, 13 | "noImplicitAny": true, 14 | "noUnusedLocals": true, 15 | "noImplicitThis": true, 16 | "strictNullChecks": true, 17 | "noImplicitReturns": true, 18 | "preserveConstEnums": true, 19 | "suppressImplicitAnyIndexErrors": true, 20 | "forceConsistentCasingInFileNames": true, 21 | "strict": true 22 | }, 23 | "exclude": [ 24 | "/**/node_modules", 25 | "lambda/local/" 26 | ] 27 | } -------------------------------------------------------------------------------- /models/en-US.json: -------------------------------------------------------------------------------- 1 | { 2 | "interactionModel": { 3 | "languageModel": { 4 | "invocationName": "greeter", 5 | "intents": [{ 6 | "name": "AMAZON.CancelIntent", 7 | "samples": [] 8 | }, 9 | { 10 | "name": "AMAZON.HelpIntent", 11 | "samples": [] 12 | }, 13 | { 14 | "name": "AMAZON.StopIntent", 15 | "samples": [] 16 | }, 17 | { 18 | "name": "HelloWorldIntent", 19 | "slots": [], 20 | "samples": [ 21 | "hello", 22 | "say hello", 23 | "say hello world" 24 | ] 25 | } 26 | ], 27 | "types": [] 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /lambda/custom/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-world", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "ask-sdk-core": { 8 | "version": "2.0.7", 9 | "resolved": "https://registry.npmjs.org/ask-sdk-core/-/ask-sdk-core-2.0.7.tgz", 10 | "integrity": "sha512-L0YoF7ls0iUoo/WYDYj7uDjAFThYZSDjzF8YvLHIEZyzKVgrNNqxetRorUB+odDoctPWW7RV1xcep8F4p7c1jg==" 11 | }, 12 | "ask-sdk-model": { 13 | "version": "1.3.1", 14 | "resolved": "https://registry.npmjs.org/ask-sdk-model/-/ask-sdk-model-1.3.1.tgz", 15 | "integrity": "sha512-ZDcmJ8sDRAzfIPz5WhRpy8HJ8SheBOyjoeHtYIcoVO6ZQEgxXtZ11GJGg8FhRDfwwIWj5Ma8G6m7OCgo4nuJDA==" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.ask/config: -------------------------------------------------------------------------------- 1 | { 2 | "deploy_settings": { 3 | "default": { 4 | "skill_id": "", 5 | "was_cloned": false, 6 | "merge": { 7 | "manifest": { 8 | "apis": { 9 | "custom": { 10 | "endpoint": { 11 | "uri": "alexa-typescript-hello-world-default" 12 | } 13 | } 14 | } 15 | } 16 | } 17 | }, 18 | "local": { 19 | "skill_id": "", 20 | "was_cloned": false, 21 | "merge": { 22 | "manifest": { 23 | "apis": { 24 | "custom": { 25 | "endpoint": { 26 | "uri": "https://YOUR_URL.ngrok.io", 27 | "sslCertificateType": "Wildcard" 28 | } 29 | } 30 | } 31 | } 32 | } 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /skill.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest": { 3 | "publishingInformation": { 4 | "locales": { 5 | "en-US": { 6 | "summary": "Sample Short Description", 7 | "examplePhrases": [ 8 | "Alexa open hello world", 9 | "Alexa tell hello world hello", 10 | "Alexa ask hello world say hello" 11 | ], 12 | "name": "ask-hello", 13 | "description": "Sample Full Description" 14 | } 15 | }, 16 | "isAvailableWorldwide": true, 17 | "testingInstructions": "Sample Testing Instructions.", 18 | "category": "EDUCATION_AND_REFERENCE", 19 | "distributionCountries": [] 20 | }, 21 | "apis": { 22 | "custom": { 23 | "endpoint": { 24 | "sourceDir": "dist/custom" 25 | } 26 | } 27 | }, 28 | "manifestVersion": "1.0" 29 | } 30 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "alexa-typescript-hello-world", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "clean": "rimraf dist", 8 | "build": "npm run clean && gulp", 9 | "deploy": "npm run build && ask deploy", 10 | "deploy:lambda": "npm run build && ask deploy --target lambda", 11 | "deploy:local": "ask deploy --target skill --profile local", 12 | "start": "nodemon" 13 | }, 14 | "author": "Mihail Cristian Dumitru", 15 | "license": "MIT", 16 | "dependencies": { 17 | "ask-sdk-core": "^2.0.7", 18 | "ask-sdk-model": "^1.3.1" 19 | }, 20 | "devDependencies": { 21 | "@types/node": "^10.5.2", 22 | "@types/express": "^4.16.0", 23 | "express": "^4.16.3", 24 | "gulp": "^3.9.1", 25 | "gulp-typescript": "^4.0.1", 26 | "rimraf": "^2.6.2", 27 | "ts-node": "^5.0.1", 28 | "typescript": "^2.9.2", 29 | "nodemon": "^1.17.5" 30 | } 31 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Mihail Cristian Dumitru 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /lambda/local/index.ts: -------------------------------------------------------------------------------- 1 | import * as express from "express"; 2 | import * as bodyParser from "body-parser"; 3 | import { AddressInfo } from "net"; 4 | import { LambdaHandler } from "ask-sdk-core/dist/skill/factory/BaseSkillFactory"; 5 | import { RequestEnvelope } from "ask-sdk-model"; 6 | 7 | import { handler as helloHandler } from "../custom"; 8 | 9 | function CreateHandler(handler: LambdaHandler): express.RequestHandler { 10 | return (req, res) => { 11 | handler(req.body as RequestEnvelope, null, (err, result) => { 12 | if (err) { 13 | return res.status(500).send(err); 14 | } 15 | return res.status(200).json(result); 16 | }); 17 | }; 18 | } 19 | 20 | // create server 21 | const server = express(); 22 | const listener = server.listen(process.env.port || process.env.PORT || 3980, function () { 23 | const { address, port } = listener.address() as AddressInfo; 24 | console.log('%s listening to %s%s', server.name, address, port); 25 | }); 26 | 27 | // parse json 28 | server.use(bodyParser.json()); 29 | 30 | // connect the lambda functions to http 31 | server.post("/", CreateHandler(helloHandler)); 32 | -------------------------------------------------------------------------------- /lambda/custom/index.ts: -------------------------------------------------------------------------------- 1 | import * as Alexa from "ask-sdk-core"; 2 | 3 | const LaunchRequestHandler: Alexa.RequestHandler = { 4 | canHandle(handlerInput) { 5 | return handlerInput.requestEnvelope.request.type === 'LaunchRequest'; 6 | }, 7 | handle(handlerInput) { 8 | const speechText = 'Welcome to the Alexa Skills Kit, you can say hello!'; 9 | 10 | return handlerInput.responseBuilder 11 | .speak(speechText) 12 | .reprompt(speechText) 13 | .withSimpleCard('Hello World', speechText) 14 | .getResponse(); 15 | } 16 | }; 17 | 18 | const HelloWorldIntentHandler: Alexa.RequestHandler = { 19 | canHandle(handlerInput) { 20 | return handlerInput.requestEnvelope.request.type === 'IntentRequest' 21 | && handlerInput.requestEnvelope.request.intent.name === 'HelloWorldIntent'; 22 | }, 23 | handle(handlerInput) { 24 | const speechText = 'Hello World!'; 25 | 26 | return handlerInput.responseBuilder 27 | .speak(speechText) 28 | .withSimpleCard('Hello World', speechText) 29 | .getResponse(); 30 | } 31 | }; 32 | 33 | const HelpIntentHandler: Alexa.RequestHandler = { 34 | canHandle(handlerInput) { 35 | return handlerInput.requestEnvelope.request.type === 'IntentRequest' 36 | && handlerInput.requestEnvelope.request.intent.name === 'AMAZON.HelpIntent'; 37 | }, 38 | handle(handlerInput) { 39 | const speechText = 'You can say hello to me!'; 40 | 41 | return handlerInput.responseBuilder 42 | .speak(speechText) 43 | .reprompt(speechText) 44 | .withSimpleCard('Hello World', speechText) 45 | .getResponse(); 46 | } 47 | }; 48 | 49 | const CancelAndStopIntentHandler: Alexa.RequestHandler = { 50 | canHandle(handlerInput) { 51 | return handlerInput.requestEnvelope.request.type === 'IntentRequest' 52 | && (handlerInput.requestEnvelope.request.intent.name === 'AMAZON.CancelIntent' 53 | || handlerInput.requestEnvelope.request.intent.name === 'AMAZON.StopIntent'); 54 | }, 55 | handle(handlerInput) { 56 | const speechText = 'Goodbye!'; 57 | 58 | return handlerInput.responseBuilder 59 | .speak(speechText) 60 | .withSimpleCard('Hello World', speechText) 61 | .getResponse(); 62 | } 63 | }; 64 | 65 | const SessionEndedRequestHandler: Alexa.RequestHandler = { 66 | canHandle(handlerInput) { 67 | return handlerInput.requestEnvelope.request.type === 'SessionEndedRequest'; 68 | }, 69 | handle(handlerInput) { 70 | //any cleanup logic goes here 71 | return handlerInput.responseBuilder.getResponse(); 72 | } 73 | }; 74 | 75 | const ErrorHandler: Alexa.ErrorHandler = { 76 | canHandle() { 77 | return true; 78 | }, 79 | handle(handlerInput, error) { 80 | console.log(`Error handled: ${error.message}`); 81 | 82 | return handlerInput.responseBuilder 83 | .speak('Sorry, I can\'t understand the command. Please say again.') 84 | .reprompt('Sorry, I can\'t understand the command. Please say again.') 85 | .getResponse(); 86 | }, 87 | }; 88 | 89 | export const handler = Alexa.SkillBuilders.custom() 90 | .addRequestHandlers( 91 | LaunchRequestHandler, 92 | HelloWorldIntentHandler, 93 | HelpIntentHandler, 94 | CancelAndStopIntentHandler, 95 | SessionEndedRequestHandler, 96 | ) 97 | .addErrorHandlers(ErrorHandler) 98 | .lambda(); 99 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Alexa Skill using AWS Lambda in Typescript 2 | 3 | This is a simple starter project for Alexa skills using Typescript. 4 | 5 | ## Pre-requisites 6 | 7 | - Node.js 8 | - Register for an [AWS Account](https://aws.amazon.com/) 9 | - Register for an [Amazon Developer Account](https://developer.amazon.com/) 10 | - Install and Setup [ASK CLI](https://developer.amazon.com/docs/smapi/quick-start-alexa-skills-kit-command-line-interface.html) 11 | 12 | ## Installation 13 | 14 | - Install the dependencies 15 | 16 | ```bash 17 | $ npm install 18 | ``` 19 | 20 | ## Deployment 21 | 22 | **ASK CLI** will create the skill and the Lambda function for you. The Lambda function will be created in `us-east-1 (Northern Virginia)` by default. 23 | 24 | 1. Navigate to the project's root directory. you should see a file named 'skill.json' there. 25 | 26 | 2. Deploy the skill and the Lambda function in one step by running the following command: 27 | 28 | ```bash 29 | $ ask deploy 30 | ``` 31 | 32 | ## Local development 33 | 34 | During development, it's a pain to always have to deploy your Lambda function to see your changes. 35 | 36 | There is a better way: you can connect Alexa to your local environment instead of Lambda, without having to make any modifications to your Lambda functions. This will save you the time of always making deployment, and your changes will be applied instantly. 37 | 38 | 1. First, we will need an HTTPS endpoint. You can use [ngrok](https://ngrok.com/) for this (it's free), or any other similar tools. 39 | 40 | ```bash 41 | $ ngrok http 3980 42 | ``` 43 | 44 | This will give you an HTTPS endpoint which will proxy all requests to your local server (something like `https://84f7599f.ngrok.io`). 45 | 46 | 2. Open `.ask/config` and replace the following line 47 | 48 | ```json 49 | "uri": "https://YOUR_URL.ngrok.io", 50 | ``` 51 | 52 | with the HTTPS endpoint from `ngrok`, e.g.: 53 | 54 | ```json 55 | "uri": "https://84f7599f.ngrok.io", 56 | ``` 57 | 58 | 3. Now we need to update the skill endpoint to our HTTPS endpoint 59 | 60 | ```bash 61 | $ ask deploy --target skill --profile local 62 | ``` 63 | 64 | You can also do this manually if you want: 65 | 66 | - Go to [your skill's dashboard](https://developer.amazon.com/alexa/console) 67 | - Select `Endpoint` 68 | - Select `HTTPS` 69 | - Enter your HTTPS url 70 | - For the `SSL certificate type` make sure you select `My development endpoint is a sub-domain of a domain that has a wildcard certificate from a certificate authority` 71 | 72 | 4. Now we need to start the local HTTP server 73 | 74 | ```bash 75 | $ npm start 76 | ``` 77 | 78 | This will start a server using `express` (check `lambda/local/index.ts`) and `nodemon`, which will restart the process when you make any code changes so that you don't have to. 79 | 80 | ## Developer tasks 81 | 82 | | Command | Description | 83 | | --- | --- | 84 | | `clean` | Deletes the `dist` folder | 85 | | `build` | Builds the lambda function and exports it to the `dist` folder | 86 | | `deploy` | Builds the lambda function and deploys EVERYTHING (skill, model, lambda) | 87 | | `deploy:lambda` | Builds the lambda function and deploys it (just the lambda function) | 88 | | `deploy:local` | Deploys the skill details for the local profile, which will update the HTTPS endpoint | 89 | | `start` | Starts the local `express` server using `nodemon` for local development | 90 | 91 | To see the actual commands, check `package.json`. 92 | 93 | Also check the [ASK CLI Command Reference](https://developer.amazon.com/docs/smapi/ask-cli-command-reference.html) for more details on using the `ASK CLI`. 94 | 95 | ## Testing 96 | 97 | Taken from [the official hello world project](https://github.com/alexa/skill-sample-nodejs-hello-world/blob/master/instructions/7-cli.md#testing). 98 | 99 | 1. To test, you need to login to Alexa Developer Console, and **enable the "Test" switch on your skill from the "Test" Tab**. 100 | 101 | 2. Simulate verbal interaction with your skill through the command line (this might take a few moments) using the following example: 102 | 103 | ```bash 104 | $ ask simulate -l en-US -t "open greeter" 105 | 106 | ✓ Simulation created for simulation id: 4a7a9ed8-94b2-40c0-b3bd-fb63d9887fa7 107 | ◡ Waiting for simulation response{ 108 | "status": "SUCCESSFUL", 109 | ... 110 | ``` 111 | 112 | 3. Once the "Test" switch is enabled, your skill can be tested on devices associated with the developer account as well. Speak to Alexa from any enabled device, from your browser at [echosim.io](https://echosim.io/welcome), or through your Amazon Mobile App and say : 113 | 114 | ```text 115 | Alexa, start hello world 116 | ``` 117 | 118 | ## Customization 119 | 120 | Taken from [the official hello world project](https://github.com/alexa/skill-sample-nodejs-hello-world/blob/master/instructions/7-cli.md#customization). 121 | 122 | 1. ```./skill.json``` 123 | 124 | Change the skill name, example phrase, icons, testing instructions etc ... 125 | 126 | Remember than many information are locale-specific and must be changed for each locale (e.g. en-US, en-GB, de-DE, etc.) 127 | 128 | See the Skill [Manifest Documentation](https://developer.amazon.com/docs/smapi/skill-manifest.html) for more information. 129 | 130 | 2. ```./lambda/custom/index.ts``` 131 | 132 | Modify messages, and data from the source code to customize the skill. 133 | 134 | 3. ```./models/*.json``` 135 | 136 | Change the model definition to replace the invocation name and the sample phrase for each intent. Repeat the operation for each locale you are planning to support. 137 | 138 | 4. Remember to re-deploy your skill and Lambda function for your changes to take effect. 139 | 140 | ```bash 141 | $ ask deploy 142 | ``` 143 | 144 | ## License 145 | 146 | Open sourced under the [MIT license](./LICENSE.md). --------------------------------------------------------------------------------