├── .commitlintrc.json ├── .eslintignore ├── .eslintrc.json ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ └── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── unit-test.yml ├── .gitignore ├── .npmignore ├── .prettierrc ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE ├── README.md ├── demo ├── Common │ ├── index.ts │ └── src │ │ └── DemoRootControl.ts ├── ComponentModeAPL │ └── src │ │ ├── buildInteractionModel.ts │ │ └── index.ts ├── ListControl │ └── YesNoMaybe │ │ ├── src │ │ ├── buildInteractionModel.ts │ │ ├── index.ts │ │ └── interactionModelTypes.ts │ │ └── test │ │ └── listDemo1.spec.ts ├── MultiValueListControl │ ├── src │ │ ├── buildInteractionModel.ts │ │ └── index.ts │ └── test │ │ └── multiValueListDemo.spec.ts ├── NumberControl │ └── NumberDemo │ │ ├── src │ │ ├── buildInteractionModel.ts │ │ └── index.ts │ │ └── test │ │ └── numberDemo.spec.ts ├── QuestionnaireControl │ ├── src │ │ ├── buildInteractionModel.ts │ │ └── index.ts │ └── test │ │ └── questionnaireDemo.spec.ts └── TwoLists │ ├── src │ ├── buildInteractionModel.ts │ └── index.ts │ └── test │ └── multiListDemo1.spec.ts ├── doc ├── img │ ├── fruitShopControlTree.png │ ├── handlingChain.png │ └── requestHandling.png └── userGuide.md ├── ide └── vscode │ ├── launch.json │ └── tasks.json ├── package-lock.json ├── package.json ├── src ├── commonControls │ ├── DateControl.ts │ ├── LanguageStrings.ts │ ├── ValueControl.ts │ ├── dateRangeControl │ │ ├── DateHelper.ts │ │ ├── DateRangeControl.ts │ │ └── DateRangeNLUHelper.ts │ ├── listControl │ │ ├── ListControl.ts │ │ └── ListControlAPL.ts │ ├── multiValueListControl │ │ ├── MultiValueListControl.ts │ │ └── MultiValueListControlAPL.ts │ ├── numberControl │ │ ├── NumberControl.ts │ │ ├── NumberControlAPL.ts │ │ └── NumberControlBuiltIns.ts │ └── questionnaireControl │ │ ├── QuestionnaireControl.ts │ │ ├── QuestionnaireControlBuiltIns.ts │ │ ├── QuestionnaireControlStructs.ts │ │ └── QuestionnaireControlSystemActs.ts ├── constants │ └── Strings.ts ├── controls │ ├── ComponentModeControlManager.ts │ ├── ContainerControl.ts │ ├── Control.ts │ ├── ControlInput.ts │ ├── ControlManager.ts │ ├── ControlResult.ts │ ├── ControlServices.ts │ ├── DynamicContainerControl.ts │ ├── Validation.ts │ ├── interfaces │ │ ├── IContainerControl.ts │ │ ├── IControl.ts │ │ ├── IControlInput.ts │ │ ├── IControlManager.ts │ │ ├── IControlResult.ts │ │ ├── IControlResultBuilder.ts │ │ ├── ILogger.ts │ │ └── ILoggerFactory.ts │ └── mixins │ │ ├── ControlStateDiagramming.ts │ │ └── InteractionModelContributor.ts ├── index.ts ├── intents │ ├── AmazonBuiltInIntent.ts │ ├── AmazonBuiltInSlotType.ts │ ├── BaseControlIntent.ts │ ├── ConjunctionControlIntent.ts │ ├── DateRangeControlIntent.ts │ ├── GeneralControlIntent.ts │ ├── OrdinalControlIntent.ts │ └── ValueControlIntent.ts ├── interactionModelGeneration │ ├── ControlInteractionModelGenerator.ts │ ├── InteractionModelGenerator.ts │ └── ModelTypes.ts ├── intl │ ├── EnglishGrammar.ts │ └── ListFormat.ts ├── logging │ ├── DefaultLogger.ts │ └── DefaultLoggerFactory.ts ├── modality │ └── ModalityEvaluation.ts ├── responseGeneration │ ├── AplMode.ts │ └── ControlResponseBuilder.ts ├── runtime │ ├── ControlHandler.ts │ └── SessionBehavior.ts ├── systemActs │ ├── ContentActs.ts │ ├── InitiativeActs.ts │ ├── PayloadTypes.ts │ └── SystemAct.ts └── utils │ ├── ArrayUtils.ts │ ├── AssertionUtils.ts │ ├── BasicTypes.ts │ ├── ControlTreeVisualization.ts │ ├── ControlUtils.ts │ ├── ControlVisitor.ts │ ├── DeepRequired.ts │ ├── ErrorUtils.ts │ ├── InputUtil.ts │ ├── IntentUtils.ts │ ├── Predicates.ts │ ├── RequestUtils.ts │ ├── ResponseUtils.ts │ ├── SerializationValidator.ts │ └── testSupport │ ├── SkillInvoker.ts │ ├── SkillWrapper.ts │ └── TestingUtils.ts ├── test ├── CustomSerdeTests.spec.ts ├── commonControlTests │ ├── DateControl.spec.ts │ ├── DateRangeControl.spec.ts │ ├── DynamicContainerControl.spec.ts │ ├── ListControl.spec.ts │ ├── MultiValueListControl.spec.ts │ ├── NumberControl.spec.ts │ ├── QuestionnaireControl.spec.ts │ └── ValueControl.spec.ts ├── custom_policy.spec.ts ├── game_e2e.spec.ts ├── game_strings.ts ├── interactionModelGeneratorTests │ ├── controlInteractionModelGenerator.spec.ts │ ├── interactionModelForTest.ts │ └── interactionModelGenerator.spec.ts ├── mock │ └── inputInteractionModel.json ├── modality.spec.ts ├── rendering.spec.ts ├── scenarios.spec.ts ├── systemActs │ └── ContentActs.spec.ts └── utilsTest │ ├── IntentUtils.spec.ts │ ├── ResponseUtils.spec.ts │ └── arrayUtils.spec.ts └── tsconfig.json /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "body-leading-blank": [1, "always"], 4 | "footer-leading-blank": [1, "always"], 5 | "subject-empty": [2, "never"], 6 | "subject-full-stop": [2, "never", "."], 7 | "type-case": [2, "always", "lower-case"], 8 | "type-empty": [2, "never"], 9 | "type-enum": [ 10 | 2, 11 | "always", 12 | [ 13 | "chore", 14 | "chore_", 15 | "docs", 16 | "docs_", 17 | "feat", 18 | "feat_", 19 | "fix", 20 | "fix_", 21 | "perf", 22 | "perf_", 23 | "refactor", 24 | "refactor_", 25 | "revert", 26 | "style", 27 | "style_", 28 | "test", 29 | "test_", 30 | "wip", 31 | "wip_" 32 | ] 33 | ] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "prettier/@typescript-eslint" 5 | //"plugin:@typescript-eslint/recommended" 6 | //"plugin:@typescript-eslint/recommended-requiring-type-checking" // TODO: fix errors and turn on. 7 | ], 8 | "parser": "@typescript-eslint/parser", 9 | "plugins": ["@typescript-eslint", "@typescript-eslint/eslint-plugin", "eslint-plugin-tsdoc"], 10 | "env": { 11 | "commonjs": true, 12 | "node": true, 13 | "mocha": true 14 | }, 15 | "globals": { 16 | "Atomics": "readonly", 17 | "SharedArrayBuffer": "readonly" 18 | }, 19 | 20 | "parserOptions": { 21 | "ecmaVersion": 2019, 22 | "sourceType": "module", 23 | "ecmaFeatures": { 24 | "modules": true, 25 | "impliedStrict": true 26 | }, 27 | 28 | // NOTE: change this to an absolute path if viewing the code via a skill 29 | // codebase with files in /lambda. This fixes 'Can't open tsconfig.json' 30 | // errors 31 | "project": "tsconfig.json" 32 | }, 33 | "rules": { 34 | // --------------------------------------------------------- 35 | // --- Rules from eslint:recommended that we override 36 | // --------------------------------------------------------- 37 | "no-empty": "warn", 38 | "no-unused-vars": "off", // because it flags types that are imported for type declarations (but otherwise unused). 39 | "no-undef": "off", // because if flags lack of import for Set. // TODO: fix the issues and remove this override. 40 | 41 | // --------------------------------------------------------- 42 | // --- Rules that we add and/or customize 43 | // --------------------------------------------------------- 44 | 45 | "eqeqeq": ["error", "smart"], 46 | "guard-for-in": "error", 47 | "id-blacklist": [ 48 | "error", 49 | "any", 50 | "Number", 51 | "number", 52 | "String", 53 | "string", 54 | "Boolean", 55 | "boolean", 56 | "Undefined", 57 | "undefined" 58 | ], 59 | "id-match": "error", 60 | "import/no-extraneous-dependencies": ["off", { "devDependencies": false }], 61 | "no-caller": "error", 62 | "no-duplicate-imports": "error", 63 | "no-eval": "error", 64 | "no-extra-bind": "error", 65 | "no-new-func": "error", 66 | "no-new-wrappers": "error", 67 | "no-return-await": "error", 68 | "no-sequences": "error", 69 | "no-shadow": ["off", { "hoist": "all" }], 70 | "no-undef-init": "error", 71 | "no-var": "error", 72 | "object-shorthand": "error", 73 | "one-var": ["error", "never"], 74 | "prefer-const": "error", 75 | "prefer-object-spread": "error", 76 | "radix": "error", 77 | 78 | // -- Rules from @typescript-eslint 79 | "@typescript-eslint/strict-boolean-expressions": ["error"], // avoids frequent sources of errors: 0, "", Promise, null vs undefined 80 | "@typescript-eslint/adjacent-overload-signatures": "error", 81 | "@typescript-eslint/array-type": ["error", { "default": "array-simple" }], 82 | "@typescript-eslint/ban-types": [ 83 | "error", 84 | { 85 | "types": { 86 | "Object": { "message": "Avoid using the `Object` type. Did you mean `object`?" }, 87 | "Function": { 88 | "message": "Avoid using the `Function` type. Prefer a specific function type, like `() => void`." 89 | }, 90 | "Boolean": { "message": "Avoid using the `Boolean` type. Did you mean `boolean`?" }, 91 | "Number": { "message": "Avoid using the `Number` type. Did you mean `number`?" }, 92 | "String": { "message": "Avoid using the `String` type. Did you mean `string`?" }, 93 | "Symbol": { "message": "Avoid using the `Symbol` type. Did you mean `symbol`?" } 94 | } 95 | } 96 | ], 97 | "@typescript-eslint/consistent-type-assertions": "error", 98 | "@typescript-eslint/consistent-type-definitions": "off", 99 | "@typescript-eslint/explicit-member-accessibility": ["off", { "accessibility": "explicit" }], 100 | "@typescript-eslint/interface-name-prefix": "off", 101 | "@typescript-eslint/member-ordering": "off", 102 | "@typescript-eslint/no-empty-function": "off", 103 | "@typescript-eslint/no-empty-interface": "off", 104 | "@typescript-eslint/no-explicit-any": "off", 105 | "@typescript-eslint/no-misused-new": "error", 106 | "@typescript-eslint/no-floating-promises": ["error"], // prevent those nasty forgotten promises ! 107 | "@typescript-eslint/no-namespace": "off", 108 | "@typescript-eslint/no-parameter-properties": "off", 109 | "@typescript-eslint/no-this-alias": "error", 110 | "@typescript-eslint/no-unused-expressions": "off", 111 | "@typescript-eslint/no-use-before-define": "off", 112 | "@typescript-eslint/no-var-requires": "error", 113 | "@typescript-eslint/prefer-for-of": "error", 114 | "@typescript-eslint/prefer-function-type": "error", 115 | "@typescript-eslint/prefer-namespace-keyword": "error", 116 | "@typescript-eslint/triple-slash-reference": [ 117 | "error", 118 | { 119 | "path": "always", 120 | "types": "prefer-import", 121 | "lib": "always" 122 | } 123 | ], 124 | "@typescript-eslint/type-annotation-spacing": "error", 125 | "@typescript-eslint/unified-signatures": "error", 126 | 127 | // -- Rules form tsdoc 128 | "tsdoc/syntax": "error" 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | 12 | 13 | ## I'm submitting a... 14 | 15 |

16 | [ ] Regression (a behavior that used to work and stopped working in a new release)
17 | [ ] Bug report  
18 | [ ] Performance issue
19 | [ ] Feature request
20 | [ ] Documentation issue or request
21 | [ ] Other... Please describe:
22 | 
23 | 24 | 25 | 26 | ## Expected Behavior 27 | 28 | 29 | 30 | ## Current Behavior 31 | 32 | 33 | 34 | 35 | 36 | ## Possible Solution 37 | 38 | 39 | 40 | ## Steps to Reproduce (for bugs) 41 | 42 | 43 | 44 | 45 | ## Context 46 | 47 | 48 | 49 | ## Your Environment 50 | 51 | * ASK SDK Controls used: x.x.x 52 | * Operating System and version: 53 | 54 | ## Node.js and NPM Info 55 | * Node.js version used for development: 56 | * NPM version used for development: 57 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 6 | ## Motivation and Context 7 | 8 | 9 | 10 | ## Testing 11 | 12 | 13 | 14 | 15 | ## Screenshots (if appropriate) 16 | 17 | ## Types of changes 18 | 19 | - [ ] Bug fix (non-breaking change which fixes an issue) 20 | - [ ] New feature (non-breaking change which adds functionality) 21 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 22 | - [ ] Docs(Add new document content) 23 | - [ ] Translate Docs(Translate document content) 24 | 25 | ## Checklist 26 | 27 | 28 | - [ ] My code follows the code style of this project 29 | - [ ] My change requires a change to the documentation 30 | - [ ] I have updated the documentation accordingly 31 | - [ ] I have read the **README** document 32 | - [ ] I have added tests to cover my changes 33 | - [ ] All new and existing tests passed 34 | - [ ] My commit message follows [Conventional Commit Guideline](https://conventionalcommits.org/) 35 | 36 | ## License 37 | - [ ] By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. 38 | 39 | [issues]: https://https://github.com/alexa/ask-sdk-controls/issues 40 | [license]: http://aws.amazon.com/apache2.0/ 41 | [cla]: http://en.wikipedia.org/wiki/Contributor_License_Agreement 42 | -------------------------------------------------------------------------------- /.github/workflows/unit-test.yml: -------------------------------------------------------------------------------- 1 | name: Linter, Formatting checks and unit Tests 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - 'doc/**' 7 | 8 | jobs: 9 | build: 10 | runs-on: ${{ matrix.os }} 11 | strategy: 12 | matrix: 13 | os: [ubuntu-latest, macos-latest, windows-latest] 14 | node: [12, 14, 16] 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Use Node.js ${{ matrix.node }} 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: ${{ matrix.node }} 22 | - run: npm install 23 | - run: npm run format-check 24 | - run: npm run lint 25 | - run: npm run test 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | .vscode 4 | .DS_Store 5 | .nycrc 6 | .nyc_output 7 | coverage/ 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | doc/ 3 | src/ 4 | node_modules/ 5 | test/ 6 | package-lock.json 7 | tsconfig.json 8 | ide/ 9 | .nyc_output 10 | .eslintrc.json 11 | .github/ 12 | .prettierrc 13 | .eslintignore 14 | .commitlintrc.json 15 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 4, 4 | "semi": true, 5 | "printWidth": 110, 6 | "singleQuote": true 7 | } 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *develop* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Create a new branch on our repository from *develop*. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your branch using clear commit messages. We use [Conventional Commit Guideline](https://conventionalcommits.org/). 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | 40 | ## Finding contributions to work on 41 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 42 | 43 | 44 | ## Code of Conduct 45 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 46 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 47 | opensource-codeofconduct@amazon.com with any additional questions or comments. 48 | 49 | 50 | ## Security issue notifications 51 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 52 | 53 | 54 | ## Licensing 55 | 56 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 57 | 58 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 59 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | ASK SDK Controls Framework 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

5 | 6 |
7 |

ASK SDK Controls Framework (Beta)

8 |

9 | 10 | The ASK SDK Controls framework builds on the [ASK SDK for Node.js](https://github.com/alexa/alexa-skills-kit-sdk-for-nodejs), offering a scalable solution for creating large, multi-turn skills in code with reusable components called *controls*. 11 | 12 | ## Documentation 13 | 14 | - [About the Controls Framework](https://alexa.design/controls) 15 | - [User Guide](https://github.com/alexa/ask-sdk-controls/blob/develop/doc/userGuide.md) 16 | - [API Reference](http://ask-sdk-controls-typedoc.s3-website-us-east-1.amazonaws.com) 17 | 18 | ## Samples 19 | 20 | ### [Hello World](https://github.com/alexa/skill-sample-controls-hello-world) 21 | 22 | Sample that familiarizes you with the Controls framework, allowing you to hear a response from Alexa when you trigger the skill. 23 | 24 | ### [Fruit Shop](https://github.com/alexa/skill-sample-controls-fruit-shop) 25 | 26 | Build a multi-modal grocery shopping skill using custom and library controls for item lists, shopping cart management, and checkout. 27 | 28 | ## Opening Issues 29 | 30 | For bug reports, feature requests and questions, we would like to hear about it. Search the [existing issues](https://github.com/alexa/ask-sdk-controls/issues) and try to make sure your problem doesn’t already exist before opening a new issue. It’s helpful if you include the version of the SDK, Node.js or browser environment and OS you’re using. Please include a stack trace and reduced repro case when appropriate, too. 31 | 32 | ## License 33 | 34 | This SDK is distributed under the Apache License, Version 2.0, see LICENSE for more information. 35 | 36 | ## Got Feedback? 37 | 38 | * [Connect with us on Slack](https://amazonalexa.slack.com/archives/C018QMP3K33)! 39 | * Request and vote for Alexa features [here](https://alexa.uservoice.com/forums/906892-alexa-skills-developer-voice-and-vote/filters/top?category_id=322783)! 40 | -------------------------------------------------------------------------------- /demo/Common/index.ts: -------------------------------------------------------------------------------- 1 | export { DemoRootControl as BasicRootControl, WelcomeAct } from './src/DemoRootControl'; 2 | -------------------------------------------------------------------------------- /demo/Common/src/DemoRootControl.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | import { IntentRequest, interfaces } from 'ask-sdk-model'; 14 | import { 15 | AmazonIntent, 16 | ContainerControl, 17 | ContainerControlProps, 18 | ContentAct, 19 | ControlInput, 20 | ControlResponseBuilder, 21 | ControlResultBuilder, 22 | InputUtil, 23 | LiteralContentAct, 24 | } from '../../../src'; 25 | 26 | /** 27 | * Communicates a simple "welcome" message. 28 | */ 29 | export class WelcomeAct extends ContentAct { 30 | render(input: ControlInput, responseBuilder: ControlResponseBuilder) { 31 | responseBuilder.addPromptFragment('Welcome.'); 32 | } 33 | } 34 | 35 | /** 36 | * A container control that handles LaunchRequest and is thus suitable for use 37 | * as a basic root control. 38 | * 39 | * On launch, a welcome message is issued 40 | */ 41 | export class DemoRootControl extends ContainerControl { 42 | handleFunc: (input: ControlInput, resultBuilder: ControlResultBuilder) => Promise; 43 | takeInitiativeFunc: (input: ControlInput, resultBuilder: ControlResultBuilder) => Promise; 44 | 45 | constructor(props: ContainerControlProps) { 46 | super({ id: props.id }); 47 | } 48 | 49 | async canHandle(input: ControlInput) { 50 | // directly handle launch request 51 | if (InputUtil.isLaunchRequest(input)) { 52 | this.handleFunc = this.handleLaunch; 53 | return true; 54 | } 55 | 56 | if (InputUtil.isSessionEndedRequest(input)) { 57 | this.handleFunc = this.handleSessionEnded; 58 | return true; 59 | } 60 | 61 | // otherwise delegate to children 62 | if (await this.canHandleByChild(input)) { 63 | return true; 64 | } 65 | 66 | this.handleFunc = this.handleFallbackEtc; 67 | return true; 68 | } 69 | 70 | async handle(input: ControlInput, resultBuilder: ControlResultBuilder): Promise { 71 | if (this.handleFunc !== undefined) { 72 | await this.handleFunc(input, resultBuilder); 73 | return; 74 | } 75 | return this.handleByChild(input, resultBuilder); 76 | } 77 | 78 | async handleLaunch(input: ControlInput, resultBuilder: ControlResultBuilder) { 79 | resultBuilder.addAct(new WelcomeAct(this)); 80 | } 81 | 82 | async handleSessionEnded(input: ControlInput, resultBuilder: ControlResultBuilder) { 83 | //nothing. 84 | } 85 | 86 | async handleFallbackEtc(input: ControlInput, resultBuilder: ControlResultBuilder) { 87 | let requestDescription; 88 | if (InputUtil.isIntent(input)) { 89 | requestDescription = (input.request as IntentRequest).intent.name; 90 | } else if (input.request.type === 'Alexa.Presentation.APL.UserEvent') { 91 | requestDescription = ''; 92 | const event = input.request as interfaces.alexa.presentation.apl.UserEvent; 93 | const args = (event.arguments ?? []).join(', '); 94 | requestDescription = `APL UserEvent with params ${args}`; 95 | } else { 96 | requestDescription = 'Input of unknown type'; 97 | } 98 | 99 | resultBuilder.addAct( 100 | new LiteralContentAct(this, { 101 | promptFragment: `${requestDescription} was not handled by any control.`, 102 | }), 103 | ); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /demo/ComponentModeAPL/src/buildInteractionModel.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | import { ComponentModeDemo } from '.'; 15 | import { ControlServices } from '../../../src/controls/ControlServices'; 16 | import { ControlInteractionModelGenerator } from '../../../src/interactionModelGeneration/ControlInteractionModelGenerator'; 17 | 18 | const log = ControlServices.getLogger('ComponentModeDemo:InteractionModel'); 19 | 20 | export namespace ComponentModeDemoIM { 21 | export const imGen = new ControlInteractionModelGenerator() 22 | .withInvocationName('controls demo') 23 | .addIntent({ name: 'AMAZON.StopIntent' }) 24 | .addIntent({ name: 'AMAZON.NavigateHomeIntent' }) 25 | .addIntent({ name: 'AMAZON.HelpIntent' }) 26 | .addIntent({ name: 'AMAZON.CancelIntent' }) 27 | .addIntent({ name: 'AMAZON.YesIntent' }) 28 | .addIntent({ name: 'AMAZON.NoIntent' }) 29 | .addIntent({ name: 'AMAZON.FallbackIntent' }) 30 | .setModelConfiguration({ fallbackIntentSensitivity: { level: 'HIGH' } }) 31 | 32 | .addOrMergeSlotType({ 33 | name: 'target', 34 | values: [ 35 | { 36 | id: 'age', 37 | name: { 38 | value: 'age', 39 | synonyms: ['my age'], 40 | }, 41 | }, 42 | { 43 | id: 'guests', 44 | name: { 45 | value: 'guests', 46 | synonyms: ['number of guests'], 47 | }, 48 | }, 49 | { 50 | id: 'theme', 51 | name: { 52 | value: 'theme', 53 | synonyms: ['party theme'], 54 | }, 55 | }, 56 | ], 57 | }) 58 | 59 | .addOrMergeSlotType({ 60 | name: 'PartyTheme', 61 | values: [ 62 | { 63 | id: 'pirate', 64 | name: { 65 | value: 'pirate', 66 | synonyms: ['pirates'], 67 | }, 68 | }, 69 | { 70 | id: 'cartoon', 71 | name: { 72 | value: 'cartoon', 73 | synonyms: ['cartoons'], 74 | }, 75 | }, 76 | { 77 | id: 'fairy', 78 | name: { 79 | value: 'fairy', 80 | synonyms: ['fairies'], 81 | }, 82 | }, 83 | { 84 | id: 'monster', 85 | name: { 86 | value: 'monster', 87 | synonyms: ['monsters'], 88 | }, 89 | }, 90 | { 91 | id: 'animal', 92 | name: { 93 | value: 'animal', 94 | synonyms: ['animals'], 95 | }, 96 | }, 97 | ], 98 | }) 99 | .buildCoreModelForControls(new ComponentModeDemo.DemoControlManager()); 100 | } 101 | 102 | // If launched directly, build and write to a file 103 | if (require.main === module) { 104 | // Build and write 105 | ComponentModeDemoIM.imGen.buildAndWrite('en-US-generated.json'); 106 | log.info('Wrote ./en-US-generated.json'); 107 | } 108 | -------------------------------------------------------------------------------- /demo/ListControl/YesNoMaybe/src/buildInteractionModel.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | import { ListDemo1 } from '.'; 15 | import { ControlServices } from '../../../../src/controls/ControlServices'; 16 | import { ControlInteractionModelGenerator } from '../../../../src/interactionModelGeneration/ControlInteractionModelGenerator'; 17 | import { filteredYesNoMaybeSlotType, yesNoMaybeSlotType } from './interactionModelTypes'; 18 | 19 | const log = ControlServices.getLogger('ComponentModeDemo:InteractionModel'); 20 | 21 | export namespace ListDemo1IM { 22 | export const imGen = new ControlInteractionModelGenerator() 23 | .withInvocationName('controls demo') 24 | .addIntent({ name: 'AMAZON.StopIntent' }) 25 | .addIntent({ name: 'AMAZON.NavigateHomeIntent' }) 26 | .addIntent({ name: 'AMAZON.HelpIntent' }) 27 | .addIntent({ name: 'AMAZON.CancelIntent' }) 28 | .addIntent({ name: 'AMAZON.YesIntent' }) 29 | .addIntent({ name: 'AMAZON.NoIntent' }) 30 | .addIntent({ name: 'AMAZON.FallbackIntent' }) 31 | .setModelConfiguration({ fallbackIntentSensitivity: { level: 'HIGH' } }) 32 | 33 | // Add a custom intent 34 | .addIntent({ 35 | name: 'HelloIntent', 36 | samples: ['Say hello', 'Say hi'], 37 | }) 38 | .addOrMergeSlotType(yesNoMaybeSlotType) 39 | .addOrMergeSlotType(filteredYesNoMaybeSlotType) 40 | .buildCoreModelForControls(new ListDemo1.DemoControlManager()); 41 | } 42 | 43 | // If launched directly, build and write to a file 44 | if (require.main === module) { 45 | // Build and write 46 | ListDemo1IM.imGen.buildAndWrite('en-US-generated.json'); 47 | log.info('Wrote ./en-US-generated.json'); 48 | } 49 | -------------------------------------------------------------------------------- /demo/ListControl/YesNoMaybe/src/index.ts: -------------------------------------------------------------------------------- 1 | import { SkillBuilders } from 'ask-sdk-core'; 2 | import { AmazonBuiltInSlotType, Strings } from '../../../../src'; 3 | import { ListControl } from '../../../../src/commonControls/listControl/ListControl'; 4 | import { Control } from '../../../../src/controls/Control'; 5 | import { ControlManager } from '../../../../src/controls/ControlManager'; 6 | import { ControlHandler } from '../../../../src/runtime/ControlHandler'; 7 | import { defaultIntentToValueMapper } from '../../../../src/utils/IntentUtils'; 8 | import { DemoRootControl } from '../../../Common/src/DemoRootControl'; 9 | import { filteredYesNoMaybeSlotType, yesNoMaybeSlotType } from './interactionModelTypes'; 10 | 11 | /** 12 | * Simple demonstration of a list control. 13 | * 14 | * root [RootControl] 15 | * - ListControl 16 | */ 17 | export namespace ListDemo1 { 18 | export class DemoControlManager extends ControlManager { 19 | createControlTree(): Control { 20 | const rootControl = new DemoRootControl({ id: 'root' }); 21 | rootControl.addChild( 22 | new ListControl({ 23 | id: 'question', 24 | listItemIDs: ['yes', 'no', 'maybe'], 25 | slotType: yesNoMaybeSlotType.name!, 26 | confirmationRequired: true, 27 | interactionModel: { 28 | targets: [Strings.Target.It, Strings.Target.Choice], 29 | slotValueConflictExtensions: { 30 | filteredSlotType: filteredYesNoMaybeSlotType.name!, 31 | intentToValueMapper: (intent) => defaultIntentToValueMapper(intent), 32 | }, 33 | }, 34 | prompts: { 35 | valueSet: '', // TODO: if confirmation required, this should probably default to ''. Possible?? 36 | }, 37 | }), 38 | ); 39 | 40 | return rootControl; 41 | } 42 | } 43 | } 44 | 45 | export const handler = SkillBuilders.custom() 46 | .addRequestHandlers(new ControlHandler(new ListDemo1.DemoControlManager())) 47 | .lambda(); 48 | -------------------------------------------------------------------------------- /demo/ListControl/YesNoMaybe/src/interactionModelTypes.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | import { v1 } from 'ask-smapi-model'; 15 | import SlotType = v1.skill.interactionModel.SlotType; 16 | 17 | export const yesNoMaybeSlotType: SlotType = { 18 | name: 'YesNoMaybe', 19 | values: [ 20 | { 21 | id: 'yes', 22 | name: { 23 | value: 'yes', 24 | synonyms: ['yep', 'correct', 'affirmative', 'yup', 'yeah'], 25 | }, 26 | }, 27 | { 28 | id: 'no', 29 | name: { 30 | value: 'no', 31 | synonyms: ['no', 'nope', 'no way'], 32 | }, 33 | }, 34 | { 35 | id: 'maybe', 36 | name: { 37 | value: 'maybe', 38 | synonyms: ['maybe', 'perhaps', "I haven't decided"], 39 | }, 40 | }, 41 | ], 42 | }; 43 | 44 | export const filteredYesNoMaybeSlotType: SlotType = { 45 | name: 'Maybe', 46 | values: [ 47 | { 48 | id: 'maybe', 49 | name: { 50 | value: 'maybe', 51 | synonyms: ['maybe', 'perhaps', "I haven't decided"], 52 | }, 53 | }, 54 | ], 55 | }; 56 | -------------------------------------------------------------------------------- /demo/ListControl/YesNoMaybe/test/listDemo1.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | import { expect } from 'chai'; 14 | import { suite, test } from 'mocha'; 15 | import { 16 | ControlHandler, 17 | IntentBuilder, 18 | ValueControlIntent, 19 | SkillInvoker, 20 | TestInput, 21 | testTurn, 22 | waitForDebugger, 23 | } from '../../../../src'; 24 | import { ListDemo1 } from '../src'; 25 | import { ListDemo1IM } from '../src/buildInteractionModel'; 26 | 27 | waitForDebugger(); 28 | 29 | suite('List Demo', () => { 30 | test('YesNoMaybe List Demo - IMGen', async () => { 31 | const imData = ListDemo1IM.imGen.build(); 32 | expect(imData.interactionModel?.languageModel?.intents?.find((x) => x.name === 'HelloIntent')).not 33 | .undefined; 34 | 35 | const yesNoMaybeValueControlIntent = imData.interactionModel?.languageModel?.intents?.find( 36 | (x) => x.name === 'YesNoMaybe_ValueControlIntent', 37 | ); 38 | expect(yesNoMaybeValueControlIntent).not.undefined; 39 | expect(yesNoMaybeValueControlIntent?.samples?.includes('{YesNoMaybe}')).is.false; 40 | }); 41 | 42 | test('YesNoMaybe List Demo - yes as value, then yes as confirmation', async () => { 43 | const requestHandler = new ControlHandler(new ListDemo1.DemoControlManager()); 44 | const invoker = new SkillInvoker(requestHandler); 45 | await testTurn( 46 | invoker, 47 | 'U: __', 48 | TestInput.launchRequest(), 49 | 'A: Welcome. What is your selection? Some suggestions are yes, no or maybe.', 50 | ); 51 | 52 | await testTurn( 53 | invoker, 54 | 'U: yes', 55 | TestInput.of(IntentBuilder.of('AMAZON.YesIntent')), 56 | 'A: Was that yes?', 57 | ); 58 | 59 | await testTurn(invoker, 'U: yes', TestInput.of(IntentBuilder.of('AMAZON.YesIntent')), 'A: Great.'); 60 | }); 61 | 62 | test('YesNoMaybe List Demo - yes as value, no as disconfirmation, maybe as new value', async () => { 63 | const requestHandler = new ControlHandler(new ListDemo1.DemoControlManager()); 64 | const invoker = new SkillInvoker(requestHandler); 65 | await testTurn( 66 | invoker, 67 | 'U: __', 68 | TestInput.launchRequest(), 69 | 'A: Welcome. What is your selection? Some suggestions are yes, no or maybe.', 70 | ); 71 | 72 | await testTurn( 73 | invoker, 74 | 'U: yes', 75 | TestInput.of(IntentBuilder.of('AMAZON.YesIntent')), 76 | 'A: Was that yes?', 77 | ); 78 | 79 | await testTurn( 80 | invoker, 81 | 'U: no', 82 | TestInput.of(IntentBuilder.of('AMAZON.NoIntent')), 83 | 'A: My mistake. What is your selection? Some suggestions are yes, no or maybe.', 84 | ); 85 | 86 | await testTurn( 87 | invoker, 88 | 'U: maybe', 89 | TestInput.of(ValueControlIntent.of('YesNoMaybe', { Maybe: 'maybe' })), 90 | 'A: Was that maybe?', 91 | ); 92 | 93 | await testTurn(invoker, 'U: yes', TestInput.of(IntentBuilder.of('AMAZON.YesIntent')), 'A: Great.'); 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /demo/MultiValueListControl/src/buildInteractionModel.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | import { MultiValueListDemo } from '.'; 15 | import { ControlInteractionModelGenerator } from '../../../src'; 16 | import { ControlServices } from '../../../src/controls/ControlServices'; 17 | import { 18 | filteredYesNoMaybeSlotType, 19 | yesNoMaybeSlotType, 20 | } from '../../ListControl/YesNoMaybe/src/interactionModelTypes'; 21 | 22 | const log = ControlServices.getLogger('MultiValueListDemo:InteractionModel'); 23 | 24 | export namespace MultiValueListDemoIM { 25 | export const imGen = new ControlInteractionModelGenerator() 26 | .withInvocationName('controls demo') 27 | .addIntent({ name: 'AMAZON.StopIntent' }) 28 | .addIntent({ name: 'AMAZON.NavigateHomeIntent' }) 29 | .addIntent({ name: 'AMAZON.HelpIntent' }) 30 | .addIntent({ name: 'AMAZON.CancelIntent' }) 31 | .addIntent({ name: 'AMAZON.YesIntent' }) 32 | .addIntent({ name: 'AMAZON.NoIntent' }) 33 | .addIntent({ name: 'AMAZON.FallbackIntent' }) 34 | .setModelConfiguration({ fallbackIntentSensitivity: { level: 'HIGH' } }) 35 | .addOrMergeSlotType(yesNoMaybeSlotType) 36 | .addOrMergeSlotType(filteredYesNoMaybeSlotType) 37 | .addOrMergeSlotType({ 38 | name: 'GroceryItem', 39 | values: [ 40 | { 41 | id: 'Milk', 42 | name: { 43 | value: 'Milk', 44 | synonyms: ['milk', 'almond milk', 'low fat milk'], 45 | }, 46 | }, 47 | { 48 | id: 'Eggs', 49 | name: { 50 | value: 'Eggs', 51 | synonyms: ['egg', 'eggs'], 52 | }, 53 | }, 54 | { 55 | id: 'Cereal', 56 | name: { 57 | value: 'Cereal', 58 | synonyms: ['cereal', 'oats', 'breakfast food'], 59 | }, 60 | }, 61 | { 62 | id: 'Bread', 63 | name: { 64 | value: 'Bread', 65 | synonyms: ['bread'], 66 | }, 67 | }, 68 | { 69 | id: 'Honey', 70 | name: { 71 | value: 'Honey', 72 | synonyms: ['honey', 'nectar'], 73 | }, 74 | }, 75 | ], 76 | }) 77 | .buildCoreModelForControls(new MultiValueListDemo.DemoControlManager()); 78 | } 79 | 80 | // If launched directly, build and write to a file 81 | if (require.main === module) { 82 | // Build and write 83 | MultiValueListDemoIM.imGen.buildAndWrite('en-US-generated.json'); 84 | log.info('Wrote ./en-US-generated.json'); 85 | } 86 | -------------------------------------------------------------------------------- /demo/MultiValueListControl/src/index.ts: -------------------------------------------------------------------------------- 1 | import { SkillBuilders } from 'ask-sdk-core'; 2 | import { Control } from '../../..//src/controls/Control'; 3 | import { 4 | MultiValueListControl, 5 | MultiValueListStateValue, 6 | MultiValueValidationFailure, 7 | } from '../../../src/commonControls/multiValueListControl/MultiValueListControl'; 8 | import { ControlManager } from '../../../src/controls/ControlManager'; 9 | import { ControlHandler } from '../../../src/runtime/ControlHandler'; 10 | import { DemoRootControl } from '../../Common/src/DemoRootControl'; 11 | 12 | export namespace MultiValueListDemo { 13 | export class DemoControlManager extends ControlManager { 14 | createControlTree(): Control { 15 | const rootControl = new DemoRootControl({ id: 'root' }); 16 | 17 | rootControl.addChild( 18 | new MultiValueListControl({ 19 | id: 'myShoppingList', 20 | validation: validateItems, 21 | listItemIDs: getShoppingList, 22 | slotType: 'GroceryItem', 23 | confirmationRequired: true, 24 | prompts: { 25 | confirmValue: 'Is that all?', 26 | }, 27 | }), 28 | ); 29 | 30 | function getShoppingList() { 31 | return ['Milk', 'Eggs', 'Cereal', 'Bread']; 32 | } 33 | 34 | function validateItems(values: MultiValueListStateValue[]): true | MultiValueValidationFailure { 35 | const invalidValues = []; 36 | for (const item of values) { 37 | if (getShoppingList().includes(item.id) !== true) { 38 | invalidValues.push(item.id); 39 | } 40 | } 41 | if (invalidValues.length > 0) { 42 | return { 43 | invalidValues, 44 | renderedReason: 'item is not available in the shopping list', 45 | }; 46 | } 47 | return true; 48 | } 49 | 50 | return rootControl; 51 | } 52 | } 53 | } 54 | 55 | export const handler = SkillBuilders.custom() 56 | .addRequestHandlers(new ControlHandler(new MultiValueListDemo.DemoControlManager())) 57 | .lambda(); 58 | -------------------------------------------------------------------------------- /demo/MultiValueListControl/test/multiValueListDemo.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | import { expect } from 'chai'; 14 | import { suite, test } from 'mocha'; 15 | import { 16 | ControlHandler, 17 | GeneralControlIntent, 18 | IntentBuilder, 19 | SkillInvoker, 20 | TestInput, 21 | testTurn, 22 | waitForDebugger, 23 | } from '../../../src'; 24 | import { Strings as $ } from '../../../src/constants/Strings'; 25 | import { ValueControlIntent } from '../../../src/intents/ValueControlIntent'; 26 | import { MultiValueListDemo } from '../src'; 27 | import { MultiValueListDemoIM } from '../src/buildInteractionModel'; 28 | waitForDebugger(); 29 | 30 | suite('MultiValueList Demo', () => { 31 | test('GroceryItem MultiValueList Demo - IMGen', async () => { 32 | const imData = MultiValueListDemoIM.imGen.build(); 33 | const groceryItemValueControlIntent = imData.interactionModel?.languageModel?.intents?.find( 34 | (x) => x.name === 'GroceryItem_ValueControlIntent', 35 | ); 36 | expect(groceryItemValueControlIntent).not.undefined; 37 | expect(groceryItemValueControlIntent?.samples?.includes('{GroceryItem}')).is.true; 38 | }); 39 | 40 | test('MultiValueList Demo - add multiple values', async () => { 41 | const requestHandler = new ControlHandler(new MultiValueListDemo.DemoControlManager()); 42 | const invoker = new SkillInvoker(requestHandler); 43 | await testTurn( 44 | invoker, 45 | 'U: __', 46 | TestInput.launchRequest(), 47 | 'A: Welcome. What is your selection? Some suggestions are Milk, Eggs or Cereal.', 48 | ); 49 | 50 | await testTurn( 51 | invoker, 52 | 'U: add Milk and Honey', 53 | TestInput.of( 54 | ValueControlIntent.of('GroceryItem', { 55 | GroceryItem: ['Milk', 'Honey'], 56 | action: $.Action.Add, 57 | }), 58 | ), 59 | "A: OK, added Milk. Sorry, Honey can't be added as item is not available in the shopping list. Is that all?", 60 | ); 61 | 62 | await testTurn(invoker, 'U: yes', TestInput.of(IntentBuilder.of('AMAZON.YesIntent')), 'A: Great.'); 63 | }); 64 | 65 | test('MultiValueList Demo - add multiple values, remove all', async () => { 66 | const requestHandler = new ControlHandler(new MultiValueListDemo.DemoControlManager()); 67 | const invoker = new SkillInvoker(requestHandler); 68 | await testTurn( 69 | invoker, 70 | 'U: __', 71 | TestInput.launchRequest(), 72 | 'A: Welcome. What is your selection? Some suggestions are Milk, Eggs or Cereal.', 73 | ); 74 | 75 | await testTurn( 76 | invoker, 77 | 'U: add Bread, Eggs and Milk', 78 | TestInput.of( 79 | ValueControlIntent.of('GroceryItem', { 80 | GroceryItem: ['Bread', 'Eggs', 'Milk'], 81 | action: $.Action.Add, 82 | }), 83 | ), 84 | 'A: OK, added Bread, Eggs and Milk. Is that all?', 85 | ); 86 | 87 | await testTurn( 88 | invoker, 89 | 'U: Clear all items', 90 | TestInput.of(GeneralControlIntent.of({ action: $.Action.Clear })), 91 | 'A: OK, cleared Bread, Eggs and Milk from the list. What is your selection? Some suggestions are Milk, Eggs or Cereal.', 92 | ); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /demo/NumberControl/NumberDemo/src/buildInteractionModel.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | import { BasicNumberDemo, CustomLoggerFactory } from '.'; 15 | import { ControlServices } from '../../../../src/controls/ControlServices'; 16 | import { ControlInteractionModelGenerator } from '../../../../src/interactionModelGeneration/ControlInteractionModelGenerator'; 17 | 18 | const services = { logger: new CustomLoggerFactory() }; 19 | ControlServices.setDefaults(services); 20 | 21 | const log = ControlServices.getLogger('NumberControlDemo:InteractionModel'); 22 | 23 | export namespace BasicNumberDemoIM { 24 | export const imGen = new ControlInteractionModelGenerator() 25 | .withInvocationName('control demos') 26 | .addIntent({ name: 'AMAZON.StopIntent' }) 27 | .addIntent({ name: 'AMAZON.NavigateHomeIntent' }) 28 | .addIntent({ name: 'AMAZON.HelpIntent' }) 29 | .addIntent({ name: 'AMAZON.CancelIntent' }) 30 | .addIntent({ name: 'AMAZON.YesIntent' }) 31 | .addIntent({ name: 'AMAZON.NoIntent' }) 32 | .addIntent({ name: 'AMAZON.FallbackIntent' }) 33 | .setModelConfiguration({ fallbackIntentSensitivity: { level: 'HIGH' } }) 34 | .buildCoreModelForControls(new BasicNumberDemo.DemoControlManager()); 35 | } 36 | 37 | // If launched directly, build and write to a file 38 | if (require.main === module) { 39 | // Build and write 40 | BasicNumberDemoIM.imGen.buildAndWrite('en-US-generated.json'); 41 | log.info('Wrote ./en-US-generated.json'); 42 | } 43 | -------------------------------------------------------------------------------- /demo/NumberControl/NumberDemo/src/index.ts: -------------------------------------------------------------------------------- 1 | import { SkillBuilders } from 'ask-sdk-core'; 2 | import Debug from 'debug'; 3 | import { DefaultLogger } from '../../../../src'; 4 | import { NumberControl } from '../../../../src/commonControls/numberControl/NumberControl'; 5 | import { Control } from '../../../../src/controls/Control'; 6 | import { ControlManager } from '../../../../src/controls/ControlManager'; 7 | import { ControlServices } from '../../../../src/controls/ControlServices'; 8 | import { ILogger } from '../../../../src/controls/interfaces/ILogger'; 9 | import { ILoggerFactory } from '../../../../src/controls/interfaces/ILoggerFactory'; 10 | import { ControlHandler } from '../../../../src/runtime/ControlHandler'; 11 | import { DemoRootControl } from '../../../Common/src/DemoRootControl'; 12 | 13 | export class CustomLoggerFactory implements ILoggerFactory { 14 | getLogger(namespace: string): ILogger { 15 | return new CustomLogger(namespace); 16 | } 17 | } 18 | 19 | export class CustomLogger extends DefaultLogger { 20 | debug(message: string): void { 21 | Debug(`debug:CustomLogger:${this.moduleName}`)(message); 22 | } 23 | 24 | info(message: string): void { 25 | Debug(`info:CustomLogger:${this.moduleName}`)(message); 26 | } 27 | 28 | sanitize(message: any): string { 29 | if ( 30 | process.env.ASK_SDK_RESTRICTIVE_LOGGING !== undefined && 31 | process.env.ASK_SDK_RESTRICTIVE_LOGGING === 'true' 32 | ) { 33 | return '###################'; 34 | } else { 35 | return message; 36 | } 37 | } 38 | } 39 | 40 | /** 41 | * Simple demonstration of a number control. 42 | * 43 | * root [RootControl] 44 | * - NumberControl 45 | */ 46 | export namespace BasicNumberDemo { 47 | export class DemoControlManager extends ControlManager { 48 | createControlTree(): Control { 49 | const rootControl = new DemoRootControl({ id: 'root' }); 50 | rootControl.addChild( 51 | new NumberControl({ 52 | id: 'number', 53 | confirmationRequired: true, 54 | validation: [ 55 | (state) => state.value! % 2 === 0 || { renderedReason: 'the value must be even' }, 56 | ], 57 | apl: { 58 | validationFailedMessage: (value) => 59 | `Sorry, ${value} is not valid, value must be even`, 60 | }, 61 | }), 62 | ); 63 | return rootControl; 64 | } 65 | } 66 | } 67 | 68 | const services = { logger: new CustomLoggerFactory() }; 69 | ControlServices.setDefaults(services); 70 | 71 | export const handler = SkillBuilders.custom() 72 | .addRequestHandlers(new ControlHandler(new BasicNumberDemo.DemoControlManager())) 73 | .lambda(); 74 | -------------------------------------------------------------------------------- /demo/NumberControl/NumberDemo/test/numberDemo.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | import { expect } from 'chai'; 14 | import { suite, test } from 'mocha'; 15 | import { 16 | AmazonBuiltInSlotType, 17 | AmazonIntent, 18 | ControlHandler, 19 | IntentBuilder, 20 | SkillInvoker, 21 | TestInput, 22 | testTurn, 23 | ValueControlIntent, 24 | waitForDebugger, 25 | } from '../../../../src'; 26 | import { BasicNumberDemo } from '../src'; 27 | import { BasicNumberDemoIM } from '../src/buildInteractionModel'; 28 | 29 | waitForDebugger(); 30 | 31 | suite('Number Demo', () => { 32 | test('Number Demo - IMGen', async () => { 33 | const imData = BasicNumberDemoIM.imGen.build(); 34 | 35 | const numberValueControlIntent = imData.interactionModel?.languageModel?.intents?.find( 36 | (x) => x.name === 'AMAZON_NUMBER_ValueControlIntent', 37 | ); 38 | expect(numberValueControlIntent).not.undefined; 39 | }); 40 | 41 | test('Number Demo - give value, then confirm', async () => { 42 | const requestHandler = new ControlHandler(new BasicNumberDemo.DemoControlManager()); 43 | const invoker = new SkillInvoker(requestHandler); 44 | await testTurn(invoker, 'U: __', TestInput.launchRequest(), 'A: Welcome. What number?'); 45 | 46 | await testTurn( 47 | invoker, 48 | 'U: four', 49 | TestInput.of(ValueControlIntent.of('AMAZON.NUMBER', { 'AMAZON.NUMBER': '4' })), 50 | 'A: Was that 4?', 51 | ); 52 | 53 | await testTurn(invoker, 'U: yes', TestInput.of(IntentBuilder.of('AMAZON.YesIntent')), 'A: Great.'); 54 | }); 55 | 56 | test('Number Demo - 14 as value, no as disconfirmation, resolve ambiguity to 40', async () => { 57 | const requestHandler = new ControlHandler(new BasicNumberDemo.DemoControlManager()); 58 | const invoker = new SkillInvoker(requestHandler); 59 | await testTurn(invoker, 'U: __', TestInput.launchRequest(), 'A: Welcome. What number?'); 60 | 61 | await testTurn( 62 | invoker, 63 | 'U: fourteen', 64 | TestInput.of(ValueControlIntent.of('AMAZON.NUMBER', { 'AMAZON.NUMBER': '14' })), 65 | 'A: Was that 14?', 66 | ); 67 | 68 | await testTurn( 69 | invoker, 70 | 'U: no', 71 | TestInput.of(IntentBuilder.of('AMAZON.NoIntent')), 72 | 'A: My mistake. Did you perhaps mean 40?', 73 | ); 74 | 75 | await testTurn(invoker, 'U: yes', TestInput.of(IntentBuilder.of('AMAZON.YesIntent')), 'A: Great.'); 76 | }); 77 | 78 | test('Number Demo - screen input, no confirmation', async () => { 79 | const requestHandler = new ControlHandler(new BasicNumberDemo.DemoControlManager()); 80 | const invoker = new SkillInvoker(requestHandler); 81 | await testTurn(invoker, 'U: __', TestInput.launchRequest(), 'A: Welcome. What number?'); 82 | 83 | await testTurn( 84 | invoker, 85 | 'U: __', 86 | TestInput.simpleUserEvent(['number', 40]), 87 | 'A: ', // No voice prompt for touch 88 | ); 89 | 90 | expect(requestHandler.getSerializableControlStates().number.value).equals(40); 91 | expect(requestHandler.getSerializableControlStates().number.confirmed).equals(true); 92 | }); 93 | 94 | test('Number Demo - screen input, invalid, new value valid', async () => { 95 | const requestHandler = new ControlHandler(new BasicNumberDemo.DemoControlManager()); 96 | const invoker = new SkillInvoker(requestHandler); 97 | await testTurn(invoker, 'U: __', TestInput.launchRequest(), 'A: Welcome. What number?'); 98 | 99 | await testTurn( 100 | invoker, 101 | 'U: __', 102 | TestInput.simpleUserEvent(['number', 3]), 103 | 'A: ', // No voice prompt for touch 104 | ); 105 | 106 | expect(requestHandler.getSerializableControlStates().number.value).equals(3); 107 | expect(requestHandler.getSerializableControlStates().number.confirmed).equals(false); 108 | 109 | await testTurn( 110 | invoker, 111 | 'U: 4', 112 | TestInput.of(ValueControlIntent.of(AmazonBuiltInSlotType.NUMBER, { 'AMAZON.NUMBER': '4' })), 113 | 'A: Was that 4?', 114 | ); 115 | 116 | await testTurn( 117 | invoker, 118 | 'U: Yes', 119 | TestInput.of(IntentBuilder.of(AmazonIntent.YesIntent)), 120 | 'A: Great.', 121 | ); 122 | }); 123 | }); 124 | -------------------------------------------------------------------------------- /demo/TwoLists/src/buildInteractionModel.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | import { MultipleLists } from '.'; 15 | import { ControlServices } from '../../../src/controls/ControlServices'; 16 | import { ControlInteractionModelGenerator } from '../../../src/interactionModelGeneration/ControlInteractionModelGenerator'; 17 | 18 | const log = ControlServices.getLogger('HelloWorld:InteractionModel'); 19 | 20 | export namespace TwoListsIM { 21 | export const imGen = new ControlInteractionModelGenerator() 22 | .withInvocationName('controls demo') 23 | .addIntent({ name: 'AMAZON.StopIntent' }) 24 | .addIntent({ name: 'AMAZON.NavigateHomeIntent' }) 25 | .addIntent({ name: 'AMAZON.HelpIntent' }) 26 | .addIntent({ name: 'AMAZON.CancelIntent' }) 27 | .addIntent({ name: 'AMAZON.YesIntent' }) 28 | .addIntent({ name: 'AMAZON.NoIntent' }) 29 | .addIntent({ name: 'AMAZON.FallbackIntent' }) 30 | .setModelConfiguration({ fallbackIntentSensitivity: { level: 'HIGH' } }) 31 | 32 | .addOrMergeSlotType({ 33 | name: 'PetSpecies', 34 | values: [ 35 | { 36 | id: 'cat', 37 | name: { 38 | value: 'cat', 39 | synonyms: ['kitty', 'house cat', 'feline'], 40 | }, 41 | }, 42 | { 43 | id: 'dog', 44 | name: { 45 | value: 'dog', 46 | synonyms: ['doggie', 'mutt', 'puppy'], 47 | }, 48 | }, 49 | { 50 | id: 'rabbit', 51 | name: { 52 | value: 'rabbit', 53 | synonyms: ['bunny', 'bunny rabbit'], 54 | }, 55 | }, 56 | ], 57 | }) 58 | 59 | .addOrMergeSlotType({ 60 | name: 'PetBreed', 61 | values: [ 62 | { 63 | id: 'labrador', 64 | name: { 65 | value: 'labrador', 66 | synonyms: [], 67 | }, 68 | }, 69 | { 70 | id: 'persian', 71 | name: { 72 | value: 'persian', 73 | synonyms: [], 74 | }, 75 | }, 76 | ], 77 | }) 78 | 79 | .addOrMergeSlotType({ 80 | name: 'TransactionType', 81 | values: [ 82 | { 83 | id: 'adopt', 84 | name: { 85 | value: 'adopt', 86 | synonyms: [], 87 | }, 88 | }, 89 | { 90 | id: 'foster', 91 | name: { 92 | value: 'foster', 93 | synonyms: [], 94 | }, 95 | }, 96 | { 97 | id: 'sponsor', 98 | name: { 99 | value: 'sponsor', 100 | synonyms: [], 101 | }, 102 | }, 103 | ], 104 | }) 105 | 106 | .addOrMergeSlotType({ 107 | name: 'target', 108 | values: [ 109 | { 110 | id: 'species', 111 | name: { value: 'species', synonyms: ['type of pet'] }, 112 | }, 113 | { 114 | id: 'petKind', 115 | name: { value: 'petKind', synonyms: ['kind of pet', 'pet kind'] }, 116 | }, 117 | { 118 | id: 'breed', 119 | name: { value: 'breed', synonyms: ['type', 'variety'] }, 120 | }, 121 | { 122 | id: 'transaction', 123 | name: { value: 'transaction', synonyms: ['contract', 'agreement'] }, 124 | }, 125 | ], 126 | }) 127 | .buildCoreModelForControls(new MultipleLists.DemoControlManager()); 128 | } 129 | 130 | // If launched directly, build and write to a file 131 | if (require.main === module) { 132 | // Build and write 133 | TwoListsIM.imGen.buildAndWrite('en-US-generated.json'); 134 | log.info('Wrote ./en-US-generated.json'); 135 | } 136 | -------------------------------------------------------------------------------- /demo/TwoLists/src/index.ts: -------------------------------------------------------------------------------- 1 | import { SkillBuilders } from 'ask-sdk-core'; 2 | import { Control } from '../../..//src/controls/Control'; 3 | import { ListControl } from '../../../src/commonControls/listControl/ListControl'; 4 | import { Strings } from '../../../src/constants/Strings'; 5 | import { ControlManager } from '../../../src/controls/ControlManager'; 6 | import { ControlHandler } from '../../../src/runtime/ControlHandler'; 7 | import { DemoRootControl } from '../../Common/src/DemoRootControl'; 8 | 9 | export namespace MultipleLists { 10 | const breedControl = new ListControl({ 11 | id: 'breed', 12 | listItemIDs: ['labrador', 'persian'], 13 | slotType: 'PetBreed', 14 | }); 15 | 16 | export class DemoControlManager extends ControlManager { 17 | createControlTree(): Control { 18 | const rootControl = new DemoRootControl({ id: 'root' }); 19 | 20 | rootControl.addChild( 21 | new ListControl({ 22 | id: 'species', 23 | listItemIDs: ['cat', 'dog', 'rabbit'], 24 | slotType: 'PetSpecies', 25 | interactionModel: { 26 | targets: [Strings.Target.It, 'species', 'petKind'], 27 | }, 28 | confirmationRequired(this: ListControl) { 29 | return this.state.value === 'rabbit'; 30 | }, 31 | }), 32 | ); 33 | 34 | rootControl.addChild(breedControl); 35 | 36 | rootControl.addChild( 37 | new ListControl({ 38 | id: 'transactionType', 39 | listItemIDs: ['adopt', 'foster', 'sponsor'], 40 | slotType: 'TransactionType', 41 | }), 42 | ); 43 | 44 | return rootControl; 45 | } 46 | } 47 | } 48 | 49 | export const handler = SkillBuilders.custom() 50 | .addRequestHandlers(new ControlHandler(new MultipleLists.DemoControlManager())) 51 | .lambda(); 52 | -------------------------------------------------------------------------------- /demo/TwoLists/test/multiListDemo1.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | import { suite, test } from 'mocha'; 14 | import { 15 | ControlHandler, 16 | ValueControlIntent, 17 | SkillInvoker, 18 | TestInput, 19 | testTurn, 20 | waitForDebugger, 21 | } from '../../../src'; 22 | import { MultipleLists } from '../src'; 23 | 24 | waitForDebugger(); 25 | 26 | suite('multiList Demo', () => { 27 | test('YesNoMaybe List Demo - yes as value, then yes as confirmation', async () => { 28 | const requestHandler = new ControlHandler(new MultipleLists.DemoControlManager()); 29 | const invoker = new SkillInvoker(requestHandler); 30 | await testTurn( 31 | invoker, 32 | 'U: __', 33 | TestInput.launchRequest(), 34 | 'A: Welcome. What is your selection? Some suggestions are cat, dog or rabbit.', 35 | ); 36 | 37 | await testTurn( 38 | invoker, 39 | 'U: cat', 40 | TestInput.of(ValueControlIntent.of('PetSpecies', { PetSpecies: 'cat' })), 41 | 'A: OK, cat. What is your selection? Some suggestions are labrador or persian.', 42 | ); 43 | 44 | await testTurn( 45 | invoker, 46 | 'U: persian', 47 | TestInput.of(ValueControlIntent.of('PetBreed', { PetBreed: 'persian' })), 48 | 'A: OK, persian. What is your selection? Some suggestions are adopt, foster or sponsor.', 49 | ); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /doc/img/fruitShopControlTree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa/ask-sdk-controls/b8dde93e0e93049d25c9e388541d8337cb630641/doc/img/fruitShopControlTree.png -------------------------------------------------------------------------------- /doc/img/handlingChain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa/ask-sdk-controls/b8dde93e0e93049d25c9e388541d8337cb630641/doc/img/handlingChain.png -------------------------------------------------------------------------------- /doc/img/requestHandling.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa/ask-sdk-controls/b8dde93e0e93049d25c9e388541d8337cb630641/doc/img/requestHandling.png -------------------------------------------------------------------------------- /ide/vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Run tests (INFO)", 11 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", 12 | "args": [ 13 | "--timeout", 14 | "999999", 15 | "--colors", 16 | 17 | // TODO. 18 | 19 | // Slow startup 20 | "-r", 21 | "ts-node/register", 22 | "${workspaceFolder}/test/**/*.spec.ts", 23 | 24 | // Demo test cases 25 | "${workspaceFolder}/demo/**/test/**/*.spec.ts" 26 | 27 | 28 | // Fast startup. 29 | // "--require", 30 | // "source-map-support/register", 31 | // "${workspaceFolder}/dist/test/**/*.spec.js" 32 | ], 33 | "env": { 34 | // "DEBUG": "error:*, warn:*, info:*" // uncomment for logs 35 | // "ASK_SDK_CONTROLS_LOG_RESPONSE_FILEPATH": // uncomment to write out response for debugging 36 | //"ASK_SDK_RESTRICTIVE_LOGGING": "true" // uncomment to restrict sensitive information from logging such as slot values 37 | }, 38 | // "smartStep": true, 39 | "skipFiles": [ 40 | "node_modules/**", 41 | "/**" 42 | ], 43 | "console": "integratedTerminal" 44 | 45 | } 46 | ] 47 | } -------------------------------------------------------------------------------- /ide/vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | // Activate via "Task: Run build task" (Ctrl-Shift-B) 7 | { 8 | "type": "npm", 9 | "script": "watch", 10 | "problemMatcher": [ 11 | "$tsc-watch" 12 | ], 13 | "isBackground": true, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ask-sdk-controls", 3 | "version": "0.9.2", 4 | "description": "Skill-building framework & library for Alexa SDK", 5 | "main": "dist/src/index.js", 6 | "types": "dist/src/index.d.ts", 7 | "scripts": { 8 | "build": "tsc && npm run copyTestSupportFiles && npm run lint", 9 | "copyTestSupportFiles": "[ -d dist/test/mock ] || mkdir dist/test/mock && cp test/mock/* dist/test/mock/", 10 | "watch": "tsc-watch --onSuccess 'npm run copyTestSupportFiles'", 11 | "lint": "eslint \"src/**/*.{ts,tsx}\" \"test/**/*.{ts,tsx}\" \"demo/**/*.{ts,tsx}\"", 12 | "format-check": "prettier --check \"src/**/*.{ts,tsx}\" \"test/**/*.{ts,tsx}\" \"demo/**/*.{ts,tsx}\"", 13 | "format-overwrite": "prettier -w \"src/**/*.{ts,tsx}\" \"test/**/*.{ts,tsx}\" \"demo/**/*.{ts,tsx}\"", 14 | "test": "cross-env TS_NODE_FILES=true mocha -r ts-node/register \"./test/**/*.spec.ts\" \"./demo/**/test/**/*.spec.ts\"", 15 | "test-all": "cross-env TS_NODE_FILES=true mocha --forbid-only -r ts-node/register \"./test/**/*.spec.ts\" \"./demo/**/test/**/*.spec.ts\"", 16 | "coverage": "nyc --cache npm run test-all && nyc report --reporter=html && echo HTML report generated at coverage/index.html", 17 | "clean": "rm -rf ./dist", 18 | "reinstall": "rm -rf ./node_modules && npm install", 19 | "rebuild": "npm run clean && npm run build", 20 | "docs": "rm -rf ./dist/api_docs && typedoc --out dist/api_docs src", 21 | "pre-release": "standard-version", 22 | "release": "npm run coverage && npm run lint && npm run format-check" 23 | }, 24 | "dependencies": { 25 | "ask-sdk-core": "^2.12.1", 26 | "ask-sdk-local-debug": "^1.1.0", 27 | "ask-sdk-model": "^1.35.1", 28 | "ask-smapi-model": "^1.15.3", 29 | "chai": "^4.2.0", 30 | "debug": "^4.1.1", 31 | "i18next": "^19.9.2", 32 | "lodash": "^4.17.15", 33 | "tslib": "^2.0.0" 34 | }, 35 | "peerDependencies": { 36 | "ask-sdk-core": "^2.12.1", 37 | "ask-sdk-local-debug": "^1.1.0", 38 | "ask-sdk-model": "^1.35.1", 39 | "ask-smapi-model": "^1.15.3", 40 | "chai": "^4.2.0", 41 | "debug": "^4.1.1", 42 | "i18next": "^19.9.2", 43 | "lodash": "^4.17.15", 44 | "tslib": "^2.0.0" 45 | }, 46 | "devDependencies": { 47 | "@commitlint/cli": "^9.1.2", 48 | "@commitlint/config-conventional": "^12.1.4", 49 | "@types/chai": "^4.2.8", 50 | "@types/debug": "^4.1.5", 51 | "@types/lodash": "^4.14.149", 52 | "@types/mocha": "^5.2.7", 53 | "@types/node": "^10.17.14", 54 | "@types/sinon": "^7.5.2", 55 | "@typescript-eslint/eslint-plugin": "^3.10.1", 56 | "@typescript-eslint/parser": "^3.3.0", 57 | "cross-env": "^7.0.2", 58 | "eslint": "^7.2.0", 59 | "eslint-config-prettier": "^6.11.0", 60 | "eslint-plugin-tsdoc": "^0.2.5", 61 | "highlight.js": ">=10.4.1", 62 | "husky": "^4.2.5", 63 | "marked": "^4.0.12", 64 | "mocha": "^7.1.2", 65 | "nyc": "^15.0.1", 66 | "prettier": "^2.1.1", 67 | "sinon": "^9.0.1", 68 | "source-map-support": "^0.5.19", 69 | "standard-version": "^9.0.0", 70 | "ts-node": "^8.10.2", 71 | "tsc-watch": "^4.2.3", 72 | "typedoc": "^0.17.6", 73 | "typedoc-neo-theme": "^1.0.8", 74 | "typescript": "^3.9.10" 75 | }, 76 | "husky": { 77 | "hooks": { 78 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS && npm run lint && npm run format-check", 79 | "pre-push": "npm run test-all" 80 | } 81 | }, 82 | "standard-version": { 83 | "types": [ 84 | { 85 | "type": "feat", 86 | "section": "Features" 87 | }, 88 | { 89 | "type": "feat_", 90 | "section": "Feature work - not included in Changelog", 91 | "hidden": true 92 | }, 93 | { 94 | "type": "fix", 95 | "section": "Bug Fixes" 96 | }, 97 | { 98 | "type": "fix_", 99 | "section": "Bug Fixes - not included in Changelog", 100 | "hidden": true 101 | }, 102 | { 103 | "type": "chore", 104 | "hidden": true 105 | }, 106 | { 107 | "type": "docs", 108 | "section": "Docs" 109 | }, 110 | { 111 | "type": "docs_", 112 | "section": "Doc work - not included in Changelog", 113 | "hidden": true 114 | }, 115 | { 116 | "type": "style", 117 | "hidden": true 118 | }, 119 | { 120 | "type": "refactor", 121 | "hidden": true 122 | }, 123 | { 124 | "type": "perf", 125 | "hidden": true 126 | }, 127 | { 128 | "type": "test", 129 | "hidden": true 130 | }, 131 | { 132 | "type": "revert", 133 | "hidden": true 134 | } 135 | ] 136 | }, 137 | "author": "Amazon Alexa Skills Kit SDK team", 138 | "repository": "github:alexa/ask-sdk-controls", 139 | "license": "Apache-2.0" 140 | } 141 | -------------------------------------------------------------------------------- /src/commonControls/dateRangeControl/DateHelper.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | // transform date to yyyy-mm-dd dateToAlexaDateFormat 15 | export function dateToAlexaDateFormat(date: Date): string { 16 | const mm = date.getMonth() + 1; 17 | const mmString: string = mm >= 10 ? `${mm}` : `0${mm}`; 18 | const dd = date.getDate(); 19 | const ddString: string = dd >= 10 ? `${dd}` : `0${dd}`; 20 | return `${date.getFullYear()}-${mmString}-${ddString}`; 21 | } 22 | 23 | // transform yyyy-mm-dd to Date 24 | // TODO: Handle all ALEXA.DATE format such as '2015-W49-WE' 25 | export function alexaDateFormatToDate(date: string): Date { 26 | return new Date(date); 27 | } 28 | 29 | // Given a specific month and year, find the last date of that month 30 | // E.G. given (2, 2020) returns 28 31 | export function getDaysInMonth(m: number, y: number) { 32 | return new Date(y, m - 1, 0).getDate(); 33 | } 34 | 35 | /** 36 | * Find Start / End date of the input date range 37 | * @param date - Alexa date format string input E.G. 2019 / 2019-04 / 2019-04-01 38 | * @param start - Flag to determine if the output should be the start / end of the input date range 39 | */ 40 | // TODO: Handle all ALEXA.DATE format such as '2015-W49-WE' 41 | export function findEdgeDateOfDateRange(date: string, start: boolean): string { 42 | const yy = getYear(date); 43 | const mm = date.charAt(4) === '-' ? getMonth(date) : undefined; 44 | const dd = date.charAt(7) === '-' ? getDay(date) : undefined; 45 | if (mm === undefined) { 46 | return start ? `${yy}-01-01` : `${yy}-12-31`; 47 | } else if (dd === undefined) { 48 | const mmString: string = mm < 10 ? `0${mm}` : `${mm}`; 49 | return start ? `${yy}-${mmString}-01` : `${yy}-${mmString}-${getDaysInMonth(mm, yy)}`; 50 | } 51 | return date; 52 | } 53 | 54 | export function getYear(date: string): number { 55 | const yy = parseInt(date.substring(0, 4), 10); 56 | return yy; 57 | } 58 | 59 | export function getMonth(date: string): number { 60 | const mm = parseInt(date.substring(5, 7), 10); 61 | return mm; 62 | } 63 | 64 | export function getDay(date: string): number { 65 | const dd = parseInt(date.substring(8, 10), 10); 66 | return dd; 67 | } 68 | 69 | export function getEndDateOfRange(date: string): Date { 70 | return alexaDateFormatToDate(findEdgeDateOfDateRange(date, false)); 71 | } 72 | 73 | export function getStartDateOfRange(date: string): Date { 74 | return alexaDateFormatToDate(findEdgeDateOfDateRange(date, true)); 75 | } 76 | 77 | export function getUTCDate(date: Date): Date { 78 | // Get the offset between local time zone and UTC in minutes 79 | const offset: number = date.getTimezoneOffset(); 80 | const utcTime = date.getTime() + offset * 60000; 81 | 82 | return new Date(utcTime); 83 | } 84 | -------------------------------------------------------------------------------- /src/commonControls/numberControl/NumberControlBuiltIns.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | import i18next from 'i18next'; 15 | import { ControlInput } from '../..'; 16 | import { NumberControlState } from './NumberControl'; 17 | 18 | export namespace NumberControlBuiltIns { 19 | export function defaultValidationFailureText(): (value?: number) => string { 20 | return (value?: number) => i18next.t('NUMBER_CONTROL_DEFAULT_APL_INVALID_VALUE', { value }); 21 | } 22 | 23 | export const confirmMostLikelyMisunderstandingInputs = (state: NumberControlState, input: ControlInput) => 24 | defaultMostLikelyMisunderstandingFunc(state.value) !== undefined; 25 | 26 | export function defaultMostLikelyMisunderstandingFunc(value: number): number | undefined { 27 | // Static map of common ambiguous pairs among integers misinterpreted by NLU. 28 | const ambiguousPairs: { [key: number]: number } = { 29 | 13: 30, 30 | 14: 40, 31 | 15: 50, 32 | 16: 60, 33 | 17: 70, 34 | 18: 80, 35 | 19: 90, 36 | 30: 13, 37 | 40: 14, 38 | 50: 15, 39 | 60: 16, 40 | 70: 17, 41 | 80: 18, 42 | 90: 19, 43 | 113: 130, 44 | 114: 140, 45 | 115: 150, 46 | 116: 160, 47 | 117: 170, 48 | 118: 180, 49 | 119: 190, 50 | 130: 113, 51 | 140: 114, 52 | 150: 115, 53 | 160: 160, 54 | 170: 170, 55 | 180: 118, 56 | 190: 119, 57 | }; 58 | return ambiguousPairs[value]; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/constants/Strings.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | /** 15 | * The strings associated with various built-ins 16 | */ 17 | export namespace Strings { 18 | /** 19 | * Built-in feedback slot values 20 | */ 21 | export enum Feedback { 22 | Affirm = 'builtin_affirm', 23 | Disaffirm = 'builtin_disaffirm', 24 | } 25 | 26 | /** 27 | * Built-in action slot values 28 | */ 29 | export enum Action { 30 | Set = 'builtin_set', 31 | Change = 'builtin_change', 32 | Select = 'builtin_select', 33 | Start = 'builtin_start', 34 | Restart = 'builtin_restart', 35 | Resume = 'builtin_resume', 36 | GoBack = 'builtin_goBack', 37 | Complete = 'builtin_complete', 38 | None = 'builtin_none', 39 | Add = 'builtin_add', 40 | Remove = 'builtin_remove', 41 | Delete = 'builtin_delete', 42 | Ignore = 'builtin_ignore', 43 | Replace = 'builtin_replace', 44 | Clear = 'builtin_clear', 45 | } 46 | 47 | /** 48 | * Built-in target slot values 49 | */ 50 | export enum Target { 51 | It = 'builtin_it', 52 | Start = 'builtin_start', 53 | End = 'builtin_end', 54 | Choice = 'builtin_choice', 55 | // eslint-disable-next-line id-blacklist 56 | Number = 'builtin_number', 57 | Date = 'builtin_date', 58 | StartDate = 'builtin_start_date', 59 | EndDate = 'builtin_end_date', 60 | DateRange = 'builtin_date_range', 61 | Questionnaire = 'builtin_questionnaire', 62 | } 63 | 64 | export const Conjunction = 'builtin_conjunction'; 65 | export const Tail = 'builtin_tail'; 66 | export const Head = 'builtin_head'; 67 | export const Preposition = 'builtin_preposition'; 68 | } 69 | -------------------------------------------------------------------------------- /src/controls/ComponentModeControlManager.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | import { IControlManager } from '..'; 15 | import { APLMode } from '../responseGeneration/AplMode'; 16 | import { ControlResponseBuilder } from '../responseGeneration/ControlResponseBuilder'; 17 | import { ControlInput } from './ControlInput'; 18 | import { ControlManager, ControlManagerProps, renderActsInSequence } from './ControlManager'; 19 | import { ControlResult } from './ControlResult'; 20 | import { ControlServices } from './ControlServices'; 21 | 22 | const MODULE_NAME = 'AskSdkControls:ComponentModeControlManager'; 23 | 24 | /** 25 | * ControlManager used to render APL in Component Mode. 26 | */ 27 | export abstract class ComponentModeControlManager extends ControlManager implements IControlManager { 28 | constructor(props?: ControlManagerProps) { 29 | super(props); 30 | this.log = ControlServices.getLogger(MODULE_NAME); 31 | } 32 | 33 | async render( 34 | result: ControlResult, 35 | input: ControlInput, 36 | controlResponseBuilder: ControlResponseBuilder, 37 | ): Promise { 38 | // set the APL mode to Component 39 | // Required to disable addRenderAPLDirective on control.renderActs() 40 | controlResponseBuilder.aplMode = APLMode.COMPONENT; 41 | 42 | await renderActsInSequence(result.acts, input, controlResponseBuilder); 43 | await this.renderAPL(result, input, controlResponseBuilder); 44 | } 45 | 46 | /** 47 | * Render an APL template. 48 | * 49 | * @param input - Input 50 | * @param controlResponseBuilder - Response builder 51 | */ 52 | abstract renderAPL( 53 | result: ControlResult, 54 | input: ControlInput, 55 | controlResponseBuilder: ControlResponseBuilder, 56 | ): Promise; 57 | } 58 | -------------------------------------------------------------------------------- /src/controls/ControlInput.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | import { HandlerInput } from 'ask-sdk-core'; 15 | import { Request } from 'ask-sdk-model'; 16 | import _ from 'lodash'; 17 | import { IControlInput } from './interfaces/IControlInput'; 18 | import { IControl } from './interfaces/IControl'; 19 | import { InputModality, ResponseStyle } from '../modality/ModalityEvaluation'; 20 | 21 | /** 22 | * Properties for creating a ControlInput instance. 23 | */ 24 | export interface ControlInputProps { 25 | /** 26 | * The core input for the request. 27 | */ 28 | handlerInput: HandlerInput; 29 | 30 | /** 31 | * The turn number of the current request. 32 | */ 33 | turnNumber: number; 34 | 35 | /** 36 | * A map of Control IDs to Controls for the current control tree. 37 | */ 38 | controlMap: { [index: string]: IControl }; 39 | 40 | /** 41 | * The style in which the skill is recommended to respond to the current request, 42 | * e.g. with a screen or voice interface, based on how the user responded to 43 | * previous requests. 44 | */ 45 | suggestedResponseStyle: ResponseStyle; 46 | 47 | /** 48 | * The history of how the user responded to previous requests, 49 | * e.g. with touch or voice. 50 | */ 51 | inputModalityHistory: InputModality[]; 52 | } 53 | 54 | /** 55 | * Defines an expanded input object passed around during processing by Controls. 56 | * 57 | * Purpose: 58 | * * Provides access to the HandlerInput and also conveniences such as simplified access 59 | * to the Request object, a turn counter and a map of all controls in the control tree. 60 | */ 61 | export class ControlInput implements IControlInput { 62 | /** 63 | * The input from {@link CustomSkillRequestHandler} 64 | */ 65 | readonly handlerInput: HandlerInput; 66 | 67 | /** 68 | * The request object 69 | * 70 | * This is a convenience for `handlerInput.requestEnvelope.request`. 71 | */ 72 | readonly request: Request; 73 | 74 | /** 75 | * The number of incoming requests during the user session. 76 | */ 77 | readonly turnNumber: number; 78 | 79 | /** 80 | * The style in which the skill is recommended to respond to the request, 81 | * e.g. with a screen or voice modality. 82 | */ 83 | readonly suggestedResponseStyle: ResponseStyle; 84 | 85 | /** 86 | * The history of what modality the user responded with for each turn, 87 | * e.g. voice or touch. 88 | */ 89 | readonly inputModalityHistory: InputModality[]; 90 | 91 | /** 92 | * All the controls of the control tree, indexed by controlID. 93 | * 94 | * Usage: 95 | * * This provides direct access to all other controls in the control tree which 96 | * can be convenient for occasional use but it increases the coupling of specific controls. 97 | * * When controls are close to one another, it is preferable to have their parents 98 | * coordinate data transfer, e.g. by get() from one and set() on the other. 99 | * * If controls that are not close to one another routinely need to share information 100 | * it would be best to create an external datastore. Consider Redux or similar solutions. 101 | */ 102 | readonly controls: { [index: string]: IControl }; 103 | 104 | constructor(props: ControlInputProps) { 105 | this.handlerInput = props.handlerInput; 106 | this.request = this.handlerInput.requestEnvelope.request; 107 | this.turnNumber = props.turnNumber; 108 | this.controls = props.controlMap; 109 | this.suggestedResponseStyle = props.suggestedResponseStyle; 110 | this.inputModalityHistory = props.inputModalityHistory; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/controls/ControlResult.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | import { SessionBehavior } from '../runtime/SessionBehavior'; 15 | import { ISystemAct, SystemAct } from '../systemActs/SystemAct'; 16 | import { IControlResult } from './interfaces/IControlResult'; 17 | 18 | /** 19 | * Represents "what" the system should communicate to the user. 20 | * 21 | * This is the output object from the handle and initiative phases and is the 22 | * intermediate representation passed to the render phase. 23 | * 24 | * If the session is not being closed, the ControlResult must contain exactly 25 | * one `InitiativeAct`. 26 | */ 27 | export class ControlResult implements IControlResult { 28 | acts: SystemAct[]; 29 | sessionBehavior: SessionBehavior; 30 | 31 | // jsDoc - see class jsDoc. 32 | constructor(acts: SystemAct[], sessionBehavior: SessionBehavior) { 33 | this.acts = acts; 34 | this.sessionBehavior = sessionBehavior; 35 | } 36 | 37 | // jsDoc - see Object. 38 | toString(): string { 39 | return stringifyItems(this.acts); 40 | } 41 | 42 | /** 43 | * Determines if the result has an initiative act. 44 | */ 45 | hasInitiativeAct(): boolean { 46 | return controlResultHasInitiativeAct(this); 47 | } 48 | } 49 | 50 | /** 51 | * Builder that creates a ControlResult. 52 | */ 53 | export class ControlResultBuilder { 54 | acts: SystemAct[]; 55 | sessionBehavior: SessionBehavior; 56 | 57 | constructor(acts?: SystemAct[]) { 58 | this.acts = acts ?? []; 59 | this.sessionBehavior = SessionBehavior.OPEN; 60 | } 61 | 62 | build(): ControlResult { 63 | return new ControlResult(this.acts, this.sessionBehavior); 64 | } 65 | 66 | /** 67 | * Add a system act. 68 | * 69 | * Usage: 70 | * * If the session needs to be ended, also call `resultBuilder.endSession()`. 71 | * 72 | * @param act - System act. 73 | */ 74 | addAct(act: SystemAct): this { 75 | if (this.hasInitiativeAct() && act.takesInitiative) { 76 | throw new Error( 77 | `Result already contains an initiative item. (If handle() produced an initiative item and takeInitiative also ran, a common issue is missing 'await').` + 78 | `Trying to add ${act.toString()}. Existing items: ${stringifyItems(this.acts)}`, 79 | ); 80 | } 81 | this.acts.push(act); 82 | return this; 83 | } 84 | 85 | /** 86 | * End the user session. 87 | * 88 | * See 89 | * https://developer.amazon.com/en-GB/docs/alexa/custom-skills/manage-skill-session-and-session-attributes.html#session-lifecycle 90 | * 91 | * Framework behavior: 92 | * * this will cause the framework to call 93 | * `ask-core.ResponseBuilder.withShouldEndSession(true)`. 94 | * 95 | * Skill/device behavior: 96 | * * Subsequent utterances will not be routed to the skill. 97 | * * A new launch command will start a fresh session. 98 | * 99 | */ 100 | endSession(): void { 101 | this.sessionBehavior = SessionBehavior.END; 102 | } 103 | 104 | /** 105 | * Place the user session in the idle state. 106 | * 107 | * https://developer.amazon.com/en-GB/docs/alexa/custom-skills/manage-skill-session-and-session-attributes.html#session-lifecycle 108 | * 109 | * * Framework behavior: 110 | * * this will cause the framework to call 111 | * `ask-core.ResponseBuilder.withShouldEndSession(undefined)`. 112 | * 113 | * Skill/device behavior: 114 | * * The session remains alive but the microphone is closed. 115 | * * The user can interact with the skill but must use the wake-word. 116 | */ 117 | enterIdleState(): void { 118 | this.sessionBehavior = SessionBehavior.IDLE; 119 | } 120 | 121 | // jsDoc - see Object 122 | toString(): string { 123 | return stringifyItems(this.acts); 124 | } 125 | 126 | /** 127 | * Determines if the result has an initiative act. 128 | */ 129 | hasInitiativeAct(): boolean { 130 | return controlResultHasInitiativeAct(this); 131 | } 132 | } 133 | 134 | function controlResultHasInitiativeAct(result: IControlResult): boolean { 135 | return result.acts.find((item) => item.takesInitiative) !== undefined; 136 | } 137 | 138 | function stringifyItems(items: ISystemAct[]) { 139 | const itemStrs: string[] = []; 140 | for (const item of items) { 141 | itemStrs.push(item.toString()); 142 | } 143 | return `[${itemStrs.join(', ')}]`; 144 | } 145 | -------------------------------------------------------------------------------- /src/controls/ControlServices.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | import { DefaultLoggerFactory } from '../logging/DefaultLoggerFactory'; 15 | import { ILogger } from './interfaces/ILogger'; 16 | import { ILoggerFactory } from './interfaces/ILoggerFactory'; 17 | 18 | /** 19 | * Defines the props of Control services. 20 | */ 21 | export interface ControlServicesProps { 22 | /** 23 | * Logger instance used across controls. 24 | */ 25 | logger: ILoggerFactory; 26 | } 27 | 28 | /** 29 | * Defines the ControlService used to customize logger implementation used 30 | * across the framework. 31 | */ 32 | export namespace ControlServices { 33 | /** 34 | * Provides default services 35 | */ 36 | let defaults: ControlServicesProps; 37 | 38 | /** 39 | * Function to override the default Control service props. 40 | * 41 | * @param customServices - Input custom controls services 42 | */ 43 | export function setDefaults(customServices: ControlServicesProps): void { 44 | defaults = customServices; 45 | } 46 | 47 | /** 48 | * Create a default control service object and set it to defaults 49 | * which includes a default logger factory. 50 | */ 51 | class DefaultControlServices { 52 | services: ControlServicesProps; 53 | 54 | constructor() { 55 | this.services = { 56 | logger: new DefaultLoggerFactory(), 57 | }; 58 | } 59 | } 60 | 61 | /** 62 | * Function to retrieve Control services. 63 | * 64 | * @returns Control Service 65 | */ 66 | export function getDefaults(): ControlServicesProps { 67 | if (defaults !== undefined) { 68 | return defaults; 69 | } 70 | const defaultServices = new DefaultControlServices(); 71 | return defaultServices.services; 72 | } 73 | 74 | /** 75 | * Function to retrieve logger implementation with a provided 76 | * namespace. 77 | * 78 | * Usage: 79 | * - ControlServices.getLogger('AskSdkControls:ControlManager'); 80 | * returns a logger implementation containing methods to log 81 | * info/debug/error and sanitize restrictive information. 82 | * 83 | * 84 | * @param namespace - Input Module Name 85 | */ 86 | export function getLogger(namespace: string): ILogger { 87 | if (namespace !== undefined) { 88 | return ControlServices.getDefaults().logger.getLogger(namespace); 89 | } 90 | throw new Error('Invalid Namespace provided'); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/controls/Validation.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | import { ControlInput } from './ControlInput'; 15 | 16 | /** 17 | * State validation function 18 | * 19 | * Takes a Control state object and returns `true` if the state passes validation. If 20 | * validation fails, a `ValidationResult` object is returned instead. 21 | */ 22 | export type StateValidationFunction = ( 23 | state: TState, 24 | input: ControlInput, 25 | ) => true | ValidationFailure | Promise; 26 | 27 | /** 28 | * Describes a validation failure. 29 | * 30 | * Usage: 31 | * - A reason code should be provided that uniquely describes the kind of validation 32 | * failure. A reason code is useful for context-specific rendering and other business 33 | * function. It is not mandatory as for some simple cases it may be duplicative to 34 | * provide both a reason code and a single rendering of that reason code. 35 | * 36 | * - A rendered reason for situations where the rendered form is not context-sensitive and 37 | * can be conveniently provided when the ValidationFailure is instantiated. 38 | */ 39 | export type ValidationFailure = { 40 | /** 41 | * A code representing what validation failed. 42 | * 43 | * Usage: 44 | * - use reasonCode for business logic and transform to a prompt during the rendering 45 | * phase. 46 | */ 47 | reasonCode?: string; 48 | 49 | /** 50 | * A rendered prompt fragment that can be directly included in the `Response`. 51 | * 52 | * Usage: 53 | * - If convenient, generate the prompt fragment at instantiation time. 54 | * - A renderedReason should not be used in logic or further transformed. 55 | */ 56 | renderedReason?: string; 57 | }; 58 | 59 | /** 60 | * Helper to evaluate a prop that accepts one or more StateValidationFunction functions. 61 | * @param validationProp - either a single StateValidationFunction or an array of 62 | * the same. 63 | * @param state - The control's state object. 64 | * @param input - ControlInput. 65 | */ 66 | export async function evaluateValidationProp( 67 | validationProp: StateValidationFunction | Array>, 68 | state: TState, 69 | input: ControlInput, 70 | ): Promise { 71 | const listOfValidationFunc: Array> = 72 | typeof validationProp === 'function' ? [validationProp] : validationProp; 73 | for (const validationFunction of listOfValidationFunc) { 74 | const validationResult: true | ValidationFailure = await validationFunction(state, input); 75 | if (validationResult !== true) { 76 | return validationResult; 77 | } 78 | } 79 | return true; 80 | } 81 | -------------------------------------------------------------------------------- /src/controls/interfaces/IContainerControl.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | import { IControl } from './IControl'; 15 | 16 | /** 17 | * Defines a Control that has child controls. 18 | * 19 | * This is the minimal definition required by the Runtime (ControlHandler) 20 | * See `ContainerControl` for the actual class used by Control implementations. 21 | */ 22 | export interface IContainerControl extends IControl { 23 | children: IControl[]; 24 | } 25 | 26 | /** 27 | * Type-guard function for the @see IContainerControl interface. 28 | * 29 | * @param control - Control to test 30 | * @returns true if the argument implements the @see ContainerControl interface. 31 | */ 32 | export function isContainerControl(control: IControl): control is IContainerControl { 33 | return (control as any).children instanceof Array; 34 | } 35 | -------------------------------------------------------------------------------- /src/controls/interfaces/IControl.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | import { IControlInput } from './IControlInput'; 15 | import { IControlResultBuilder } from './IControlResultBuilder'; 16 | 17 | /** 18 | * Defines a Control object that manages state and dialog behavior. 19 | * 20 | * This is the minimal definition required by the Runtime (ControlHandler) 21 | * See `Control` for the actual class used by Control implementations. 22 | */ 23 | export interface IControl { 24 | id: string; 25 | 26 | /** 27 | * Determines if the Control or one of its children can consume the request. 28 | */ 29 | canHandle(input: IControlInput): boolean | Promise; 30 | 31 | /** 32 | * Handles the request. 33 | */ 34 | handle(input: IControlInput, resultBuilder: IControlResultBuilder): void | Promise; 35 | 36 | /** 37 | * Determines if the Control can take the initiative. 38 | */ 39 | canTakeInitiative(input: IControlInput): boolean | Promise; 40 | 41 | /** 42 | * Takes the initiative by adding an InitiativeAct to the result. 43 | */ 44 | takeInitiative(input: IControlInput, resultBuilder: IControlResultBuilder): void | Promise; 45 | 46 | /** TODO */ 47 | reestablishState(state: any, controlStateMap: { [index: string]: any }): void; 48 | 49 | /** 50 | * Gets the Control's state as an object that is serializable. 51 | * 52 | * Framework behavior: 53 | * - The object will be serialized via a call to `JSON.stringify(obj)` 54 | */ 55 | getSerializableState(): any; 56 | 57 | /** 58 | * Sets the state from a serialized state object 59 | */ 60 | setSerializableState(serializedState: any): void; 61 | } 62 | -------------------------------------------------------------------------------- /src/controls/interfaces/IControlInput.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | import { HandlerInput } from 'ask-sdk-core'; 15 | 16 | /** 17 | * Defines the input object passed around during processing by Controls. 18 | * 19 | * This is the minimal definition required by the Runtime (ControlHandler) 20 | * See `ControlInput` for the actual class used by Control implementations. 21 | */ 22 | export interface IControlInput { 23 | readonly handlerInput: HandlerInput; 24 | readonly turnNumber: number; 25 | } 26 | -------------------------------------------------------------------------------- /src/controls/interfaces/IControlManager.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | import { HandlerInput } from 'ask-sdk-core'; 15 | import { InputModality, ResponseStyle } from '../../modality/ModalityEvaluation'; 16 | import { ControlResponseBuilder } from '../../responseGeneration/ControlResponseBuilder'; 17 | import { IControl } from './IControl'; 18 | import { IControlInput } from './IControlInput'; 19 | import { IControlResult } from './IControlResult'; 20 | 21 | /** 22 | * Manages a skill built with Controls. 23 | * 24 | * This is the minimal definition required by the Runtime (ControlHandler) 25 | * See `ControlManager` for the actual class used by implementations. 26 | */ 27 | export interface IControlManager { 28 | /** 29 | * Creates the tree of controls to handle state management and dialog 30 | * decisions for the skill. 31 | * 32 | * Usage: 33 | * - A single control is legal and will suffice for small skills. For larger 34 | * skills a tree of controls structured using @see ContainerControl will 35 | * help manage skill complexity. 36 | * 37 | * - In advanced scenarios with dynamic control tree shapes, this method 38 | * should produce only the static section of the tree. The dynamic 39 | * portion is 40 | * expected to produce a tree that is identical to the tree at the end of 41 | * the previous turn. The serializable control state can be inspected as 42 | * necessary. 43 | * 44 | * - If state objects are re-established during this method, the subsequent 45 | * 46 | * @param controlStateMap - Map of control state objects keyed by 47 | * `controlId` This is provided for advanced cases 48 | * in which the tree has a dynamic shape based on 49 | * the application state. 50 | * @returns A Control that is either a single @see Control or a @see 51 | * ContainerControl that is the root of a tree. 52 | */ 53 | createControlTree(): IControl; 54 | 55 | reestablishControlStates(rootControl: IControl, stateMap: { [key: string]: any }): void; 56 | 57 | /** 58 | * Builds the response. 59 | * 60 | * @param result - The result to be rendered 61 | * @param input - Input 62 | * @param responseBuilder - Response builder 63 | */ 64 | render( 65 | result: IControlResult, 66 | input: IControlInput, 67 | responseBuilder: ControlResponseBuilder, 68 | ): void | Promise; 69 | 70 | /** 71 | * Custom handling of a internal error. 72 | * 73 | * @param input - ControlInput object or undefined if an error occurs early in processing 74 | * @param error - Error object 75 | * @param responseBuilder - Response builder 76 | */ 77 | handleInternalError?( 78 | input: IControlInput | undefined, 79 | error: any, 80 | responseBuilder: ControlResponseBuilder, 81 | ): void; 82 | 83 | /** 84 | * 85 | */ 86 | loadControlStateMap(handlerInput: HandlerInput): Promise<{ [key: string]: any }>; 87 | 88 | /** 89 | * 90 | */ 91 | saveControlStateMap(state: any, handlerInput: HandlerInput): Promise; 92 | 93 | /** 94 | * Determines the input modality of the current turn, based on the 95 | * currently-configured evaluator function. 96 | * @param handlerInput - Input for the current turn. 97 | * @returns InputModality - Input modality determined for the current turn. 98 | */ 99 | evaluateInputModality(handlerInput: HandlerInput): InputModality; 100 | 101 | /** 102 | * Determines the recommended response style for the current turn, based on the 103 | * current-configured evaluator function. 104 | * @param handlerInput - Input for the current turn. 105 | * @param history - History of input modality of previous turns. 106 | * @returns ResponseStyle - Recommended response style for the current turn. 107 | */ 108 | evaluateResponseStyle(input: HandlerInput, history: InputModality[]): ResponseStyle; 109 | } 110 | -------------------------------------------------------------------------------- /src/controls/interfaces/IControlResult.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | import { SessionBehavior } from '../../runtime/SessionBehavior'; 15 | import { ISystemAct } from '../../systemActs/SystemAct'; 16 | 17 | /** 18 | * Defines the result of processing a turn using Controls. 19 | * 20 | * This is the minimal definition required by the Runtime (ControlHandler) See 21 | * `ControlResult` for the actual class used by Control implementations. 22 | */ 23 | export interface IControlResult { 24 | acts: ISystemAct[]; 25 | sessionBehavior: SessionBehavior; 26 | hasInitiativeAct(): boolean; 27 | } 28 | -------------------------------------------------------------------------------- /src/controls/interfaces/IControlResultBuilder.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | import { IControlResult } from './IControlResult'; 15 | import { SessionBehavior } from '../../runtime/SessionBehavior'; 16 | import { ISystemAct } from '../../systemActs/SystemAct'; 17 | 18 | /** 19 | * Defines a builder used to prepare a ControlResult. 20 | * 21 | * This is the minimal definition required by the Runtime (ControlHandler) See 22 | * `ControlResultBuilder` for the actual class used by Control implementations. 23 | */ 24 | export interface IControlResultBuilder { 25 | acts: ISystemAct[]; 26 | sessionBehavior: SessionBehavior; 27 | hasInitiativeAct(): boolean; 28 | build(): IControlResult; 29 | } 30 | -------------------------------------------------------------------------------- /src/controls/interfaces/ILogger.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | /** 15 | * Defines the Logger interface to define the logging format and 16 | * log level used across the framework. 17 | */ 18 | export interface ILogger { 19 | moduleName: string; 20 | 21 | /** 22 | * Log a message as an "error". 23 | * @param message - Message 24 | */ 25 | error(message: string): void; 26 | 27 | /** 28 | * Log a message as an "informational" message. 29 | * @param message - Message 30 | */ 31 | info(message: string): void; 32 | 33 | /** 34 | * Log a message as an "warning". 35 | * @param message - Message 36 | */ 37 | warn(message: string): void; 38 | 39 | /** 40 | * Log a message as an "low-level debug message". 41 | * @param message - Message 42 | */ 43 | debug(message: string): void; 44 | 45 | /** 46 | * Log an object with pretty print formatting and also 47 | * masking sensitive information if required 48 | * 49 | * @param id - Unique label. 50 | * @param object - Object to be logged 51 | */ 52 | logObject(level: string, id: string, object: any, stringify?: boolean): void; 53 | 54 | /** 55 | * Function that returns string with special characters to 56 | * mask restrictive information. 57 | * 58 | * @param message - Logging message 59 | */ 60 | sanitize(message: any): string; 61 | } 62 | -------------------------------------------------------------------------------- /src/controls/interfaces/ILoggerFactory.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | import { ILogger } from './ILogger'; 15 | 16 | /** 17 | * Define the Logger Factory required on the 18 | * framework to customize the logging. 19 | */ 20 | export interface ILoggerFactory { 21 | getLogger(namespace: string): ILogger; 22 | } 23 | -------------------------------------------------------------------------------- /src/controls/mixins/ControlStateDiagramming.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | import { IControl } from '../interfaces/IControl'; 15 | 16 | /** 17 | * Optional interface for Controls that wish to add detail to diagnostic output. 18 | */ 19 | export interface ControlStateDiagramming { 20 | /** 21 | * Generates a human-readable representation of the state. 22 | * 23 | * @param this - Control 24 | * @returns Human-readable state 25 | */ 26 | stringifyStateForDiagram(this: IControl): string; 27 | } 28 | 29 | /** 30 | * Type-guard for the ControlStateDiagramming interface. 31 | * 32 | * @param arg - Object to test 33 | * @returns `true` if the argument implements the `ControlStateDiagramming` 34 | * interface. 35 | */ 36 | export function implementsControlStateDiagramming(arg: any): arg is ControlStateDiagramming { 37 | return typeof arg.stringifyStateForDiagram === 'function'; 38 | } 39 | -------------------------------------------------------------------------------- /src/controls/mixins/InteractionModelContributor.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | import { ControlInteractionModelGenerator } from '../../interactionModelGeneration/ControlInteractionModelGenerator'; 15 | import { ModelData } from '../../interactionModelGeneration/ModelTypes'; 16 | 17 | /** 18 | * Optional interface for Controls that wish to customize Interaction Model. 19 | */ 20 | export interface InteractionModelContributor { 21 | /** 22 | * Updates the interaction model. 23 | * 24 | * Usage: 25 | * - Controls should add any content to the interaction model that they rely 26 | * on. In particular, intents, slot types and slot values. 27 | * - The imData parameter contains localized information for all known slot 28 | * types and intents. The `updateInteractionModel()` method should use 29 | * this to obtain the raw data that is added to the generator. 30 | * 31 | * @param generator - Interaction Model generator 32 | * @param imData - Localized data for all known intents and slots. 33 | * 34 | */ 35 | updateInteractionModel(generator: ControlInteractionModelGenerator, imData: ModelData): void; 36 | } 37 | 38 | /** 39 | * Type-guard for the InteractionModelContributor interface. 40 | * 41 | * @param arg - Object to test 42 | * @returns `true` if the argument implements the InteractionModelContributor 43 | * interface. 44 | */ 45 | export function implementsInteractionModelContributor(arg: any): arg is InteractionModelContributor { 46 | return typeof arg.updateInteractionModel === 'function'; 47 | } 48 | -------------------------------------------------------------------------------- /src/intents/AmazonBuiltInIntent.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | /** 15 | * Constants for Alexa's built-in intents. 16 | * 17 | * (This is not a complete list) 18 | */ 19 | //TODO: naming should be consistent with AmazonBuiltInSlotType. pick one. 20 | export enum AmazonIntent { 21 | CancelIntent = 'AMAZON.CancelIntent', 22 | FallbackIntent = 'AMAZON.FallbackIntent', 23 | HelpIntent = 'AMAZON.HelpIntent', 24 | MoreIntent = 'AMAZON.MoreIntent', 25 | NavigateHomeIntent = 'AMAZON.NavigateHomeIntent', 26 | NavigateSettingsIntent = 'AMAZON.NavigateSettingsIntent', 27 | NextIntent = 'AMAZON.NextIntent', 28 | NoIntent = 'AMAZON.NoIntent', 29 | PageUpIntent = 'AMAZON.PageUpIntent', 30 | PageDownIntent = 'AMAZON.PageDownIntent', 31 | PreviousIntent = 'AMAZON.PreviousIntent', 32 | PauseIntent = 'AMAZON.PauseIntent', 33 | ResumeIntent = 'AMAZON.ResumeIntent', 34 | ScrollRightIntent = 'AMAZON.ScrollRightIntent', 35 | ScrollDownIntent = 'AMAZON.ScrollDownIntent', 36 | ScrollLeftIntent = 'AMAZON.ScrollLeftIntent', 37 | ScrollUpIntent = 'AMAZON.ScrollUpIntent', 38 | StopIntent = 'AMAZON.StopIntent', 39 | YesIntent = 'AMAZON.YesIntent', 40 | } 41 | -------------------------------------------------------------------------------- /src/intents/AmazonBuiltInSlotType.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | /** 15 | * Constants for Alexa's built-in slot types. 16 | * 17 | * (This is not a complete list.) 18 | */ 19 | 20 | //TODO: naming should be consistent with AmazonIntent. pick one. 21 | export enum AmazonBuiltInSlotType { 22 | DATE = 'AMAZON.DATE', 23 | DURATION = 'AMAZON.DURATION', 24 | NUMBER = 'AMAZON.NUMBER', 25 | FOUR_DIGIT_NUMBER = 'AMAZON.FOUR_DIGIT_NUMBER', 26 | ORDINAL = 'AMAZON.Ordinal', 27 | PHONENUMBER = 'AMAZON.PhoneNumber', 28 | TIME = 'AMAZON.TIME', 29 | SEARCH_QUERY = 'AMAZON.SearchQuery', 30 | } 31 | -------------------------------------------------------------------------------- /src/intents/BaseControlIntent.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | import { v1 } from 'ask-smapi-model'; 14 | import Intent = v1.skill.interactionModel.Intent; 15 | 16 | /** 17 | * Abstract base class for the "Control Intents" 18 | */ 19 | export abstract class BaseControlIntent { 20 | /** 21 | * Name of the intent as it will appear in the interaction model. 22 | * 23 | * Default: the class name. 24 | */ 25 | name: string = this.constructor.name; 26 | 27 | /** 28 | * Generate a complete Intent object. 29 | */ 30 | abstract generateIntent(): Intent; 31 | } 32 | -------------------------------------------------------------------------------- /src/intents/GeneralControlIntent.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | import { Intent } from 'ask-sdk-model'; 15 | import { v1 } from 'ask-smapi-model'; 16 | import { SharedSlotType } from '../interactionModelGeneration/ModelTypes'; 17 | import { getSlotResolutions, IntentBuilder } from '../utils/IntentUtils'; 18 | import { BaseControlIntent } from './BaseControlIntent'; 19 | 20 | /** 21 | * Slot values conveyed by a GeneralControlIntent 22 | */ 23 | export interface GeneralControlIntentSlots { 24 | feedback?: string; 25 | action?: string; 26 | target?: string; 27 | } 28 | 29 | /** 30 | * Unpacks the complete intent object into a simpler representation. 31 | * 32 | * Note re "empty slots": 33 | * - Slots in the intent with no value appear in the intent object as "". 34 | * However, these are unpacked as **`undefined`** to be more explicit and ease 35 | * the implementation of predicates. 36 | * @param intent - Intent 37 | */ 38 | export function unpackGeneralControlIntent(intent: Intent): GeneralControlIntentSlots { 39 | if (!intent.name.startsWith('GeneralControlIntent')) { 40 | throw new Error(`Not a compatible intent : ${intent.name}`); 41 | } 42 | 43 | let feedback: string | undefined; 44 | let action: string | undefined; 45 | let target: string | undefined; 46 | 47 | for (const [name, slot] of Object.entries(intent.slots!)) { 48 | const slotObject = getSlotResolutions(slot); 49 | const slotValue = slotObject ? slotObject.slotValue : undefined; 50 | 51 | switch (name) { 52 | case 'feedback': 53 | feedback = slotValue; 54 | break; 55 | case 'filteredFeedback': 56 | feedback = slotValue; 57 | break; 58 | case 'action': 59 | action = slotValue; 60 | break; 61 | case 'target': 62 | target = slotValue; 63 | break; 64 | case 'head': 65 | break; 66 | case 'tail': 67 | break; 68 | 69 | default: 70 | throw new Error('not handled'); 71 | } 72 | } 73 | 74 | return { 75 | feedback, 76 | action, 77 | target, 78 | }; 79 | } 80 | 81 | /** 82 | * Intent that conveys feedback, action, and target but no value. 83 | * 84 | * This general-purpose intent conveys the intent for utterances such as: 85 | * - "The time" 86 | * - "Change the date" 87 | * - "Yes, that one" 88 | * - "No, change the event date" 89 | */ 90 | export class GeneralControlIntent extends BaseControlIntent { 91 | /** 92 | * Create Intent from specification of the slots 93 | */ 94 | static of(slots: GeneralControlIntentSlots): Intent { 95 | return IntentBuilder.of(this.prototype.constructor.name, slots); 96 | } 97 | 98 | // tsDoc: see BaseControlIntent 99 | generateIntent(): v1.skill.interactionModel.Intent { 100 | return { 101 | name: this.name, 102 | slots: this.generateSlots(), 103 | samples: [], 104 | }; 105 | } 106 | 107 | private generateSlots(): v1.skill.interactionModel.SlotDefinition[] { 108 | const slots: v1.skill.interactionModel.SlotDefinition[] = [ 109 | { 110 | name: 'feedback', 111 | type: SharedSlotType.FEEDBACK, 112 | }, 113 | { 114 | name: 'filteredFeedback', 115 | type: SharedSlotType.FILTERED_FEEDBACK, 116 | }, 117 | { 118 | name: 'action', 119 | type: SharedSlotType.ACTION, 120 | }, 121 | { 122 | name: 'target', 123 | type: SharedSlotType.TARGET, 124 | }, 125 | { 126 | name: 'head', 127 | type: SharedSlotType.HEAD, 128 | }, 129 | { 130 | name: 'tail', 131 | type: SharedSlotType.TAIL, 132 | }, 133 | ]; 134 | 135 | return slots; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/intents/OrdinalControlIntent.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | import { Intent } from 'ask-sdk-model'; 15 | import { v1 } from 'ask-smapi-model'; 16 | import { SharedSlotType } from '../interactionModelGeneration/ModelTypes'; 17 | import { getSlotResolutions, IntentBuilder } from '../utils/IntentUtils'; 18 | import { AmazonBuiltInSlotType } from './AmazonBuiltInSlotType'; 19 | import { BaseControlIntent } from './BaseControlIntent'; 20 | 21 | /** 22 | * Slot values conveyed by a OrdinalControlIntent 23 | */ 24 | export interface OrdinalControlIntentSlots { 25 | feedback?: string; 26 | action?: string; 27 | target?: string; 28 | 'AMAZON.Ordinal'?: string; 29 | } 30 | 31 | /** 32 | * Unpacks the complete intent object into a simpler representation. 33 | * 34 | * Note re "empty slots": 35 | * - Slots in the intent with no value appear in the intent object as "". 36 | * However, these are unpacked as **`undefined`** to be more explicit and ease 37 | * the implementation of predicates. 38 | * @param intent - Intent 39 | */ 40 | export function unpackOrdinalControlIntent(intent: Intent): OrdinalControlIntentSlots { 41 | if (!intent.name.startsWith('OrdinalControlIntent')) { 42 | throw new Error(`Not a compatible intent: ${intent.name}`); 43 | } 44 | 45 | let action: string | undefined; 46 | let feedback: string | undefined; 47 | let target: string | undefined; 48 | let valueStr: string | undefined; 49 | 50 | for (const [name, slot] of Object.entries(intent.slots!)) { 51 | const slotObject = getSlotResolutions(slot); 52 | const slotValue = slotObject ? slotObject.slotValue : undefined; 53 | 54 | switch (name) { 55 | case 'feedback': 56 | feedback = slotValue !== undefined ? slotValue : undefined; 57 | break; 58 | case 'action': 59 | action = slotValue !== undefined ? slotValue : undefined; 60 | break; 61 | case 'target': 62 | target = slotValue !== undefined ? slotValue : undefined; 63 | break; 64 | case 'AMAZON.Ordinal': 65 | valueStr = slotValue !== undefined ? slotValue : undefined; 66 | break; 67 | // default: ignore content-free slots 68 | } 69 | } 70 | 71 | return { 72 | feedback, 73 | action, 74 | target, 75 | 'AMAZON.Ordinal': valueStr, 76 | }; 77 | } 78 | 79 | /** 80 | * Intent that conveys feedback, action, target and an AMAZON.Ordinal value 81 | * 82 | * This general-purpose intent conveys the intent for utterances such as: 83 | * - "The time" 84 | * - "Change the date" 85 | * - "Yes, that one" 86 | * - "No, change the event date" 87 | */ 88 | export class OrdinalControlIntent extends BaseControlIntent { 89 | /** 90 | * Create Intent from specification of the slots 91 | */ 92 | static of(slots: OrdinalControlIntentSlots): Intent { 93 | return IntentBuilder.of(this.prototype.constructor.name, slots); 94 | } 95 | 96 | // tsDoc: see BaseControlIntent 97 | generateIntent(): v1.skill.interactionModel.Intent { 98 | return { 99 | name: this.name, 100 | slots: this.generateSlots(), 101 | samples: [], 102 | }; 103 | } 104 | 105 | private generateSlots(): v1.skill.interactionModel.SlotDefinition[] { 106 | const slots: v1.skill.interactionModel.SlotDefinition[] = [ 107 | { 108 | name: 'feedback', 109 | type: SharedSlotType.FEEDBACK, 110 | }, 111 | { 112 | name: 'action', 113 | type: SharedSlotType.ACTION, 114 | }, 115 | { 116 | name: 'target', 117 | type: SharedSlotType.TARGET, 118 | }, 119 | { 120 | name: 'AMAZON.Ordinal', 121 | type: AmazonBuiltInSlotType.ORDINAL, 122 | }, 123 | { 124 | name: 'preposition', 125 | type: SharedSlotType.PREPOSITION, 126 | }, 127 | { 128 | name: 'head', 129 | type: SharedSlotType.HEAD, 130 | }, 131 | { 132 | name: 'tail', 133 | type: SharedSlotType.TAIL, 134 | }, 135 | ]; 136 | 137 | return slots; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/interactionModelGeneration/ModelTypes.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | import { v1 } from 'ask-smapi-model'; 14 | import SlotType = v1.skill.interactionModel.SlotType; 15 | 16 | /** 17 | * Names of the built-in slot types used by ControlIntents 18 | */ 19 | export enum SharedSlotType { 20 | FEEDBACK = 'feedback', 21 | FILTERED_FEEDBACK = 'filteredFeedback', 22 | HEAD = 'head', 23 | TAIL = 'tail', 24 | ACTION = 'action', 25 | TARGET = 'target', 26 | CONJUNCTION = 'conjunction', 27 | PREPOSITION = 'preposition', 28 | } 29 | 30 | /** 31 | * Localized information Names of the built-in slot types used by ControlIntents 32 | */ 33 | export interface ModelData { 34 | slotTypes: SlotType[]; 35 | intentValues: IntentUtterances[]; // TODO: naming: review name 36 | } 37 | 38 | export interface SlotValue { 39 | synonyms: string[]; 40 | id: string; 41 | } 42 | 43 | export interface IntentUtterances { 44 | name: string; 45 | samples: string[]; 46 | } 47 | 48 | export type ModelDataMap = { [locale: string]: ModelData }; 49 | -------------------------------------------------------------------------------- /src/intl/EnglishGrammar.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | /** 15 | * Utilities for prompt generation in English 16 | */ 17 | export namespace EnglishGrammar { 18 | /** 19 | * Choose the article to use in English noun phrases (a vs an) 20 | * 21 | * This is only a partial implementation. 22 | * See https://www.grammar.com/a-vs-an-when-to-use/ 23 | * @param antecedent - Antecedent 24 | */ 25 | export function article(antecedent: string) { 26 | const firstLetter = antecedent.trim()[0]; 27 | if (['a', 'e', 'i', 'o', 'u'].includes(firstLetter)) { 28 | return 'an'; 29 | } else { 30 | return 'a'; 31 | } 32 | } 33 | 34 | /** 35 | * Render a noun in singular or plural form 36 | * 37 | * Both the singular and plural form of the noun must be supplied as arguments. 38 | * 39 | * Rule: 40 | * * `1 -> singular` 41 | * * `else -> plural` (including zero) 42 | * 43 | * @param count - Count 44 | * @param singular - Singular form 45 | * @param plural - Plural form 46 | */ 47 | export function renderNoun(count: number | string, singular: string, plural: string) { 48 | return count === 1 || count === '1' ? singular : plural; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/intl/ListFormat.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | import _ from 'lodash'; 15 | 16 | /** 17 | * Utilities for rendering lists to strings/prompts. 18 | */ 19 | export namespace ListFormatting { 20 | /** 21 | * Format a list with commas and a joiner word. 22 | * 23 | * Example: `formatList(['a', 'b', 'c'], 'and or') -> 'a, b, and or c'` 24 | * 25 | * This can mostly be replaced with Intl.ListFormat(style:'long', ..) once it is implemented for NodeJS. 26 | * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat 27 | * 28 | * @param arr - Array 29 | * @param joiner - Joiner 30 | */ 31 | export function format(arr: string[], joiner = 'or'): string { 32 | if (arr.length === 0) { 33 | return '(empty)'; 34 | } 35 | if (arr.length === 1) { 36 | return arr[0]; 37 | } 38 | if (arr.length === 2) { 39 | return `${arr[0]} ${joiner} ${arr[1]}`; 40 | } else { 41 | return `${_.join(_.take(arr, arr.length - 1), ', ')} ${joiner} ${arr[arr.length - 1]}`; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/logging/DefaultLogger.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | import Debug from 'debug'; 14 | import { ILogger } from '../controls/interfaces/ILogger'; 15 | 16 | const DEFAULT_LOG_LEVEL = 'error:*, warn:*'; 17 | 18 | /** 19 | * Default Logger Implementation. 20 | * 21 | * This wraps the Debug object from npm 'Debug' package to provide "log-levels". 22 | * The log-levels are handled as top-level namespaces. 23 | * 24 | * Examples 25 | * ``` 26 | * export DEBUG="error:*" -> Log 'error' messages from every module 27 | * export DEBUG="error:moduleA" -> Log 'error' messages for moduleA only 28 | * export DEBUG="error:*, warn:*, info:*, debug:*" -> Log everything 29 | * ``` 30 | * 31 | * See https://www.npmjs.com/package/debug for more information on 32 | * configuration. 33 | * 34 | * When instantiated for a given "moduleName", this object provides an `error()` 35 | * function that logs with amended name "error:moduleName". Likewise for `warn()`, 36 | * `info()`, and `debug()`. 37 | */ 38 | export class DefaultLogger implements ILogger { 39 | moduleName: string; 40 | 41 | constructor(moduleName: string) { 42 | this.moduleName = moduleName; 43 | 44 | const namespace = process.env.DEBUG ?? DEFAULT_LOG_LEVEL; 45 | Debug.enable(namespace); 46 | } 47 | 48 | /** 49 | * Log a message as an "error". 50 | * @param message - Message 51 | */ 52 | error(message: string): void { 53 | Debug(`error:${this.moduleName}`)(message); 54 | } 55 | 56 | /** 57 | * Log a message as an "warning". 58 | * @param message - Message 59 | */ 60 | warn(message: string): void { 61 | Debug(`warn:${this.moduleName}`)(message); 62 | } 63 | 64 | /** 65 | * Log a message as an "informational" message. 66 | * @param message - Message 67 | */ 68 | info(message: string): void { 69 | Debug(`info:${this.moduleName}`)(message); 70 | } 71 | 72 | /** 73 | * Log a message as an "low-level debug message". 74 | * @param message - Message 75 | */ 76 | debug(message: string): void { 77 | Debug(`debug:${this.moduleName}`)(message); 78 | } 79 | 80 | /** 81 | * Log an object with pretty print formatting and also 82 | * masking sensitive information if required 83 | * 84 | * @param id - Unique label. 85 | * @param object - Object to be logged 86 | */ 87 | logObject(logLevel: string, id: string, object: any, stringify?: true): void { 88 | const objectToLogJson: string = stringify !== true ? object : JSON.stringify(object, null, 2); 89 | Debug(`${logLevel}:${this.moduleName} ${id}`)(objectToLogJson); 90 | } 91 | 92 | /** 93 | * Function that returns string with actual message if sensitive logging is 94 | * turned off or returns special character string masking the information. 95 | * 96 | * @param message - Logging message 97 | * 98 | * Eg: When sensitive logging is turned on 99 | * "env": \{ 100 | * "ASK_SDK_RESTRICTIVE_LOGGING": "true" 101 | * \} 102 | * 103 | * All messages which are wrapped around `sanitize()` in log.info for example 104 | * are masked using special characters. 105 | */ 106 | sanitize(message: any): string { 107 | if ( 108 | process.env.ASK_SDK_RESTRICTIVE_LOGGING !== undefined && 109 | process.env.ASK_SDK_RESTRICTIVE_LOGGING === 'true' 110 | ) { 111 | return '****'; 112 | } else { 113 | return message; 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/logging/DefaultLoggerFactory.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | import { ILogger } from '../controls/interfaces/ILogger'; 15 | import { ILoggerFactory } from '../controls/interfaces/ILoggerFactory'; 16 | import { DefaultLogger } from './DefaultLogger'; 17 | 18 | /** 19 | * Default Logger Factory 20 | */ 21 | export class DefaultLoggerFactory implements ILoggerFactory { 22 | /** 23 | * Function which returns an instance of Default Logger 24 | * implementation. 25 | * 26 | * @param namespace - Module Namespace 27 | */ 28 | getLogger(namespace: string): ILogger { 29 | return new DefaultLogger(namespace); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/modality/ModalityEvaluation.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | import { HandlerInput } from 'ask-sdk-core'; 15 | 16 | /** 17 | * Possible modalities a user can use to respond to a request. 18 | */ 19 | export enum InputModality { 20 | /** 21 | * User touched the screen. 22 | */ 23 | TOUCH = 'touch', 24 | /** 25 | * User used their voice. 26 | */ 27 | VOICE = 'voice', 28 | } 29 | 30 | /** 31 | * Possible modalities with which the skill may respond to a request. 32 | */ 33 | export enum OutputModality { 34 | /** 35 | * Response is only displayed on the screen. 36 | */ 37 | SCREEN = 'screen', 38 | 39 | /** 40 | * Response is in the form of a voice prompt, 41 | * and may or may not include content displayed on the screen. 42 | */ 43 | VOICE = 'voice', 44 | 45 | /** 46 | * Response type is not yet known. 47 | */ 48 | INDETERMINATE = 'indeterminate', 49 | } 50 | 51 | /** 52 | * Describes the style in which a request should be responded to. 53 | * The most basic characteristic is what modality the response should have, 54 | * but this interface may be extended to describe additional characteristics 55 | * as desired (e.g. whispering). 56 | */ 57 | export interface ResponseStyle { 58 | /** 59 | * The modality with which a request should be responded to. 60 | */ 61 | modality: OutputModality; 62 | } 63 | 64 | /** 65 | * A function that determines the input modality of a request. 66 | */ 67 | export type InputModalityEvaluator = (input: HandlerInput) => InputModality; 68 | 69 | /** 70 | * A function that suggests the style in which a request should be responded to, 71 | * based on the content of the request and the history of how the user responded. 72 | */ 73 | export type ResponseStyleEvaluator = (input: HandlerInput, history: InputModality[]) => ResponseStyle; 74 | 75 | /** 76 | * Default functions for determining input modality and response styles. 77 | */ 78 | export namespace ModalityEvaluationDefaults { 79 | /** 80 | * Default function for determining the input modality of a request. 81 | * This is best-effort and may not work in all cases. 82 | * @param input - Input for the current request 83 | * @returns InputModality - TOUCH if the request came from a TouchWrapper, 84 | * VOICE otherwise. 85 | */ 86 | export function defaultInputModalityEvaluator(input: HandlerInput): InputModality { 87 | const request = input.requestEnvelope.request; 88 | if (request.type === 'Alexa.Presentation.APL.UserEvent') { 89 | if (['TouchWrapper', 'EditText', 'VectorGraphic'].includes(request.source?.type)) { 90 | return InputModality.TOUCH; 91 | } 92 | } 93 | return InputModality.VOICE; 94 | } 95 | 96 | /** 97 | * Default function for suggesting the style in which a request should be responded to. 98 | * @param input - Input for the current request 99 | * @param history - History of how the user responded to previous requests. 100 | * The current request is the last entry. 101 | * @returns ResponseStyle - SCREEN modality if the last InputModality was TOUCH, VOICE otherwise. 102 | */ 103 | export function defaultResponseStyleEvaluator( 104 | input: HandlerInput, 105 | history: InputModality[], 106 | ): ResponseStyle { 107 | if (history?.length > 0 && history[history.length - 1] === InputModality.TOUCH) { 108 | return { modality: OutputModality.SCREEN }; 109 | } 110 | 111 | return { modality: OutputModality.VOICE }; 112 | } 113 | 114 | /** 115 | * Function that always returns an INDETERMINATE modality for a suggested ResponseStyle. 116 | * This is primarily used as the default override function for built-in controls, which results 117 | * in the controls deferring the decision to the function at the ControlManager level. 118 | * @returns OutputModality.INDETERMINATE 119 | */ 120 | export function indeterminateResponseStyleEvaluator(input: HandlerInput, history: InputModality[]) { 121 | return { modality: OutputModality.INDETERMINATE }; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/responseGeneration/AplMode.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | /** 15 | * APL rendering mode 16 | */ 17 | export enum APLMode { 18 | /** 19 | * Controls add APL directives directly to the ResponseBuilder as they are rendering 20 | * their prompts. 21 | */ 22 | DIRECT = 'Direct', 23 | 24 | /** 25 | * The top-level ControlManager.renderAPL produces the APL for each turn. 26 | * - typically this will be a document that includes various control.renderAPLComponent calls. 27 | * 28 | * Controls do not render APL directives during renderAct. 29 | */ 30 | COMPONENT = 'Component', 31 | } 32 | -------------------------------------------------------------------------------- /src/runtime/SessionBehavior.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | /** 15 | * Defines the behavior for the user's session when the Response is delivered. 16 | */ 17 | export enum SessionBehavior { 18 | /** 19 | * Keep the session open. 20 | */ 21 | OPEN, 22 | 23 | /** 24 | * End the session. 25 | */ 26 | END, 27 | 28 | /** 29 | * Keep the session open for approximately 30 seconds but with the 30 | * microphone disabled. 31 | * 32 | * This only applies to devices with a screen. See 33 | * https://developer.amazon.com/en-US/docs/alexa/custom-skills/manage-skill-session-and-session-attributes.html#screen-session 34 | */ 35 | IDLE, 36 | } 37 | -------------------------------------------------------------------------------- /src/systemActs/PayloadTypes.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | import { canfulfill } from 'ask-sdk-model'; 14 | 15 | /** 16 | * Payload for ValueSetAct 17 | */ 18 | export interface ValueSetPayload { 19 | /** 20 | * The control value. 21 | */ 22 | value: T; 23 | 24 | /** 25 | * The rendered value. 26 | */ 27 | renderedValue: string; 28 | } 29 | 30 | /** 31 | * Payload for ValueAddeddAct 32 | */ 33 | export interface ValueAddedPayload { 34 | /** 35 | * The control value. 36 | */ 37 | value: T; 38 | 39 | /** 40 | * The rendered value. 41 | */ 42 | renderedValue: string; 43 | } 44 | 45 | /** 46 | * Payload for ValueRemovedAct 47 | */ 48 | export interface ValueRemovedPayload { 49 | /** 50 | * The control value. 51 | */ 52 | value: T; 53 | 54 | /** 55 | * The rendered value. 56 | */ 57 | renderedValue: string; 58 | } 59 | 60 | /** 61 | * Payload for ValueClearedAct 62 | */ 63 | export interface ValueClearedPayload { 64 | /** 65 | * The control value. 66 | */ 67 | value: T; 68 | 69 | /** 70 | * The rendered value. 71 | */ 72 | renderedValue: string; 73 | } 74 | 75 | /** 76 | * Payload for ValueChangedAct 77 | */ 78 | export interface ValueChangedPayload { 79 | /** 80 | * The control value. 81 | */ 82 | value: T; 83 | 84 | /** 85 | * The rendered value. 86 | */ 87 | renderedValue: string; 88 | 89 | /** 90 | * The previous control value. 91 | */ 92 | previousValue?: T; 93 | 94 | /** 95 | * The previous rendered value. 96 | */ 97 | renderedPreviousValue?: string; 98 | } 99 | 100 | /** 101 | * Payload for SuggestActionAct 102 | */ 103 | export interface SuggestActionPayload { 104 | renderedTarget?: string; 105 | } 106 | 107 | /** 108 | * Payload for InvalidValueAct 109 | */ 110 | export interface InvalidValuePayload { 111 | value: T; 112 | renderedValue: string; 113 | reasonCode?: string; 114 | renderedReason?: string; 115 | } 116 | 117 | /** 118 | * Payload for UnusableInputValueAct 119 | */ 120 | export interface ProblematicInputValuePayload { 121 | value: T; 122 | renderedValue: string; 123 | reasonCode: string; 124 | renderedReason?: string; 125 | } 126 | 127 | /** 128 | * Payload for RequestValueAct 129 | */ 130 | export interface RequestValuePayload { 131 | renderedTarget?: string; 132 | } 133 | 134 | /** 135 | * Payload for RequestChangedValueAct 136 | */ 137 | export interface RequestChangedValuePayload { 138 | currentValue: string; 139 | renderedValue: string; 140 | renderedTarget?: string; 141 | } 142 | 143 | /** 144 | * Payload for RequestValueByListAct 145 | */ 146 | export interface RequestValueByListPayload { 147 | choicesFromActivePage: string[]; 148 | allChoices: string[]; 149 | renderedChoicesFromActivePage: string[]; 150 | renderedAllChoices: string[]; 151 | renderedTarget?: string; 152 | renderedChoices?: string; 153 | } 154 | 155 | /** 156 | * Payload for RequestChangedValueByListAct 157 | */ 158 | export interface RequestChangedValueByListPayload { 159 | currentValue: string; 160 | renderedValue: string; 161 | choicesFromActivePage: string[]; 162 | allChoices: string[]; 163 | renderedChoicesFromActivePage: string[]; 164 | renderedAllChoices: string[]; 165 | renderedTarget?: string; 166 | renderedChoices?: string; 167 | } 168 | 169 | /** 170 | * Payload for RequestRemovedValueByListAct 171 | */ 172 | export interface RequestRemovedValueByListActPayload { 173 | availableChoicesFromActivePage: string[]; 174 | availableChoices: string[]; 175 | renderedChoicesFromActivePage: string[]; 176 | renderedAvailableChoices: string[]; 177 | renderedTarget?: string; 178 | renderedChoices?: string; 179 | } 180 | 181 | /** 182 | * Payload for LiteralInitiativeAct 183 | */ 184 | export interface LiteralContentPayload { 185 | promptFragment: string; 186 | repromptFragment?: string; 187 | } 188 | 189 | /** 190 | * Payload for CanFulfillIntentAct 191 | */ 192 | export interface CanFulfillIntentPayload { 193 | intent: canfulfill.CanFulfillIntent; 194 | } 195 | -------------------------------------------------------------------------------- /src/systemActs/SystemAct.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | import _ from 'lodash'; 15 | import { Control } from '../controls/Control'; 16 | import { ControlInput } from '../controls/ControlInput'; 17 | import { ControlResponseBuilder } from '../responseGeneration/ControlResponseBuilder'; 18 | 19 | /** 20 | * Describes a piece of information to be communicated to the user. 21 | * 22 | * This is the minimal definition required by the Runtime (ControlHandler) 23 | * See `SystemAct` for the actual class used by Control implementations. 24 | */ 25 | export interface ISystemAct { 26 | takesInitiative: boolean; 27 | } 28 | 29 | /** 30 | * Base type for system dialog acts. 31 | * 32 | * Each `SystemAct` represents a precise 'piece of information' that is to be communicated to the user. Each 33 | * `SystemAct` is to be associated with a Control that can be used to render it. 34 | * 35 | * There are two more specific base classes that should be used as the base for user-defined SystemActs: 36 | * * `ContentAct`: the base class for System Acts that just communicate some information. 37 | * * `InitiativeAct`: the base class for System Acts that ask a question or otherwise encourage the user to continue the conversation. 38 | * 39 | * Usage: 40 | * * Add `SystemActs` to the result in `Control.canHandle` and `Control.takeInitiative` to represent what the system wants to communicate. 41 | * * Convert the `SystemActs` into surface forms (prompts, APL, etc) during the render phase. 42 | * * Always extend `ContentAct` or `InitiativeAct` rather than this base type. 43 | * * Introduce new System Acts whenever the available acts are not suitable or precise enough. 44 | */ 45 | export abstract class SystemAct implements ISystemAct { 46 | control: Control; 47 | takesInitiative: boolean; 48 | 49 | /** 50 | * Creates an instance of SystemAct. 51 | * 52 | * Each system act represent a specific 'atom of dialog' the system wishes to communicate to the user. 53 | * 54 | * Usage: 55 | * - New acts should generally extend `InitiativeAct` or `ContentAct` rather than this class, to improve readability. 56 | */ 57 | constructor(control: Control, props: { takesInitiative: boolean }) { 58 | this.control = control; 59 | this.takesInitiative = props.takesInitiative; 60 | } 61 | 62 | /** 63 | * Produces a string representation of the SystemAct. 64 | * 65 | * The associated `Control.id` is included but the complete details of the associate `Control` are omitted for brevity. 66 | */ 67 | toString() { 68 | return `${this.constructor.name}:${JSON.stringify(this.cloneWithControlIdNotDetails())}`; 69 | } 70 | 71 | /** 72 | * For use in toString. 73 | * Creates a clone that replaces the control object with controlId. 74 | */ 75 | private cloneWithControlIdNotDetails(): any { 76 | const cleanAct = _.cloneDeep(this) as any; 77 | cleanAct.controlId = this.control?.id ?? ''; 78 | delete cleanAct.control; 79 | return cleanAct; 80 | } 81 | 82 | /** 83 | * Render the dialog act. 84 | * 85 | * This is the one-size-fits-all direct rendering of a dialog act. This is often appropriate 86 | * for custom acts that are not used by a shared control. 87 | * 88 | * Framework behavior: 89 | * * Shared controls cannot rely on a one-size-fits-all rendering and so they provide 90 | * their own appropriate defaults and props that allow the developer to override the defaults. 91 | * 92 | * @param input - Input 93 | * @param responseBuilder - Response builder 94 | */ 95 | abstract render(input: ControlInput, responseBuilder: ControlResponseBuilder): void; 96 | } 97 | -------------------------------------------------------------------------------- /src/utils/ArrayUtils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | /** 15 | * Randomly selects one item from an array with uniform probability. 16 | */ 17 | export function randomlyPick(input: T[]): T { 18 | return input[Math.floor(Math.random() * input.length)]; 19 | } 20 | 21 | /** 22 | * Moves one item of an array in place and returns the mutated array. 23 | * @param arr - Array 24 | * @param from - Index of item to move 25 | * @param to - Index to move it to 26 | */ 27 | export function moveArrayItem(arr: any[], from: number, to: number): any[] { 28 | arr.splice(to, 0, arr.splice(from, 1)[0]); 29 | return arr; 30 | } 31 | 32 | // TODO: API: remove 33 | /** 34 | * Returns `true` if the value is defined and does not appear in the array. 35 | * @param value - Value 36 | * @param array - Array 37 | */ 38 | export function mismatch(value: any, array: any[]): boolean { 39 | return value !== undefined && !array.includes(value); 40 | } 41 | 42 | // TODO: API: remove 43 | /** 44 | * Returns `true` if the value is undefined or appears in the array. 45 | * @param value - Value 46 | * @param array - Array 47 | */ 48 | export function matchIfDefined(value: any, array: any[]) { 49 | return value === undefined || array.includes(value); 50 | } 51 | -------------------------------------------------------------------------------- /src/utils/AssertionUtils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Assertion, with Typescript 3.7 asserts to convey the information to compiler. 3 | * @param condition - condition 4 | * @param message - message 5 | */ 6 | export function assert(condition: boolean, message?: string): asserts condition { 7 | if (!condition) { 8 | throw new Error(message !== undefined ? `Assertion failed. ${message}` : 'Assertion failed'); 9 | } 10 | return; 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/BasicTypes.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | /** 15 | * Either a string or an array of strings 16 | */ 17 | export type StringOrList = string | string[]; 18 | 19 | // TODO: API: remove or consolidate with ValidationResult / validation function types 20 | /** 21 | * Either the boolean literal `true` or a string. 22 | * 23 | * Purpose: 24 | * - For use in validation functions that return true or an error string. 25 | */ 26 | export type StringOrTrue = true | string; 27 | -------------------------------------------------------------------------------- /src/utils/ControlTreeVisualization.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | import { implementsControlStateDiagramming } from '../controls/mixins/ControlStateDiagramming'; 15 | import { IControl } from '../controls/interfaces/IControl'; 16 | import { isContainerControl } from '../controls/interfaces/IContainerControl'; 17 | 18 | /** 19 | * Creates a text diagram of the Control tree 20 | * 21 | * Notes: 22 | * 1. The controls in the handling chain are highlighted with `= H =` 23 | * 2. The controls in the initiative chain are highlighted with `= I =` 24 | * 3. Controls that are in both chains are highlighted with `= B =` 25 | * @param control - Control 26 | * @param turnNumber - Turn number 27 | * @param indent - Indent 28 | */ 29 | export function generateControlTreeTextDiagram( 30 | control: IControl, 31 | turnNumber: number, 32 | indent?: number, 33 | ): string { 34 | indent = indent ?? 0; 35 | let text = control.id; 36 | let stateStr = control.constructor.name; 37 | if (implementsControlStateDiagramming(control)) { 38 | stateStr = control.stringifyStateForDiagram(); 39 | } 40 | text += ` (${stateStr})`; 41 | 42 | const coloredText = text; 43 | let str = `${coloredText}\n`; 44 | 45 | if (isContainerControl(control)) { 46 | for (const child of control.children) { 47 | const childHandledThisTurn = 48 | child.id === (control as any).state?.lastHandlingControl?.controlId && 49 | (control as any).state?.lastHandlingControl?.turnNumber === turnNumber; 50 | const childTookInitiativeThisTurn = 51 | child.id === (control as any).state?.lastInitiativeChild?.controlId && 52 | (control as any).state?.lastInitiativeChild?.turnNumber === turnNumber; 53 | // const text = `|${(childHandledThisTurn || childTookInitiativeThisTurn) ? "== " : "-- "}`; 54 | 55 | const text = `|${ 56 | childHandledThisTurn && childTookInitiativeThisTurn 57 | ? '= B = ' 58 | : childHandledThisTurn 59 | ? '= H = ' 60 | : childTookInitiativeThisTurn 61 | ? '= I = ' 62 | : '----- ' 63 | }`; 64 | 65 | str += '| '.repeat(indent) + text + generateControlTreeTextDiagram(child, turnNumber, indent + 1); 66 | } 67 | } 68 | 69 | return str; 70 | } 71 | -------------------------------------------------------------------------------- /src/utils/ControlUtils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | import { Control } from '../controls/Control'; 14 | import { ControlInput } from '../controls/ControlInput'; 15 | import { ControlServices } from '../controls/ControlServices'; 16 | 17 | const MODULE_NAME = 'AskSdkControls:ControlUtils'; 18 | /* 19 | * // TODO: tighten up the contract.. what props are supported, precisely. 20 | * probably also good to factor into gatherFuncs() and evaluateFuncs() 21 | 22 | * // TODO: tighten by refactoring NumberControl/DateRangeControl canHandle. 23 | /** 24 | * Evaluate the Input handlers 25 | * 26 | * Handlers can be defined in two places: 27 | * 1. `props.inputHandling.customHandlingFuncs`, and 28 | * 2. `props.apl.customHandlingFuncs` 29 | * @param control - control 30 | * @param input - input 31 | */ 32 | export async function evaluateInputHandlers(control: Control, input: ControlInput): Promise { 33 | const log = ControlServices.getLogger(MODULE_NAME); 34 | const stdHandlers = (control as any).standardInputHandlers ?? []; 35 | const customHandlers = (control as any).props?.inputHandling?.customHandlingFuncs ?? []; 36 | 37 | const aplProps = (control as any).props.apl; 38 | 39 | // TODO: deprecate apl customHandlers prop 40 | if (aplProps !== undefined) { 41 | Object.entries(aplProps).forEach(([_key, value]) => { 42 | if (typeof value === 'object' && 'customHandlingFuncs' in value!) { 43 | customHandlers.push(...(value as any).customHandlingFuncs); 44 | } 45 | }); 46 | } 47 | 48 | const matches = []; 49 | for (const handler of stdHandlers.concat(customHandlers)) { 50 | if ((await handler.canHandle.call(control as any, input)) === true) { 51 | matches.push(handler); 52 | } 53 | } 54 | 55 | if (matches.length > 1) { 56 | throw Error( 57 | `More than one handler matched. Handlers in a single control should be mutually exclusive.` + 58 | ` handlers: ${JSON.stringify(matches.map((x) => x.name))}`, 59 | ); 60 | } 61 | 62 | if (matches.length >= 1) { 63 | (control as any).handleFunc = matches[0].handle.bind(control as any); 64 | return true; 65 | } else { 66 | return false; 67 | } 68 | } 69 | 70 | //Exported for internal use only. Not sufficiently well-defined or valuable for public export. 71 | export function _logIfBothTrue(customCanHandle: boolean, builtInCanHandle: boolean) { 72 | const log = ControlServices.getLogger(MODULE_NAME); 73 | if (customCanHandle === true && builtInCanHandle === true) { 74 | log.warn( 75 | 'Custom canHandle function and built-in canHandle function both returned true. Turn on debug logging for more information', 76 | ); 77 | } 78 | } 79 | 80 | /** 81 | * Selects the first control with specific ID from an array. 82 | * 83 | * Behavior: 84 | * - implemented by linear search. 85 | * - if more than one control matches, the first is returned. 86 | * - if parameter `id` is undefined, returns `undefined`. 87 | * - if there is no control with matching id, returns `undefined`. 88 | * 89 | * @param controls - Controls 90 | * @param childId - The id to match 91 | * @returns - The matching childControl, or undefined if not present. 92 | */ 93 | export function findControlById(controls: Control[], id: string | undefined): Control | undefined { 94 | if (id === undefined) { 95 | return undefined; 96 | } 97 | return controls.find((c) => c.id === id); 98 | } 99 | -------------------------------------------------------------------------------- /src/utils/ControlVisitor.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | import { IControl } from '../controls/interfaces/IControl'; 15 | import { isContainerControl } from '../controls/interfaces/IContainerControl'; 16 | 17 | /** 18 | * Visits all controls in a control tree and calls `fn(control)` for each. 19 | * @param control - Control 20 | * @param fn - Function to run on each control 21 | */ 22 | export function visitControls(control: IControl, fn: (control: IControl) => void) { 23 | fn.call(undefined, control); 24 | 25 | if (isContainerControl(control)) { 26 | for (const child of control.children) { 27 | visitControls(child, fn); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/utils/DeepRequired.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | /** 15 | * A type that changes all optional properties to be required, recursively. 16 | * 17 | * Function types are not rewritten as that would change their formal-argument 18 | * types. 19 | */ 20 | 21 | export type DeepRequired = 22 | // eslint-disable-next-line @typescript-eslint/ban-types 23 | T extends Function 24 | ? T 25 | : { 26 | [k in keyof T]-?: DeepRequired; 27 | }; 28 | -------------------------------------------------------------------------------- /src/utils/ErrorUtils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | // TODO: API: review if these are still required. 15 | 16 | /** 17 | * Throws an Error if the guard is true. 18 | */ 19 | export function throwIf(guard: boolean, message: string): guard is true { 20 | if (guard) { 21 | throw new Error(message); 22 | } 23 | return true; 24 | } 25 | 26 | /** 27 | * Throws an Error if the object is undefined. 28 | */ 29 | export function throwIfUndefined(object: T | undefined, message: string): object is T { 30 | if (object === undefined) { 31 | throw new Error(message); 32 | } 33 | return true; 34 | } 35 | -------------------------------------------------------------------------------- /src/utils/Predicates.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | /** 15 | * Represents that the state of a control is not consistent and cannot be used. 16 | */ 17 | export class StateConsistencyError extends Error { 18 | constructor(message?: string) { 19 | super(message); 20 | // see: typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html 21 | Object.setPrototypeOf(this, new.target.prototype); // restore prototype chain 22 | this.name = StateConsistencyError.name; // stack traces display correctly now 23 | } 24 | } 25 | 26 | /** 27 | * Represents that a predicate-guard expression failed. 28 | * 29 | * Purpose: 30 | * * This helps to write predicate functions as a linear chain of individual tests. 31 | */ 32 | export class GuardFailed extends Error { 33 | constructor(message?: string) { 34 | super(message); 35 | // see: typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html 36 | Object.setPrototypeOf(this, new.target.prototype); // restore prototype chain 37 | this.name = GuardFailed.name; // stack traces display correctly now 38 | } 39 | } 40 | 41 | /** 42 | * Does nothing if the predicate is true. Otherwise throws GuardFailed error. 43 | * 44 | * Purpose: 45 | * * This helps to write predicate functions as a linear chain of individual tests. 46 | */ 47 | export function okIf(predicate: boolean): asserts predicate { 48 | if (!predicate) { 49 | throw new GuardFailed('okIf failed. predicate===false.'); 50 | } 51 | } 52 | 53 | /** 54 | * Throws a GuardFailed error if predicate is true. 55 | * 56 | * Purpose: 57 | * * This helps to write predicate functions as a linear chain of individual tests. 58 | * @param predicate - Predicate 59 | */ 60 | export function failIf(predicate: boolean) { 61 | if (predicate) { 62 | throw new GuardFailed('failIf triggered. predicate===true.'); 63 | } 64 | } 65 | 66 | /** 67 | * Consumes an GuardFailed error and returns false, but otherwise rethrows. 68 | * 69 | * Purpose: 70 | * * This helps to write predicate functions as a linear chain of individual tests. 71 | * 72 | * @returns `false` if error is a `GuardFailed` error. 73 | * @throws Rethrows the error if it is not a `GuardFailed` error. 74 | */ 75 | export function falseIfGuardFailed(err: Error): false { 76 | if (err instanceof GuardFailed) { 77 | return false; 78 | } 79 | throw err; // otherwise rethrow 80 | } 81 | 82 | // different style than falseIfGuardFailed 83 | // better comprehension, but results in 2-lines of boilerplate rather than one. 84 | export function verifyErrorIsGuardFailure(err: Error): void { 85 | if (!(err instanceof GuardFailed)) { 86 | throw err; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/utils/RequestUtils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | import { Request } from 'ask-sdk-model'; 15 | import { SimplifiedIntent } from './IntentUtils'; 16 | 17 | /** 18 | * Utility to stringify a Request object. 19 | * @param request - Request 20 | */ 21 | export function requestToString(request: Request) { 22 | switch (request.type) { 23 | case 'IntentRequest': 24 | return SimplifiedIntent.fromIntent(request.intent).toString(); 25 | case 'Alexa.Presentation.APL.UserEvent': 26 | return `APL.UserEvent: ${JSON.stringify(request.arguments)}`; 27 | default: 28 | return request.type; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/utils/ResponseUtils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | import { OutputModality, ResponseStyle, ResponseStyleEvaluator } from '../modality/ModalityEvaluation'; 15 | import { ControlResponseBuilder } from '../responseGeneration/ControlResponseBuilder'; 16 | import { Response } from 'ask-sdk-model'; 17 | import { ControlInput } from '../controls/ControlInput'; 18 | import { ControlResultBuilder } from '..'; 19 | 20 | /** 21 | * Properties for building a response based on ResponseStyle. 22 | */ 23 | export interface ResponseStyleBuilderProps { 24 | voicePrompt: string; 25 | voiceReprompt?: string; 26 | responseStyle: ResponseStyle; 27 | builder: ControlResponseBuilder; 28 | } 29 | 30 | /** 31 | * Adds voice prompt and reprompt to a ControlResponseBuilder conditionally based on 32 | * the ResponseStyle contained in the ResponseStyleBuilderProps. 33 | * 34 | * If the modality of the response style is SCREEN, the voice prompt and reprompt 35 | * will not be added. In any other case, they will be added. 36 | * 37 | * @param props - ResponseStyleBuilderProps - properties needed for adding prompts to 38 | * the response. 39 | */ 40 | export function addFragmentsForResponseStyle(props: ResponseStyleBuilderProps): void { 41 | const voicePrompt = props.voicePrompt; 42 | const voiceReprompt = props.voiceReprompt; 43 | const responseStyle = props.responseStyle; 44 | 45 | const builder = props.builder; 46 | if (responseStyle.modality === OutputModality.SCREEN) { 47 | return; 48 | } 49 | 50 | builder.addPromptFragment(voicePrompt); 51 | 52 | if (voiceReprompt !== undefined) { 53 | builder.addRepromptFragment(voiceReprompt); 54 | } 55 | } 56 | 57 | /** 58 | * Adds voice prompt and reprompt to a ControlResponseBuilder conditionally based on 59 | * the ResponseStyle contained in the ResponseStyleBuilderProps. 60 | * 61 | * If the modality of the response style is SCREEN, the voice prompt and reprompt 62 | * will not be added. In any other case, they will be added. 63 | * 64 | * After adding the prompts, the response is built. 65 | * 66 | * @param props - ResponseStyleBuilderProps - properties needed for adding prompts to 67 | * the response. 68 | * @returns Response - The response for the current skill turn. 69 | */ 70 | export function buildResponseForStyle(props: ResponseStyleBuilderProps): Response { 71 | addFragmentsForResponseStyle(props); 72 | 73 | return props.builder.build(); 74 | } 75 | 76 | /** 77 | * Executes a ResponseStyleEvaluator override function and determines whether to use 78 | * the result from it or the base, non-overridden result. 79 | * @param evaluator - ResponseStyleEvaluator - Overridden ResponseStyleEvaluator 80 | * @param input - ControlInput - Input for the current skill turn 81 | * @returns ResponseStyle - Uses the result from the provided evaluator if it was 82 | * determinate, otherwise uses the ResponseStyle present in the provided ControlInput. 83 | */ 84 | export function getDeterminateResponseStyle( 85 | evaluator: ResponseStyleEvaluator, 86 | input: ControlInput, 87 | ): ResponseStyle { 88 | const overriddenResponseStyle = evaluator(input.handlerInput, input.inputModalityHistory); 89 | 90 | if (overriddenResponseStyle.modality !== OutputModality.INDETERMINATE) { 91 | return overriddenResponseStyle; 92 | } 93 | 94 | return input.suggestedResponseStyle; 95 | } 96 | 97 | /** 98 | * Overrides the default session behavior depending on the requested response style. 99 | * If the response style includes a screen output modality, the session will enter an idle state. 100 | * In all other cases, the default behavior is preserved. 101 | * @param resultBuilder - ControlResultBuilder - The ControlResultBuilder for the current skill turn 102 | * @param responseStyle - ResponseStyle - The ResponseStyle for the current skill turn 103 | */ 104 | export function setSessionBehaviorForStyle( 105 | resultBuilder: ControlResultBuilder, 106 | responseStyle: ResponseStyle, 107 | ): void { 108 | if (responseStyle.modality === OutputModality.SCREEN) { 109 | resultBuilder.enterIdleState(); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/utils/SerializationValidator.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | import _ from 'lodash'; 15 | import { ControlServices } from '../controls/ControlServices'; 16 | import { IControl } from '../controls/interfaces/IControl'; 17 | import { IControlInput } from '../controls/interfaces/IControlInput'; 18 | import { IControlManager } from '../controls/interfaces/IControlManager'; 19 | import { _extractStateFromControlTree } from '../runtime/ControlHandler'; 20 | 21 | const MODULE_NAME = 'AskSdkControls:SerializationValidator'; 22 | 23 | /** 24 | * Validates that the serialized state will survive the round-trip successfully. 25 | * 26 | * If round-trip fails, diagnostic information is printed to the console. 27 | * @param serializedState - Serialized state (a string in JSON format) 28 | * @param controlManager - Control manager 29 | * @param input - Input 30 | * @throws Error if round-trip fails. 31 | */ 32 | export function validateSerializedState( 33 | serializedState: string, 34 | controlManager: IControlManager, 35 | input: IControlInput, 36 | ): void { 37 | const log = ControlServices.getLogger(MODULE_NAME); 38 | 39 | // perform deserialization 40 | const deserializedState = JSON.parse(serializedState); 41 | const rebuiltTopControl: IControl = controlManager.createControlTree(); 42 | controlManager.reestablishControlStates(rebuiltTopControl, deserializedState); 43 | 44 | // and then re-serialize to complete the round trip 45 | const roundTrippedState = _extractStateFromControlTree(rebuiltTopControl); 46 | const roundTrippedUISerialized = JSON.stringify(roundTrippedState, null, 2); 47 | 48 | if (!_.isEqual(deserializedState, roundTrippedState)) { 49 | log.info( 50 | 'serializedState did not survive the simulated round trip (deserialization into controlTree and re-serialization).', 51 | ); 52 | const lines1 = serializedState.split('\n'); 53 | const lines2 = roundTrippedUISerialized.split('\n'); 54 | if (lines1.length === lines2.length) { 55 | log.info('================================================='); 56 | for (const [i, line1] of lines1.entries()) { 57 | const line2 = lines2[i]; 58 | if (line1 !== line2) { 59 | log.info(`${i}: Expected: ${log.sanitize(line1)}`); 60 | log.info(`${i}: Actual: ${log.sanitize(line2)}`); 61 | } 62 | } 63 | log.info('================================================='); 64 | } else { 65 | log.info('Diff is complicated.. use a text differ on the following'); 66 | log.info('================================================='); 67 | log.info(log.sanitize(serializedState)); 68 | log.info('================================================='); 69 | log.info('================================================='); 70 | log.info(log.sanitize(roundTrippedUISerialized)); 71 | log.info('================================================='); 72 | 73 | log.info('First diff:'); 74 | for (const [i, line1] of lines1.entries()) { 75 | const line2 = lines2[i]; 76 | if (line1 !== line2) { 77 | log.info(`${i}: Expected: ${log.sanitize(line1)}`); 78 | log.info(`${i}: Actual: ${log.sanitize(line2)}`); 79 | break; 80 | } 81 | } 82 | } 83 | 84 | throw new Error( 85 | 'Problem round-tripping JSON serialization (see logs above to obtain diff). Do your custom controls have fromJson, new up correct type and pass all properties?', 86 | ); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/utils/testSupport/SkillInvoker.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the 'License'). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the 'license' file accompanying this file. This file is distributed 9 | * on an 'AS IS' BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | import { RequestHandler, Skill } from 'ask-sdk-core'; 15 | import { Directive, Response, ResponseEnvelope, ui } from 'ask-sdk-model'; 16 | import { IControlInput } from '../../controls/interfaces/IControlInput'; 17 | import { wrapRequestHandlerAsSkill } from './SkillWrapper'; 18 | 19 | type TSessionAttributes = { [key: string]: any } | undefined; 20 | 21 | /** 22 | * Utility to invoke a Skill or RequestHandler for testing. 23 | * 24 | * This wrapper mimics the lifecycle of an ASK Skill using AWS Lambda 25 | * that is repeatedly invoked as a 'stateless' function which receives actual state 26 | * in the session attributes of the Request object. 27 | * 28 | */ 29 | export class SkillInvoker { 30 | private sessionAttributes: TSessionAttributes; 31 | private skill: Skill; 32 | 33 | constructor(skillOrRequestHandler: Skill | RequestHandler) { 34 | this.skill = 35 | (skillOrRequestHandler as any).handle !== undefined 36 | ? wrapRequestHandlerAsSkill(skillOrRequestHandler as RequestHandler) 37 | : (skillOrRequestHandler as Skill); 38 | } 39 | 40 | /** 41 | * Invoke the skill with a control-input object. 42 | * The control input is first converted to a RequestEnvelope(IntentRequest) 43 | * 44 | * @param input - Input 45 | * @returns [prompt ssml inner-text, reprompt ssml inner-text] 46 | */ 47 | public async invoke(input: IControlInput): Promise { 48 | const envelope = input.handlerInput.requestEnvelope; 49 | envelope.session!.attributes = this.sessionAttributes; // populate saved state 50 | 51 | // ********** INVOKE SKILL **************** 52 | const responseEnvelope = await this.skill.invoke(envelope, undefined); 53 | 54 | this.sessionAttributes = responseEnvelope.sessionAttributes; // save updated state 55 | 56 | const promptSSML = (responseEnvelope.response?.outputSpeech as ui.SsmlOutputSpeech)?.ssml; 57 | const prompt = promptSSML?.replace('', '').replace('', ''); 58 | 59 | const repromptSSML = ( 60 | (responseEnvelope.response?.reprompt as ui.Reprompt)?.outputSpeech as ui.SsmlOutputSpeech 61 | )?.ssml; 62 | const reprompt = repromptSSML?.replace('', '').replace('', ''); 63 | 64 | const directive: Directive[] | undefined = responseEnvelope.response?.directives; 65 | // const cardContent = (responseEnv.response.card as ui.SimpleCard).content; 66 | return { 67 | responseEnvelope, 68 | response: responseEnvelope.response, 69 | prompt, 70 | reprompt, 71 | directive, 72 | }; 73 | } 74 | } 75 | 76 | export interface TestResponseObject { 77 | responseEnvelope: ResponseEnvelope; 78 | response?: Response; 79 | prompt: string; 80 | reprompt?: string; 81 | directive?: Directive[]; 82 | } 83 | -------------------------------------------------------------------------------- /src/utils/testSupport/SkillWrapper.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | import { ErrorHandler, HandlerInput, RequestHandler, Skill, SkillBuilders } from 'ask-sdk-core'; 15 | import { ControlServices } from '../../controls/ControlServices'; 16 | 17 | const MODULE_NAME = 'AskSdkControls:SkillWrapper'; 18 | 19 | /** 20 | * Wraps a single RequestHandler as a Skill for testing. 21 | * 22 | * The resulting Skill has a single requestHandler (the one provided) and a 23 | * default error handler that logs any internal error that may occur. 24 | * 25 | * @param requestHandler - Request handler 26 | */ 27 | export function wrapRequestHandlerAsSkill(requestHandler: RequestHandler): Skill { 28 | const log = ControlServices.getLogger(MODULE_NAME); 29 | const errorHandler: ErrorHandler = { 30 | canHandle() { 31 | return true; 32 | }, 33 | handle(handlerInput: HandlerInput, error: Error) { 34 | log.error(`~~~~ Error handled: ${error.stack}`); 35 | const speakOutput = `${error.message}`; 36 | 37 | return handlerInput.responseBuilder.speak(speakOutput).reprompt(speakOutput).getResponse(); 38 | }, 39 | }; 40 | 41 | /* 42 | * The SkillBuilder acts as the entry point for your skill, routing all request and response 43 | * payloads to the handlers above. Make sure any new handlers or interceptors you've 44 | * defined are included below. The order matters - they're processed top to bottom. 45 | */ 46 | 47 | const _handler = SkillBuilders.custom().addRequestHandlers(requestHandler).addErrorHandlers(errorHandler); 48 | const skill = _handler.create(); 49 | return skill; 50 | } 51 | -------------------------------------------------------------------------------- /test/CustomSerdeTests.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | import { expect } from 'chai'; 15 | import { suite, test } from 'mocha'; 16 | import { Control, SystemAct, TestInput, testTurn, waitForDebugger } from '../src'; 17 | import { ControlProps, ControlState } from '../src/controls/Control'; 18 | import { ControlInput } from '../src/controls/ControlInput'; 19 | import { ControlManager } from '../src/controls/ControlManager'; 20 | import { ControlResultBuilder } from '../src/controls/ControlResult'; 21 | import { GeneralControlIntent } from '../src/intents/GeneralControlIntent'; 22 | import { ControlResponseBuilder } from '../src/responseGeneration/ControlResponseBuilder'; 23 | import { ControlHandler } from '../src/runtime/ControlHandler'; 24 | import { SkillInvoker } from '../src/utils/testSupport/SkillInvoker'; 25 | 26 | waitForDebugger(); 27 | 28 | /* 29 | * Tests a control that has state (Set) that doesn't automatically serialize with JSON.stringify. 30 | * 31 | * The test shows the control changing state and being serialized and deserialized. 32 | * To accomplish this, the state object implements toJSON() and assign() 33 | * - the name and signature for toJSON() is defined by JSON.stringify and called automatically by JSON.stringify 34 | * - the name and signature for fromJSON() is defined by the Control framework and used as an alternative to Object.assign() when present. 35 | 36 | */ 37 | suite('== Custom Serde ==', () => { 38 | test('explicit confirmation and disaffirm', async () => { 39 | // Note: this test demonstrates using testTurn to run a multi-turn scenario with assertions between turns. 40 | const requestHandler = new ControlHandler(new CustomSerdeControlManager()); 41 | const invoker = new SkillInvoker(requestHandler); 42 | await testTurn(invoker, 'U: ', TestInput.of(GeneralControlIntent.of({})), 'A:'); 43 | 44 | expect((requestHandler.getSerializableControlStates().customControl as string[])[0]).exist.and.equals( 45 | 'x', 46 | ); 47 | 48 | await testTurn(invoker, 'U: ', TestInput.of(GeneralControlIntent.of({})), 'A:'); 49 | 50 | expect((requestHandler.getSerializableControlStates().customControl as string[])[0]).exist.and.equals( 51 | 'y', 52 | ); 53 | }); 54 | }); 55 | 56 | class CustomSerdeControlManager extends ControlManager { 57 | createControlTree(): Control { 58 | return new CustomSerdeControl({ 59 | id: 'customControl', 60 | }); 61 | } 62 | } 63 | 64 | class CustomSerdeControlState implements ControlState { 65 | value: Set; 66 | 67 | lastInitiative: { 68 | actname?: string; 69 | }; 70 | constructor(value: Set) { 71 | this.value = value; 72 | this.lastInitiative = {}; 73 | } 74 | } 75 | 76 | class CustomSerdeControl extends Control { 77 | state: CustomSerdeControlState; 78 | 79 | constructor(props: ControlProps, state?: CustomSerdeControlState) { 80 | super(props.id); 81 | this.state = state ?? new CustomSerdeControlState(new Set()); 82 | } 83 | 84 | canHandle(input: ControlInput): boolean { 85 | return true; 86 | } 87 | 88 | async handle(input: ControlInput, resultBuilder: ControlResultBuilder): Promise { 89 | if (this.state.value.size === 0) { 90 | this.state.value.add('x'); 91 | } else if (this.state.value.size === 1 && this.state.value.has('x')) { 92 | this.state.value.delete('x'); 93 | this.state.value.add('y'); 94 | } 95 | 96 | return; 97 | } 98 | 99 | canTakeInitiative(input: ControlInput): boolean { 100 | return false; 101 | } 102 | async takeInitiative(input: ControlInput, resultBuilder: ControlResultBuilder): Promise { 103 | throw new Error('Method not implemented.'); 104 | } 105 | 106 | getSerializableState(): string[] { 107 | // render the set as a simple list. 108 | return [...this.state.value.keys()]; 109 | } 110 | 111 | setSerializableState(obj: string[]) { 112 | if (obj !== undefined) { 113 | // refreshes the set from the serialized array 114 | this.state.value = new Set(); 115 | for (const x of obj) { 116 | this.state.value.add(x); 117 | } 118 | } 119 | } 120 | 121 | async renderAct(act: SystemAct, input: ControlInput, builder: ControlResponseBuilder): Promise { 122 | throw new Error('Method not implemented.'); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /test/game_strings.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | export namespace GameStrings { 15 | export enum SlotType { 16 | DomainValue = 'DomainValue', 17 | } 18 | 19 | export enum Target { 20 | Name = 'name', 21 | AvatarName = 'avatarName', 22 | PetName = 'petName', 23 | Age = 'age', 24 | CharClass = 'class', 25 | Species = 'species', 26 | } 27 | 28 | export enum ID { 29 | GameContainer = 'gameContainer', 30 | 31 | PlayerContainer = 'playerContainer', 32 | PlayerName = 'playerName', 33 | PlayerAge = 'playerAge', 34 | PlayerClass = 'playerClass', 35 | 36 | PetContainer = 'petContainer', 37 | PetName = 'petName', 38 | PetSpecies = 'petSpecies', 39 | } 40 | 41 | export enum Value { 42 | Elf = 'elf', 43 | Dwarf = 'dwarf', 44 | Human = 'human', 45 | Cat = 'cat', 46 | Dog = 'dog', 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/mock/inputInteractionModel.json: -------------------------------------------------------------------------------- 1 | { 2 | "interactionModel": { 3 | "languageModel": { 4 | "modelConfiguration": { 5 | "fallbackIntentSensitivity": { "level": "HIGH" } 6 | }, 7 | "invocationName": "mock skill", 8 | "intents": [ 9 | { 10 | "name": "AMAZON.CancelIntent", 11 | "samples": [] 12 | }, 13 | { 14 | "name": "HelloWorldIntent", 15 | "slots": [], 16 | "samples": ["sample one", "sample two"] 17 | } 18 | ], 19 | "types": [] 20 | }, 21 | "prompts": [] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/modality.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | import { suite, test } from 'mocha'; 15 | import { expect } from 'chai'; 16 | import { 17 | InputModality, 18 | ModalityEvaluationDefaults, 19 | OutputModality, 20 | ResponseStyle, 21 | } from '../src/modality/ModalityEvaluation'; 22 | import { HandlerInput } from 'ask-sdk-core'; 23 | import { TestInput } from '../src/utils/testSupport/TestingUtils'; 24 | import { ControlHandler } from '../src/runtime/ControlHandler'; 25 | import { SkillInvoker } from '../src/utils/testSupport/SkillInvoker'; 26 | import { ControlManager, ControlManagerProps } from '../src/controls/ControlManager'; 27 | import { ControlInput } from '../src/controls/ControlInput'; 28 | import { ContainerControl } from '../src/controls/ContainerControl'; 29 | import { ControlResultBuilder } from '../src/controls/ControlResult'; 30 | import UserEvent = interfaces.alexa.presentation.apl.UserEvent; 31 | import { interfaces } from 'ask-sdk-model'; 32 | 33 | suite('== Modality evaluation and tracking scenarios ==', () => { 34 | const touchInput = TestInput.simpleUserEvent([]); 35 | const voiceInput = TestInput.of('VoiceIntent'); 36 | const editTextInput = TestInput.userEvent(userEventWithSource('EditText')); 37 | const vectorGraphicInput = TestInput.userEvent(userEventWithSource('EditText')); 38 | let manager: TestControlManager; 39 | let handler: ControlHandler; 40 | let rootControl: TestControl; 41 | let lastHistory: InputModality[] | undefined; 42 | let invoker: SkillInvoker; 43 | 44 | beforeEach(() => { 45 | manager = new TestControlManager({ 46 | responseStyleEvaluator, 47 | }); 48 | handler = new ControlHandler(manager); 49 | rootControl = manager.createControlTree(); 50 | rootControl.lastKnownModality = undefined; 51 | lastHistory = undefined; 52 | invoker = new SkillInvoker(handler); 53 | }); 54 | 55 | test('Input and output modality are properly evaluated', async () => { 56 | await invoker.invoke(touchInput); 57 | expect(await rootControl.lastKnownModality).to.equal(OutputModality.SCREEN); 58 | 59 | await invoker.invoke(editTextInput); 60 | expect(await rootControl.lastKnownModality).to.equal(OutputModality.SCREEN); 61 | 62 | await invoker.invoke(vectorGraphicInput); 63 | expect(await rootControl.lastKnownModality).to.equal(OutputModality.SCREEN); 64 | 65 | await invoker.invoke(voiceInput); 66 | expect(await rootControl.lastKnownModality).to.equal(OutputModality.VOICE); 67 | }); 68 | 69 | test('Input modality history is properly tracked', async () => { 70 | await invoker.invoke(touchInput); 71 | await invoker.invoke(voiceInput); 72 | expect(lastHistory).to.deep.equal(['touch', 'voice']); 73 | }); 74 | 75 | function responseStyleEvaluator(input: HandlerInput, history: InputModality[]): ResponseStyle { 76 | lastHistory = JSON.parse(JSON.stringify(history)); 77 | return ModalityEvaluationDefaults.defaultResponseStyleEvaluator(input, history); 78 | } 79 | 80 | function userEventWithSource(source: string): UserEvent { 81 | return { 82 | type: 'Alexa.Presentation.APL.UserEvent', 83 | requestId: 'amzn1.echo-api.request.1234567890', 84 | timestamp: '2019-10-04T18:48:22Z', 85 | locale: 'en-US', 86 | components: {}, 87 | source: { 88 | type: source, 89 | handler: 'Press', 90 | id: 'TestComponent', 91 | }, 92 | token: 'testToken', 93 | }; 94 | } 95 | }); 96 | 97 | class TestControlManager extends ControlManager { 98 | constructor(props?: ControlManagerProps) { 99 | super(props); 100 | } 101 | 102 | createControlTree(state?: any, input?: ControlInput): TestControl { 103 | return TestControl.getInstance(); 104 | } 105 | } 106 | 107 | class TestControl extends ContainerControl { 108 | private static instance = new TestControl(); 109 | 110 | lastKnownModality: OutputModality | undefined = undefined; 111 | 112 | private constructor() { 113 | super({ id: 'TestControl' }); 114 | } 115 | 116 | async canHandle(input: ControlInput): Promise { 117 | this.lastKnownModality = input.suggestedResponseStyle.modality; 118 | return true; 119 | } 120 | 121 | async handle(input: ControlInput, resultBuilder: ControlResultBuilder): Promise { 122 | // Do nothing 123 | } 124 | 125 | static getInstance() { 126 | return this.instance; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /test/systemActs/ContentActs.spec.ts: -------------------------------------------------------------------------------- 1 | import { suite, test } from 'mocha'; 2 | import { ContainerControl } from '../../src/controls/ContainerControl'; 3 | import { Control } from '../../src/controls/Control'; 4 | import { ControlManager } from '../../src/controls/ControlManager'; 5 | import { ControlHandler } from '../../src/runtime/ControlHandler'; 6 | import { SkillInvoker, TestResponseObject } from '../../src/utils/testSupport/SkillInvoker'; 7 | import { TestInput, testTurn } from '../../src/utils/testSupport/TestingUtils'; 8 | import { ControlInput } from '../../src/controls/ControlInput'; 9 | import { ControlResultBuilder } from '../../src/controls/ControlResult'; 10 | import { CanFulfillIntentAct } from '../../src/systemActs/ContentActs'; 11 | import { canfulfill } from 'ask-sdk-model'; 12 | 13 | suite('Test CanFulfillIntentRequest', () => { 14 | const canFulfillIntent: canfulfill.CanFulfillIntent = { 15 | canFulfill: 'YES', 16 | slots: { 17 | Artist: { 18 | canUnderstand: 'YES', 19 | canFulfill: 'YES', 20 | }, 21 | Song: { 22 | canUnderstand: 'YES', 23 | canFulfill: 'YES', 24 | }, 25 | DedicatedPerson: { 26 | canUnderstand: 'YES', 27 | canFulfill: 'YES', 28 | }, 29 | }, 30 | }; 31 | class TestControlManager extends ControlManager { 32 | createControlTree(): Control { 33 | return new CustomRootControl({ id: 'root' }); 34 | } 35 | } 36 | 37 | class CustomRootControl extends ContainerControl { 38 | async canHandle(input: ControlInput): Promise { 39 | return input.request.type === 'CanFulfillIntentRequest'; 40 | } 41 | 42 | async handle(input: ControlInput, resultBuilder: ControlResultBuilder): Promise { 43 | if (input.request.type === 'CanFulfillIntentRequest') { 44 | resultBuilder.addAct( 45 | new CanFulfillIntentAct(this, { 46 | intent: canFulfillIntent, 47 | }), 48 | ); 49 | } 50 | return; 51 | } 52 | } 53 | 54 | test('CanFulfillIntentRequest', async () => { 55 | const requestHandler = new ControlHandler(new TestControlManager()); 56 | const invoker = new SkillInvoker(requestHandler); 57 | const intentName = 'SendSongRequest'; 58 | const expectedResponse: TestResponseObject = { 59 | responseEnvelope: { 60 | version: '', 61 | response: { 62 | canFulfillIntent, 63 | }, 64 | }, 65 | prompt: '', 66 | }; 67 | const slots = { 68 | Artist: { 69 | name: 'Artist', 70 | confirmationStatus: 'NONE', 71 | }, 72 | Song: { 73 | name: 'Song', 74 | confirmationStatus: 'NONE', 75 | }, 76 | DedicatedPerson: { 77 | name: 'DedicatedPerson', 78 | confirmationStatus: 'NONE', 79 | }, 80 | }; 81 | await testTurn( 82 | invoker, 83 | 'U: __', 84 | TestInput.canFulfillIntentRequest(intentName, slots), 85 | expectedResponse, 86 | ); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /test/utilsTest/IntentUtils.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | import { expect } from 'chai'; 15 | import { suite, test } from 'mocha'; 16 | import { defaultIntentToValueMapper, IntentBuilder } from '../../src/utils/IntentUtils'; 17 | 18 | suite('== IntentUtils.IntentNameToValueMapper ==', () => { 19 | test("WeatherIntent -> 'weather'", async () => { 20 | expect('weather').equal(defaultIntentToValueMapper(IntentBuilder.of('weatherIntent'))); 21 | }); 22 | 23 | test("GetWeatherIntent -> 'getWeather'", async () => { 24 | expect('getWeather').equal(defaultIntentToValueMapper(IntentBuilder.of('GetWeatherIntent'))); 25 | }); 26 | 27 | test("AMAZON.YesIntent -> 'yes'", async () => { 28 | expect('yes').equal(defaultIntentToValueMapper(IntentBuilder.of('AMAZON.YesIntent'))); 29 | }); 30 | 31 | test("AMAZON.ShuffleOffIntent -> 'shuffleOff'", async () => { 32 | expect('shuffleOff').equal(defaultIntentToValueMapper(IntentBuilder.of('AMAZON.ShuffleOffIntent'))); 33 | }); 34 | 35 | test("Namespace1.namespace2.ThingIntent -> 'thing'", async () => { 36 | expect('thing').equal( 37 | defaultIntentToValueMapper(IntentBuilder.of('Namespace1.namespace2.ThingIntent')), 38 | ); 39 | }); 40 | 41 | test("SomethingRandom -> 'somethingRandom'", async () => { 42 | expect('somethingRandom').equal(defaultIntentToValueMapper(IntentBuilder.of('SomethingRandom'))); 43 | }); 44 | 45 | test('weatherIntent -> weather', async () => { 46 | expect('weather').equal(defaultIntentToValueMapper(IntentBuilder.of('weatherIntent'))); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /test/utilsTest/ResponseUtils.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | import { ResponseFactory } from 'ask-sdk-core'; 15 | import { Response, ui } from 'ask-sdk-model'; 16 | import { expect } from 'chai'; 17 | import { suite, test } from 'mocha'; 18 | import { ControlResultBuilder } from '../../src'; 19 | import { OutputModality } from '../../src/modality/ModalityEvaluation'; 20 | import { ControlResponseBuilder } from '../../src/responseGeneration/ControlResponseBuilder'; 21 | import { SessionBehavior } from '../../src/runtime/SessionBehavior'; 22 | import { buildResponseForStyle, setSessionBehaviorForStyle } from '../../src/utils/ResponseUtils'; 23 | 24 | suite('== ResponseUtils ==', () => { 25 | let builder: ControlResponseBuilder; 26 | 27 | beforeEach(() => { 28 | builder = new ControlResponseBuilder(ResponseFactory.init()); 29 | }); 30 | 31 | suite('== ResponseUtils.buildResponseForStyle ==', () => { 32 | const voicePrompt = 'voicePrompt'; 33 | const voiceReprompt = 'voiceReprompt'; 34 | 35 | test('Voice prompts and reprompts are ignored for screen modality', async () => { 36 | const { voicePromptResponse, voiceRepromptResponse } = getSsmlFromResponse( 37 | buildResponseForStyle({ 38 | voicePrompt, 39 | voiceReprompt, 40 | responseStyle: { modality: OutputModality.SCREEN }, 41 | builder, 42 | }), 43 | ); 44 | 45 | expect(voicePromptResponse).equal(getSsmlString()); 46 | expect(voiceRepromptResponse).equal(getSsmlString()); 47 | }); 48 | 49 | test('Voice prompts are included for voice modality', async () => { 50 | const { voicePromptResponse, voiceRepromptResponse } = getSsmlFromResponse( 51 | buildResponseForStyle({ 52 | voicePrompt, 53 | responseStyle: { modality: OutputModality.VOICE }, 54 | builder, 55 | }), 56 | ); 57 | 58 | expect(voicePromptResponse).equal(getSsmlString(voicePrompt)); 59 | expect(voiceRepromptResponse).equal(getSsmlString()); 60 | }); 61 | 62 | test('Voice reprompts are included for voice modality', async () => { 63 | const { voicePromptResponse, voiceRepromptResponse } = getSsmlFromResponse( 64 | buildResponseForStyle({ 65 | voicePrompt, 66 | voiceReprompt, 67 | responseStyle: { modality: OutputModality.VOICE }, 68 | builder, 69 | }), 70 | ); 71 | 72 | expect(voicePromptResponse).equal(getSsmlString(voicePrompt)); 73 | expect(voiceRepromptResponse).equal(getSsmlString(voiceReprompt)); 74 | }); 75 | 76 | function getSsmlString(prompt?: string) { 77 | return `${prompt ?? ''}`; 78 | } 79 | 80 | function getSsmlFromResponse(response: Response) { 81 | return { 82 | voicePromptResponse: (response.outputSpeech as ui.SsmlOutputSpeech).ssml, 83 | voiceRepromptResponse: (response.reprompt?.outputSpeech as ui.SsmlOutputSpeech)?.ssml, 84 | }; 85 | } 86 | }); 87 | 88 | suite('== ResponseUtils.setSessionBehaviorForStyle ==', () => { 89 | test('Session state is OPEN when output modality is voice', async () => { 90 | const builder = new ControlResultBuilder(); 91 | setSessionBehaviorForStyle(builder, { modality: OutputModality.VOICE }); 92 | expect(builder.build().sessionBehavior).to.equal(SessionBehavior.OPEN); 93 | }); 94 | 95 | test('Session state is IDLE when output modality is screen', async () => { 96 | const builder = new ControlResultBuilder(); 97 | setSessionBehaviorForStyle(builder, { modality: OutputModality.SCREEN }); 98 | expect(builder.build().sessionBehavior).to.equal(SessionBehavior.IDLE); 99 | }); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /test/utilsTest/arrayUtils.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | import { expect } from 'chai'; 15 | import { suite, test } from 'mocha'; 16 | import { moveArrayItem, randomlyPick } from '../../src/utils/ArrayUtils'; 17 | 18 | suite('== ArrayUtils.randomlyPick() ==', () => { 19 | test('ensure distribution is reasonable', async () => { 20 | const array = [0, 1, 2]; 21 | const histogram: number[] = [0, 0, 0, 0, 0, 0, 0]; 22 | 23 | for (let i = 0; i < 30000; i++) { 24 | histogram[randomlyPick(array)]++; 25 | } 26 | 27 | for (let i = 0; i < array.length; i++) { 28 | const relativeDiff = Math.abs(histogram[0] / histogram[i] - 1.0); 29 | expect(relativeDiff).lessThan(0.1); // conservative comparison to reduce false negatives 30 | } 31 | }); 32 | }); 33 | 34 | suite('== ArrayUtils.reorder() ==', () => { 35 | test('basics', async () => { 36 | const array = [0, 1, 2]; 37 | expect(moveArrayItem([0, 1, 2, 3, 4], 1, 0)).deep.equal([1, 0, 2, 3, 4]); 38 | expect(moveArrayItem([0, 1, 2, 3, 4], 0, 2)).deep.equal([1, 2, 0, 3, 4]); 39 | expect(moveArrayItem([0, 1, 2, 3, 4], 0, 5)).deep.equal([1, 2, 3, 4, 0]); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", // es2017 is lowest with JS await keyword which makes async debugging much nicer. 4 | "outDir": "dist", 5 | "rootDir": "./", 6 | "lib": [], // do not add any libs here. See https://github.com/Microsoft/TypeScript/issues/27416 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "strict": true, 10 | "alwaysStrict": true, 11 | "strictPropertyInitialization": false, 12 | "noImplicitReturns": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "downlevelIteration": true, 15 | "inlineSourceMap": true, 16 | "inlineSources": true, 17 | "declaration": true, 18 | "declarationMap": true, 19 | "composite": true, 20 | "esModuleInterop": true, 21 | "forceConsistentCasingInFileNames": true, 22 | "importHelpers": true 23 | }, 24 | "include": [ 25 | "src", 26 | "test", 27 | "demo" 28 | ], 29 | "exclude": [ 30 | "doc", 31 | "node_modules" 32 | ], 33 | "typedocOptions": { 34 | "name": "ASK SDK Controls Framework", 35 | "readme": "./README.md", 36 | "target": "ES2018", 37 | "module": "commonjs", 38 | "out": "typedoc", 39 | "theme": "node_modules/typedoc-neo-theme/bin/default", 40 | "excludeExternals": true, 41 | "hideGenerator": true, 42 | "excludePrivate": true, 43 | "excludeNotExported": true, 44 | "links": [ 45 | 46 | ], 47 | "outline": [{ 48 | "Controls Framework (Major types)": { 49 | "Control": "classes/_controls_control_.control", 50 | "ContainerControl": "modules/_controls_containercontrol_", 51 | "DynamicContainerControl": "modules/_controls_dynamiccontainercontrol_", 52 | "ControlManager": "modules/_controls_controlmanager_", 53 | "ComponentModeControlManager": "modules/_controls_componentmodecontrolmanager_", 54 | "ControlInput": "classes/_controls_controlinput_.controlinput", 55 | "ControlResultBuilder": "classes/_controls_controlresult_.controlresultbuilder", 56 | "ControlResponseBuilder": "classes/_responsegeneration_controlresponsebuilder_.controlresponsebuilder" 57 | }, 58 | "System acts": { 59 | "SystemAct": "classes/_systemacts_systemact_.systemact", 60 | "InitiativeAct": "modules/_systemacts_initiativeacts_", 61 | "ContentAct": "modules/_systemacts_contentacts_" 62 | }, 63 | "Controls runtime": { 64 | "ControlHandler": "classes/_runtime_controlhandler_.controlhandler" 65 | }, 66 | "Controls Library": { 67 | "ValueControl": "modules/_commoncontrols_valuecontrol_", 68 | "ListControl": "modules/_commoncontrols_listcontrol_listcontrol_", 69 | "DateControl": "modules/_commoncontrols_datecontrol_", 70 | "DateRangeControl": "modules/_commoncontrols_daterangecontrol_daterangecontrol_", 71 | "NumberControl": "modules/_commoncontrols_numbercontrol_numbercontrol_", 72 | "QuestionnaireControl": "modules/_commoncontrols_questionnairecontrol_questionnairecontrol_", 73 | "MultiValueListControl": "modules/_commoncontrols_multivaluelistcontrol_multivaluelistcontrol_" 74 | }, 75 | "Interaction Model Generation": { 76 | "ControlInteractionModelGenerator": "classes/_interactionmodelgeneration_controlinteractionmodelgenerator_.controlinteractionmodelgenerator", 77 | "GeneralControlIntent": "classes/_intents_generalcontrolintent_.generalcontrolintent", 78 | "ValueControlIntent": "classes/_intents_valuecontrolintent_.valuecontrolintent", 79 | "OrdinalControlIntent": "classes/_intents_ordinalcontrolintent_.ordinalcontrolintent", 80 | "ConjunctionControlIntent": "classes/_intents_conjunctioncontrolintent_.conjunctioncontrolintent", 81 | "DateRangeControlIntent": "classes/_intents_daterangecontrolintent_.daterangecontrolintent" 82 | 83 | }, 84 | "Test support": { 85 | "SkillInvoker": "classes/_utils_testsupport_skillinvoker_.skillinvoker", 86 | "SkillWrapper": "modules/_utils_testsupport_skillwrapper_", 87 | "TestingUtils": "modules/_utils_testsupport_testingutils_" 88 | }, 89 | "Utilities (selected)": { 90 | "InputUtils": "modules/_utils_inpututil_.inpututil", 91 | "IntentUtils": "modules/_utils_intentutils_", 92 | "Predicates": "modules/_utils_predicates_" 93 | } 94 | }] 95 | } 96 | } --------------------------------------------------------------------------------