├── lambda └── py │ ├── requirements.txt │ └── lambda_function.py ├── NOTICE ├── .github └── PULL_REQUEST_TEMPLATE.md ├── CODE_OF_CONDUCT.md ├── .ask └── config ├── skill.json ├── isps.samples ├── entitlement │ ├── space_pack.json │ ├── history_pack.json │ └── science_pack.json └── subscription │ └── all_access.json ├── hooks ├── pre_deploy_hook.ps1 ├── pre_deploy_hook.sh ├── post_new_hook.sh └── post_new_hook.ps1 ├── README.md ├── CONTRIBUTING.md ├── LICENSE └── models └── en-US.json /lambda/py/requirements.txt: -------------------------------------------------------------------------------- 1 | ask-sdk 2 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Skill Sample Python Fact In Skill Purchases 2 | Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.ask/config: -------------------------------------------------------------------------------- 1 | { 2 | "deploy_settings": { 3 | "default": { 4 | "skill_id": "", 5 | "resources": { 6 | "lambda": [ 7 | { 8 | "alexaUsage": [ 9 | "custom/default" 10 | ], 11 | "handler": "lambda_function.lambda_handler", 12 | "runtime": "python3.6" 13 | } 14 | ] 15 | }, 16 | "was_cloned": false, 17 | "merge": {} 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /skill.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest": { 3 | "publishingInformation": { 4 | "locales": { 5 | "en-US": { 6 | "summary": "Sample Short Description", 7 | "examplePhrases": [ 8 | "Alexa open Premium Facts Sample", 9 | "tell me a science fact", 10 | "buy an all access subscription" 11 | ], 12 | "keywords": [], 13 | "name": "Facts Sample w/In Skill Purchasing", 14 | "description": "Sample Full Description" 15 | } 16 | }, 17 | "isAvailableWorldwide": true, 18 | "distributionCountries": [] 19 | }, 20 | "apis": { 21 | "custom": { 22 | "endpoint": { 23 | "sourceDir": "lambda/py", 24 | "uri": "ask-custom-isp-default" 25 | } 26 | } 27 | }, 28 | "manifestVersion": "1.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /isps.samples/entitlement/space_pack.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0", 3 | "type": "ENTITLEMENT", 4 | "referenceName": "space_pack", 5 | "publishingInformation": { 6 | "locales": { 7 | "en-US": { 8 | "name": "Space Pack", 9 | "summary": "The space pack is a great addition because you will hear facts about space.", 10 | "description": "The space pack expands the set of facts shared with you to include facts about space.", 11 | "smallIconUri": "https://github.com/alexa/skill-sample-nodejs-fact-in-skill-purchases/raw/master/isps.samples/s_blue_small.png", 12 | "largeIconUri": "https://github.com/alexa/skill-sample-nodejs-fact-in-skill-purchases/raw/master/isps.samples/s_blue_large.png", 13 | "examplePhrases": [ 14 | "buy space pack" 15 | ], 16 | "keywords": [ 17 | "space" 18 | ], 19 | "customProductPrompts": { 20 | "purchasePromptDescription": "The space pack adds over 10 space facts into the set of facts randomly chosen to be shared with you.", 21 | "boughtCardDescription": "Thanks for purchasing the space pack. You now have access to space facts!" 22 | } 23 | } 24 | }, 25 | "distributionCountries": [ 26 | "US" 27 | ], 28 | "pricing": { 29 | "amazon.com": { 30 | "releaseDate": "2018-05-09T00:00Z", 31 | "defaultPriceListing": { 32 | "price": 0.99, 33 | "currency": "USD" 34 | } 35 | } 36 | }, 37 | "taxInformation": { 38 | "category": "SOFTWARE" 39 | } 40 | }, 41 | "privacyAndCompliance": { 42 | "locales": { 43 | "en-US": { 44 | "privacyPolicyUrl": "https://localhost/privacy.html" 45 | } 46 | } 47 | }, 48 | "testingInstructions": "", 49 | "purchasableState": "PURCHASABLE" 50 | } 51 | -------------------------------------------------------------------------------- /isps.samples/entitlement/history_pack.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0", 3 | "type": "ENTITLEMENT", 4 | "referenceName": "history_pack", 5 | "publishingInformation": { 6 | "locales": { 7 | "en-US": { 8 | "name": "History Pack", 9 | "summary": "The history pack is a great addition because you will hear facts about history.", 10 | "description": "The history pack expands the set of facts shared with you to include facts about history.", 11 | "smallIconUri": "https://github.com/alexa/skill-sample-nodejs-fact-in-skill-purchases/raw/master/isps.samples/h_blue_small.png", 12 | "largeIconUri": "https://github.com/alexa/skill-sample-nodejs-fact-in-skill-purchases/raw/master/isps.samples/h_blue_large.png", 13 | "examplePhrases": [ 14 | "buy history pack" 15 | ], 16 | "keywords": [ 17 | "history" 18 | ], 19 | "customProductPrompts": { 20 | "purchasePromptDescription": "The history pack adds over 10 history facts into the set of facts randomly chosen to be shared with you.", 21 | "boughtCardDescription": "Thanks for purchasing the history pack. You now have access to history facts!" 22 | } 23 | } 24 | }, 25 | "distributionCountries": [ 26 | "US" 27 | ], 28 | "pricing": { 29 | "amazon.com": { 30 | "releaseDate": "2018-05-09T00:00Z", 31 | "defaultPriceListing": { 32 | "price": 0.99, 33 | "currency": "USD" 34 | } 35 | } 36 | }, 37 | "taxInformation": { 38 | "category": "SOFTWARE" 39 | } 40 | }, 41 | "privacyAndCompliance": { 42 | "locales": { 43 | "en-US": { 44 | "privacyPolicyUrl": "https://localhost/privacy.html" 45 | } 46 | } 47 | }, 48 | "testingInstructions": "", 49 | "purchasableState": "PURCHASABLE" 50 | } 51 | -------------------------------------------------------------------------------- /isps.samples/entitlement/science_pack.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0", 3 | "type": "ENTITLEMENT", 4 | "referenceName": "science_pack", 5 | "publishingInformation": { 6 | "locales": { 7 | "en-US": { 8 | "name": "Science Pack", 9 | "summary": "The science pack is a great addition because you will hear facts about science.", 10 | "description": "The science pack expands the set of facts shared with you to include facts about science.", 11 | "smallIconUri": "https://github.com/alexa/skill-sample-nodejs-fact-in-skill-purchases/raw/master/isps.samples/s_blue_small.png", 12 | "largeIconUri": "https://github.com/alexa/skill-sample-nodejs-fact-in-skill-purchases/raw/master/isps.samples/s_blue_large.png", 13 | "examplePhrases": [ 14 | "buy science pack" 15 | ], 16 | "keywords": [ 17 | "science" 18 | ], 19 | "customProductPrompts": { 20 | "purchasePromptDescription": "The science pack adds over 10 science facts into the set of facts randomly chosen to be shared with you.", 21 | "boughtCardDescription": "Thanks for purchasing the science pack. You now have access to science facts!" 22 | } 23 | } 24 | }, 25 | "distributionCountries": [ 26 | "US" 27 | ], 28 | "pricing": { 29 | "amazon.com": { 30 | "releaseDate": "2018-05-09T00:00Z", 31 | "defaultPriceListing": { 32 | "price": 0.99, 33 | "currency": "USD" 34 | } 35 | } 36 | }, 37 | "taxInformation": { 38 | "category": "SOFTWARE" 39 | } 40 | }, 41 | "privacyAndCompliance": { 42 | "locales": { 43 | "en-US": { 44 | "privacyPolicyUrl": "https://localhost/privacy.html" 45 | } 46 | } 47 | }, 48 | "testingInstructions": "", 49 | "purchasableState": "PURCHASABLE" 50 | } 51 | -------------------------------------------------------------------------------- /isps.samples/subscription/all_access.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0", 3 | "type": "SUBSCRIPTION", 4 | "referenceName": "all_access", 5 | "subscriptionInformation": { 6 | "subscriptionPaymentFrequency": "MONTHLY", 7 | "subscriptionTrialPeriodDays": 0 8 | }, 9 | "publishingInformation": { 10 | "locales": { 11 | "en-US": { 12 | "name": "All Access", 13 | "summary": "All Access is a great addition because you will hear facts about history, science and space, plus any future categories added.", 14 | "description": "All Access expands the set of facts shared with you to include facts about history, science and space.", 15 | "smallIconUri": "https://github.com/alexa/skill-sample-nodejs-fact-in-skill-purchases/raw/master/isps.samples/a_purple_small.png", 16 | "largeIconUri": "https://github.com/alexa/skill-sample-nodejs-fact-in-skill-purchases/raw/master/isps.samples/a_purple_large.png", 17 | "examplePhrases": [ 18 | "Buy All Access" 19 | ], 20 | "keywords": [ 21 | "All Access" 22 | ], 23 | "customProductPrompts": { 24 | "purchasePromptDescription": "The All Access pass adds over 30 history, science and space facts into the set of facts randomly chosen to be shared with you.", 25 | "boughtCardDescription": "Thanks for purchasing the All Access pass! You now have access to history, science and space facts!" 26 | } 27 | } 28 | }, 29 | "distributionCountries": [ 30 | "US" 31 | ], 32 | "pricing": { 33 | "amazon.com": { 34 | "releaseDate": "2018-05-01T00:00Z", 35 | "defaultPriceListing": { 36 | "price": 0.99, 37 | "currency": "USD" 38 | } 39 | } 40 | }, 41 | "taxInformation": { 42 | "category": "SOFTWARE" 43 | } 44 | }, 45 | "privacyAndCompliance": { 46 | "locales": { 47 | "en-US": { 48 | "privacyPolicyUrl": "https://localhost/privacy.html" 49 | } 50 | } 51 | }, 52 | "testingInstructions": "", 53 | "purchasableState": "PURCHASABLE" 54 | } 55 | -------------------------------------------------------------------------------- /hooks/pre_deploy_hook.ps1: -------------------------------------------------------------------------------- 1 | # Powershell script for ask-cli pre-deploy hook for Python 2 | # Script Usage: pre_deploy_hook.ps1 3 | 4 | # SKILL_NAME is the preformatted name passed from the CLI, after removing special characters. 5 | # DO_DEBUG is boolean value for debug logging 6 | # TARGET is the deploy TARGET provided to the CLI. (eg: all, skill, lambda etc.) 7 | 8 | # Run this script under the skill root folder 9 | 10 | # The script does the following: 11 | # - Create a temporary 'lambda_upload' directories under each SOURCE_DIR folder 12 | # - Copy the contents of '/SOURCE_DIR' folder into '/SOURCE_DIR/lambda_upload' 13 | # - Copy the contents of site packages in $VIRTUALENV created in /.venv/ folder 14 | # - Update the location of this 'lambda_upload' folder to skill.json for zip and upload 15 | 16 | param( 17 | [string] $SKILL_NAME, 18 | [bool] $DO_DEBUG = $False, 19 | [string] $TARGET = "all" 20 | ) 21 | 22 | if ($DO_DEBUG) { 23 | Write-Output "###########################" 24 | Write-Output "##### pre-deploy hook #####" 25 | Write-Output "###########################" 26 | } 27 | 28 | if ($TARGET -eq "all" -Or $TARGET -eq "lambda") { 29 | $ALL_SOURCE_DIRS = Get-Content -Path "skill.json" | select-string -Pattern "sourceDir" -CaseSensitive 30 | Foreach ($SOURCE_DIR in $ALL_SOURCE_DIRS) { 31 | # Step 1: Decide source path and upload path 32 | $FILTER_SOURCE_DIR = $SOURCE_DIR -replace "`"", "" -replace "\s", "" -replace ",","" -replace "sourceDir:", "" 33 | if ($FILTER_SOURCE_DIR.endsWith("/lambda_upload")) { 34 | $UPLOAD_DIR_PATH = $FILTER_SOURCE_DIR 35 | $CODE_PATH = $FILTER_SOURCE_DIR.replace("/lambda_upload", "") 36 | } else { 37 | $UPLOAD_DIR_PATH = $FILTER_SOURCE_DIR + "/lambda_upload" 38 | $CODE_PATH = $FILTER_SOURCE_DIR 39 | } 40 | # Step 2: Create empty lambda_upload folder 41 | Remove-Item -Recurse -Force $UPLOAD_DIR_PATH -ErrorAction Ignore 42 | New-Item -Force $UPLOAD_DIR_PATH -ItemType "directory" 2>&1 | Out-Null 43 | 44 | # Step 3: Copy source code in sourceDir to lambda_upload 45 | $EXCLUDE_PATH = Resolve-Path -Path ((pwd).Path + "/" + $UPLOAD_DIR_PATH) 46 | robocopy $CODE_PATH $UPLOAD_DIR_PATH /s /e /ndl /XD $EXCLUDE_PATH 2>&1 | Out-Null 47 | 48 | # Step 4: Find virtual environment site packages, copy contents to lambda_upload 49 | $SITE = $(.venv\skill_env\Scripts\python -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())") 50 | Copy-Item "$SITE\*" -Destination $UPLOAD_DIR_PATH -Recurse 51 | 52 | # Step 5: Update the "manifest.apis.custom.endpoint.sourceDir" value in skill.json if necessary 53 | if (!$FILTER_SOURCE_DIR.endsWith("/lambda_upload")) { 54 | $RAW_SOURCE_DIR_LINE = "`"sourceDir`": `"$FILTER_SOURCE_DIR`"" 55 | $NEW_SOURCE_DIR_LINE = "`"sourceDir`": `"$UPLOAD_DIR_PATH`"" 56 | (Get-Content "skill.json").replace($RAW_SOURCE_DIR_LINE, $NEW_SOURCE_DIR_LINE) | Set-Content "skill.json" 57 | } 58 | } 59 | 60 | if ($DO_DEBUG) { 61 | Write-Output "###########################" 62 | } 63 | 64 | exit 0 65 | } 66 | -------------------------------------------------------------------------------- /hooks/pre_deploy_hook.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Shell script for ask-cli pre-deploy hook for Python 3 | # Script Usage: pre_deploy_hook.sh 4 | 5 | # SKILL_NAME is the preformatted name passed from the CLI, after removing special characters. 6 | # DO_DEBUG is boolean value for debug logging 7 | # TARGET is the deploy TARGET provided to the CLI. (eg: all, skill, lambda etc.) 8 | 9 | # Run this script under skill root folder 10 | 11 | # The script does the following: 12 | # - Create a temporary 'lambda_upload' directories under each SOURCE_DIR folder 13 | # - Copy the contents of '/SOURCE_DIR' folder into '/SOURCE_DIR/lambda_upload' 14 | # - Copy the contents of site packages in $VIRTUALENV created in /.venv/ folder 15 | # - Update the location of this 'lambda_upload' folder to skill.json for zip and upload 16 | 17 | SKILL_NAME=$1 18 | DO_DEBUG=${2:-false} 19 | TARGET=${3:-"all"} 20 | SKILL_ENV_NAME="skill_env" 21 | 22 | if ! $DO_DEBUG ; then 23 | exec > /dev/null 2>&1 24 | fi 25 | 26 | echo "###########################" 27 | echo "##### pre-deploy hook #####" 28 | echo "###########################" 29 | 30 | 31 | RET=0 32 | if [[ $TARGET == "all" || $TARGET == "lambda" ]]; then 33 | grep "sourceDir" ./skill.json | cut -d: -f2 | sed 's/"//g' | sed 's/,//g' | while read -r SOURCE_DIR; do 34 | # Step 1: Decide source path and upload path 35 | if [[ $SOURCE_DIR == */lambda_upload ]]; then 36 | ADJUSTED_SOURCE_DIR=${SOURCE_DIR%"/lambda_upload"} 37 | UPLOAD_DIR=$SOURCE_DIR 38 | else 39 | ADJUSTED_SOURCE_DIR=$SOURCE_DIR 40 | UPLOAD_DIR="$SOURCE_DIR/lambda_upload" 41 | fi 42 | 43 | # Step 2: Create empty lambda_upload folder 44 | echo "Checking for lambda_upload folder existence in sourceDir $ADJUSTED_SOURCE_DIR" 45 | rm -rf $UPLOAD_DIR 46 | mkdir $UPLOAD_DIR 47 | 48 | # Step 3: Copy source code in sourceDir to lambda_upload 49 | echo "Copying source code in $SKILL_NAME/$ADJUSTED_SOURCE_DIR folder to $SKILL_NAME/$UPLOAD_DIR" 50 | rsync -avzq --exclude '*lambda_upload' $ADJUSTED_SOURCE_DIR/* $UPLOAD_DIR 51 | 52 | # Step 4: Find virtual environment site packages, copy contents to lambda_upload 53 | echo "Copying dependencies installed in $SKILL_NAME/.venv/$SKILL_ENV_NAME to $SKILL_NAME/$UPLOAD_DIR" 54 | if [[ ! "$(ls -A .venv/$SKILL_ENV_NAME/bin/python)" ]]; then 55 | echo "Failed to get virtual env Python runtime" 56 | RET=1 57 | break; 58 | fi 59 | 60 | SITE=$(.venv/$SKILL_ENV_NAME/bin/python -c 'from distutils.sysconfig import get_python_lib; print(get_python_lib())') 61 | if [[ "$(ls -A $SITE/*)" ]]; then 62 | cp -r $SITE/* $UPLOAD_DIR 63 | else 64 | echo "Failed to get the SITE package path" 65 | RET=1 66 | break; 67 | fi 68 | 69 | # Step 5: Update the "manifest.apis.custom.endpoint.sourceDir" value in skill.json if necessary 70 | if ! [[ $SOURCE_DIR == */lambda_upload ]]; then 71 | echo "Updating sourceDir to point to lambda_upload folder in skill.json" 72 | RAW_SOURCE_DIR_LINE="\"sourceDir\": \"$SOURCE_DIR\"" 73 | NEW_SOURCE_DIR_LINE="\"sourceDir\": \"$UPLOAD_DIR\"" 74 | sed -in "s#$RAW_SOURCE_DIR_LINE#$NEW_SOURCE_DIR_LINE#g" ./skill.json 75 | fi 76 | done 77 | echo "###########################" 78 | fi 79 | 80 | exit $RET 81 | -------------------------------------------------------------------------------- /hooks/post_new_hook.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Shell script for ask-cli post-new hook for Python 3 | # Script Usage: post_new_hook.sh 4 | 5 | # SKILL_NAME is the preformatted name passed from the CLI, after removing special characters. 6 | # DO_DEBUG is boolean value for debug logging 7 | 8 | # Run this script one level outside of the skill root folder 9 | 10 | # The script does the following: 11 | # - Create a '.venv' directory under folder 12 | # - Find if python3 is installed. 13 | # - If yes, try creating virtual environment using built-in venv 14 | # - If that fails, install virtualenv and create virtualenv 15 | # - If no, install virtualenv and create virtualenv 16 | # - If virtual environment is created, use container pip to install dependencies from ${SOURCE_DIR}/requirements.txt 17 | # - Provide message on activation script location and additional dependencies 18 | 19 | create_env () { 20 | # Check for Python3 installation 21 | if command -v python3 &> /dev/null; then 22 | PYTHON=python3 23 | # Use Python3's venv script to create virtualenv. 24 | if $PYTHON -m venv "$ENV_LOC"; then 25 | echo "Using Python3's venv script" 26 | return 0 27 | else 28 | # No venv script present (< Py 3.3). Install using virtualenv 29 | create_using_virtualenv $PYTHON 30 | return $? 31 | fi 32 | else 33 | # Python2 environment. Install using virtualenv 34 | PYTHON=python 35 | create_using_virtualenv $PYTHON 36 | return $? 37 | fi 38 | return 1 39 | } 40 | 41 | create_using_virtualenv () { 42 | # Check for virtualenv installation or install 43 | if $1 -m pip install virtualenv; then 44 | echo "Using virtualenv library" 45 | # Try creating env 46 | if $1 -m virtualenv "$ENV_LOC"; then 47 | return 0 48 | else 49 | echo "There was a problem creating virtualenv" 50 | return 1 51 | fi 52 | else 53 | echo "There was a problem installing virtualenv" 54 | return 1 55 | fi 56 | } 57 | 58 | install_dependencies() { 59 | # Install dependencies at lambda/py/requirements.txt 60 | return $("$ENV_LOC"/bin/python -m pip -q install -r "$SKILL_DIR"/"$1"/requirements.txt) 61 | } 62 | 63 | SKILL_NAME=$1 64 | DO_DEBUG=${2:-false} 65 | SKILL_DIR=$SKILL_NAME 66 | SKILL_ENV_NAME="skill_env" 67 | ENV_LOC="$SKILL_DIR/.venv/$SKILL_ENV_NAME" 68 | 69 | if ! $DO_DEBUG ; then 70 | exec > /dev/null 2>&1 71 | fi 72 | 73 | echo "###########################" 74 | echo "###### post-new hook ######" 75 | echo "###########################" 76 | echo "Creating virtualenv for $SKILL_NAME" 77 | mkdir "$SKILL_NAME/.venv" 78 | if create_env; then 79 | echo "Created $SKILL_ENV_NAME virtualenv at $ENV_LOC" 80 | echo "###########################" 81 | echo "Installing dependencies based on sourceDir" 82 | grep "sourceDir" "$SKILL_NAME/skill.json" | cut -d: -f2 | sed 's/"//g' | sed 's/,//g' | while read -r SOURCE_DIR; do 83 | if install_dependencies $SOURCE_DIR; then 84 | echo "Codebase ($SOURCE_DIR) built successfully." 85 | else 86 | echo "There was a problem installing dependencies for ($SOURCE_DIR)." 87 | exit 1 88 | fi 89 | done 90 | echo "###########################" 91 | echo "Activate the environment before installing any other dependencies by running 'source $ENV_LOC/bin/activate'" 92 | exit 0 93 | else 94 | exit 1 95 | fi 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Build An Alexa Skill with In-Skill Purchases using Developer Console 2 | 3 | 4 | 5 | Create Skill on Developer Console by importing from GitHub 6 | -------------------- 7 | 8 | 1. Go to the **[Alexa Developer Console](https://developer.amazon.com/alexa/console/ask)**. Enter your account credentials and click the **Sign In** button. 9 | (If you don't already have an account, you will be able to create a new one for free.) 10 | 11 | 2. Once you have signed in, select the **Create Skill** button near the top-right of the list of your Alexa Skills. 12 | 13 | 3. Give your new skill a **Name**, for example, 'Premium Facts Sample'. 14 | 15 | 4. Select the Default Language. This tutorial will presume you have selected 'English (US)'. Click the **Next** button at the top right. 16 | 17 | 5. Select **Other** under the *'Choose a type of experience'* section. 18 | 19 | 6. Select the **Custom** model under the *'Choose a model'* section. 20 | 21 | 7. Select **Alexa-hosted (Python)** under the *'Hosting services'* section. Click the **Next** button at the top right. 22 | 23 | 8. Choose **Start from scratch** from the *Templates* section and click the **Import skill** button at the top right. 24 | 25 | 9. Paste the following link in the dialog box and click **Import**. 26 | ``` 27 | https://github.com/alexa-samples/skill-sample-python-fact-in-skill-purchases 28 | ``` 29 | 30 | Once the skill is created, you will have an Alexa-hosted skill with the interaction model based on the content provided in this [JSON file](models/en-US.json), and the code as per this [lambda function](lambda/py/lambda_function.py). 31 | 32 | Creating In-Skill Products 33 | -------------------- 34 | 35 | 1. To create an In-Skill product, on the **Build** page, under Skill builder checklist, click **Monetize Your Skill**. Or, in the left pane, click **TOOLS**, and then select **Monetize Your Skill**. 36 | 37 | 2. Follow all the steps in the documentation for **[Add a product and link it with the skill](https://developer.amazon.com/en-US/docs/alexa/in-skill-purchase/create-isp-dev-console.html#create-and-edit-in-skill-products)** 38 | 39 | Testing 40 | -------------------- 41 | 42 | 1. To test, login to [Alexa Developer Console](https://developer.amazon.com/alexa/console/as), click on the **Premium Facts Sample** entry in your skill list, and click on the "Test" tab. The "Test" switch on your skill should have been automatically enabled. If it was not, enable it now. 43 | 44 | 2. Your skill can now be tested on devices associated with your developer account, as well as the Test tab in the Developer Portal. To start using your skill, just type or say: 45 | 46 | ```text 47 | Alexa, open premium facts sample 48 | ``` 49 | 50 | **Note: The developer account associated with the skill is never charged for in-skill products.** For more details about testing skills with in-skill products, please refer to the [In-Skill Purchase Testing Guide](https://developer.amazon.com/docs/in-skill-purchase/isp-test-guide.html) 51 | 52 | 53 | Additional Resources 54 | -------------------- 55 | 56 | ### Community 57 | 58 | - [Amazon Developer Forums](https://forums.developer.amazon.com/spaces/165/index.html) : Join the conversation! 59 | - [Hackster.io](https://www.hackster.io/amazon-alexa) - See what others are building with Alexa. 60 | 61 | ### Documentation 62 | 63 | - [Create and Manage In-Skill Products](https://developer.amazon.com/en-US/docs/alexa/in-skill-purchase/create-isp-dev-console.html) 64 | - [Test In-Skill Purchasing Skills](https://developer.amazon.com/en-US/docs/alexa/in-skill-purchase/isp-test-guide.html#test-products) 65 | 66 | -------------------------------------------------------------------------------- /hooks/post_new_hook.ps1: -------------------------------------------------------------------------------- 1 | # Powershell script for ask-cli post-new hook for Python 2 | # Script Usage: post_new_hook.ps1 3 | 4 | # SKILL_NAME is the preformatted name passed from the CLI, after removing special characters. 5 | # DO_DEBUG is boolean value for debug logging 6 | 7 | # Run this script one level outside of the skill root folder 8 | 9 | # The script does the following: 10 | # - Create a '.venv' directory under folder 11 | # - Find if python3 is installed. 12 | # - If yes, try creating virtual environment using built-in venv 13 | # - If that fails, install virtualenv and create virtualenv 14 | # - If no, install virtualenv and create virtualenv 15 | # - If virtual environment is created, use container pip to install dependencies from ${SOURCE_DIR}/requirements.txt 16 | # - Provide message on activation script location and additional dependencies 17 | 18 | param( 19 | [string] $SKILL_NAME, 20 | [bool] $DO_DEBUG = $False 21 | ) 22 | 23 | if ($DO_DEBUG) { 24 | Write-Output "###########################" 25 | Write-Output "###### post-new hook ######" 26 | Write-Output "###########################" 27 | } 28 | 29 | function create_env () { 30 | # Check for Python3 installation 31 | python -V | Select-String -Pattern "Python 3." 2>&1 | Out-Null 32 | if ($?) { 33 | python -m venv $ENV_LOC 2>&1 | Out-Null 34 | if ($?) { 35 | return $true 36 | } 37 | } 38 | return create_using_virtualenv 39 | } 40 | 41 | function create_using_virtualenv() { 42 | # Check for virtualenv installation or install 43 | python -m pip install virtualenv 2>&1 | Out-Null 44 | if ($?) { 45 | python -m virtualenv $ENV_LOC 2>&1 | Out-Null 46 | if ($?) { 47 | return $true 48 | } 49 | } 50 | if ($DO_DEBUG) { 51 | Write-Output "There was a problem installing virtualenv" 52 | } 53 | return $false 54 | } 55 | 56 | function install_dependencies($PARAM_SOURCE_DIR) { 57 | # Install dependencies at lambda/py/requirements.txt 58 | $PYTHON_PATH = $ENV_LOC + "\Scripts\python" 59 | $REQUIREMENTS_PATH = $SKILL_NAME + "\" + $PARAM_SOURCE_DIR + "\requirements.txt" 60 | $CMD = "$PYTHON_PATH -m pip -q install -r $REQUIREMENTS_PATH" 61 | return Invoke-Expression $CMD 2>&1 | Out-Null 62 | } 63 | 64 | 65 | $SKILL_ENV_NAME = "skill_env" 66 | $ENV_LOC = $SKILL_NAME + "\.venv\" + $SKILL_ENV_NAME 67 | if (create_env) { 68 | $SKILL_FILE_PATH = $SKILL_NAME + "\skill.json" 69 | $ALL_SOURCE_DIRS = Get-Content -Path $SKILL_FILE_PATH | select-string -Pattern "sourceDir" -CaseSensitive 70 | if ($DO_DEBUG) { 71 | Write-Output "Created $SKILL_ENV_NAME virtualenv at $ENV_LOC" 72 | Write-Output "###########################" 73 | Write-Output "Installing dependencies based on sourceDir" 74 | } 75 | Foreach ($SOURCE_DIR in $ALL_SOURCE_DIRS) { 76 | $FILTER_SOURCE_DIR = $SOURCE_DIR -replace "`"", "" -replace "\s", "" -replace ",","" -replace "sourceDir:", "" 77 | if (-Not (install_dependencies $FILTER_SOURCE_DIR)) { 78 | if ($DO_DEBUG) { 79 | Write-Output "Codebase ($FILTER_SOURCE_DIR) built successfully." 80 | } 81 | } else { 82 | if ($DO_DEBUG) { 83 | Write-Output "There was a problem installing dependencies for ($FILTER_SOURCE_DIR)." 84 | } 85 | exit 1 86 | } 87 | } 88 | if ($DO_DEBUG) { 89 | Write-Output "###########################" 90 | Write-Output "Activate the environment before installing any other dependencies by running 'source $ENV_LOC/bin/activate'" 91 | } 92 | exit 0 93 | } else { 94 | exit 1 95 | } 96 | -------------------------------------------------------------------------------- /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](https://github.com/alexa-labs/skill-sample-python-fact-in-skill-purchases/issues), or [recently closed](https://github.com/alexa-labs/skill-sample-python-fact-in-skill-purchases/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), 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 *master* 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. Fork the repository. 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 fork using clear commit messages. 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 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | 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'](https://github.com/alexa-labs/skill-sample-python-fact-in-skill-purchases/labels/help%20wanted) issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | 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. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](https://github.com/alexa-labs/skill-sample-python-fact-in-skill-purchases/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Amazon Software License 1.0 2 | 3 | This Amazon Software License ("License") governs your use, reproduction, and 4 | distribution of the accompanying software as specified below. 5 | 6 | 1. Definitions 7 | 8 | "Licensor" means any person or entity that distributes its Work. 9 | 10 | "Software" means the original work of authorship made available under this 11 | License. 12 | 13 | "Work" means the Software and any additions to or derivative works of the 14 | Software that are made available under this License. 15 | 16 | The terms "reproduce," "reproduction," "derivative works," and 17 | "distribution" have the meaning as provided under U.S. copyright law; 18 | provided, however, that for the purposes of this License, derivative works 19 | shall not include works that remain separable from, or merely link (or bind 20 | by name) to the interfaces of, the Work. 21 | 22 | Works, including the Software, are "made available" under this License by 23 | including in or with the Work either (a) a copyright notice referencing the 24 | applicability of this License to the Work, or (b) a copy of this License. 25 | 26 | 2. License Grants 27 | 28 | 2.1 Copyright Grant. Subject to the terms and conditions of this License, 29 | each Licensor grants to you a perpetual, worldwide, non-exclusive, 30 | royalty-free, copyright license to reproduce, prepare derivative works of, 31 | publicly display, publicly perform, sublicense and distribute its Work and 32 | any resulting derivative works in any form. 33 | 34 | 2.2 Patent Grant. Subject to the terms and conditions of this License, each 35 | Licensor grants to you a perpetual, worldwide, non-exclusive, royalty-free 36 | patent license to make, have made, use, sell, offer for sale, import, and 37 | otherwise transfer its Work, in whole or in part. The foregoing license 38 | applies only to the patent claims licensable by Licensor that would be 39 | infringed by Licensor's Work (or portion thereof) individually and 40 | excluding any combinations with any other materials or technology. 41 | 42 | 3. Limitations 43 | 44 | 3.1 Redistribution. You may reproduce or distribute the Work only if 45 | (a) you do so under this License, (b) you include a complete copy of this 46 | License with your distribution, and (c) you retain without modification 47 | any copyright, patent, trademark, or attribution notices that are present 48 | in the Work. 49 | 50 | 3.2 Derivative Works. You may specify that additional or different terms 51 | apply to the use, reproduction, and distribution of your derivative works 52 | of the Work ("Your Terms") only if (a) Your Terms provide that the use 53 | limitation in Section 3.3 applies to your derivative works, and (b) you 54 | identify the specific derivative works that are subject to Your Terms. 55 | Notwithstanding Your Terms, this License (including the redistribution 56 | requirements in Section 3.1) will continue to apply to the Work itself. 57 | 58 | 3.3 Use Limitation. The Work and any derivative works thereof only may be 59 | used or intended for use with the web services, computing platforms or 60 | applications provided by Amazon.com, Inc. or its affiliates, including 61 | Amazon Web Services, Inc. 62 | 63 | 3.4 Patent Claims. If you bring or threaten to bring a patent claim against 64 | any Licensor (including any claim, cross-claim or counterclaim in a 65 | lawsuit) to enforce any patents that you allege are infringed by any Work, 66 | then your rights under this License from such Licensor (including the 67 | grants in Sections 2.1 and 2.2) will terminate immediately. 68 | 69 | 3.5 Trademarks. This License does not grant any rights to use any 70 | Licensor's or its affiliates' names, logos, or trademarks, except as 71 | necessary to reproduce the notices described in this License. 72 | 73 | 3.6 Termination. If you violate any term of this License, then your rights 74 | under this License (including the grants in Sections 2.1 and 2.2) will 75 | terminate immediately. 76 | 77 | 4. Disclaimer of Warranty. 78 | 79 | THE WORK IS PROVIDED "AS IS" WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 80 | EITHER EXPRESS OR IMPLIED, INCLUDING WARRANTIES OR CONDITIONS OF 81 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE OR 82 | NON-INFRINGEMENT. YOU BEAR THE RISK OF UNDERTAKING ANY ACTIVITIES UNDER 83 | THIS LICENSE. SOME STATES' CONSUMER LAWS DO NOT ALLOW EXCLUSION OF AN 84 | IMPLIED WARRANTY, SO THIS DISCLAIMER MAY NOT APPLY TO YOU. 85 | 86 | 5. Limitation of Liability. 87 | 88 | EXCEPT AS PROHIBITED BY APPLICABLE LAW, IN NO EVENT AND UNDER NO LEGAL 89 | THEORY, WHETHER IN TORT (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE 90 | SHALL ANY LICENSOR BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY DIRECT, 91 | INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF OR 92 | RELATED TO THIS LICENSE, THE USE OR INABILITY TO USE THE WORK (INCLUDING 93 | BUT NOT LIMITED TO LOSS OF GOODWILL, BUSINESS INTERRUPTION, LOST PROFITS 94 | OR DATA, COMPUTER FAILURE OR MALFUNCTION, OR ANY OTHER COMM ERCIAL DAMAGES 95 | OR LOSSES), EVEN IF THE LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF 96 | SUCH DAMAGES. 97 | -------------------------------------------------------------------------------- /models/en-US.json: -------------------------------------------------------------------------------- 1 | { 2 | "interactionModel": { 3 | "languageModel": { 4 | "invocationName": "premium facts sample", 5 | "intents": [ 6 | { 7 | "name": "AMAZON.CancelIntent", 8 | "samples": [] 9 | }, 10 | { 11 | "name": "AMAZON.HelpIntent", 12 | "samples": [ 13 | "help me" 14 | ] 15 | }, 16 | { 17 | "name": "AMAZON.StopIntent", 18 | "samples": [] 19 | }, 20 | { 21 | "name": "AMAZON.YesIntent", 22 | "samples": [] 23 | }, 24 | { 25 | "name": "AMAZON.NoIntent", 26 | "samples": [] 27 | }, 28 | { 29 | "name": "GetFactIntent", 30 | "slots": [], 31 | "samples": [ 32 | "read fact", 33 | "read a fact", 34 | "Tell me a fact", 35 | "Give me a fact" 36 | ] 37 | }, 38 | { 39 | "name": "GetCategoryFactIntent", 40 | "slots": [ 41 | { 42 | "name": "factCategory", 43 | "type": "factType", 44 | "samples": [ 45 | "{factCategory} fact", 46 | "{factCategory}" 47 | ] 48 | } 49 | ], 50 | "samples": [ 51 | "I want a {factCategory} fact", 52 | "{factCategory} fact", 53 | "Tell me a {factCategory} fact", 54 | "Get me a {factCategory} fact" 55 | ] 56 | }, 57 | { 58 | "name": "ShoppingIntent", 59 | "slots": [], 60 | "samples": [ 61 | "I want to purchase", 62 | "I want to buy", 63 | "what could i buy", 64 | "can i buy something", 65 | "i want to buy something", 66 | "what is available to buy", 67 | "what can i buy" 68 | ] 69 | }, 70 | { 71 | "name": "BuyIntent", 72 | "slots": [ 73 | { 74 | "name": "productCategory", 75 | "type": "factType" 76 | } 77 | ], 78 | "samples": [ 79 | "subscribe for all access", 80 | "subscribe for all access pack", 81 | "subscribe to all access", 82 | "buy all access", 83 | "subscribe all access pack", 84 | "subscribe to all access pack", 85 | "buy all access subscription", 86 | "I want to buy all access subscription", 87 | "i want to buy all access pack", 88 | "let's buy {productCategory} pack", 89 | "how do i buy {productCategory} pack", 90 | "Can I buy {productCategory} pack", 91 | "purchase {productCategory} pack ", 92 | "buy {productCategory} pack", 93 | "I want to buy {productCategory} pack" 94 | ] 95 | }, 96 | { 97 | "name": "CancelSubscriptionIntent", 98 | "slots": [ 99 | { 100 | "name": "productCategory", 101 | "type": "factType" 102 | } 103 | ], 104 | "samples": [ 105 | "cancel {productCategory} fact", 106 | "cancel all access", 107 | "cancel subscription", 108 | "cancel all access subscription", 109 | "Cancel all access pack", 110 | "Cancel {productCategory} pack" 111 | ] 112 | }, 113 | { 114 | "name": "ProductDetailIntent", 115 | "slots": [ 116 | { 117 | "name": "productCategory", 118 | "type": "factType", 119 | "samples": [ 120 | "{productCategory} pack", 121 | "Tell me about a {productCategory} pack", 122 | "Tell me about the {productCategory} pack", 123 | "Tell me about {productCategory} pack" 124 | ] 125 | }, 126 | { 127 | "name": "allAccess", 128 | "type": "allAccessType", 129 | "samples": [ 130 | "{allAccess} pack", 131 | "Tell me about a {allAccess} pack", 132 | "Tell me about the {allAccess} pack", 133 | "Tell me about {allAccess} pack" 134 | ] 135 | } 136 | ], 137 | "samples": [ 138 | "tell me more about {productCategory} pack", 139 | "tell me more about the {productCategory} pack", 140 | "tell me more about {productCategory} product", 141 | "tell me about {productCategory} product", 142 | "Tell me about {productCategory} pack", 143 | "tell me more about {allAccess} pack", 144 | "tell me more about the {allAccess} pack", 145 | "tell me more about {allAccess} product", 146 | "tell me about {allAccess} product", 147 | "Tell me about {allAccess} pack" 148 | ] 149 | }, 150 | { 151 | "name": "AMAZON.FallbackIntent", 152 | "samples": [] 153 | } 154 | ], 155 | "types": [ 156 | { 157 | "name": "factType", 158 | "values": [ 159 | { 160 | "name": { 161 | "value": "space" 162 | } 163 | }, 164 | { 165 | "name": { 166 | "value": "history" 167 | } 168 | }, 169 | { 170 | "name": { 171 | "value": "science" 172 | } 173 | } 174 | ] 175 | }, 176 | { 177 | "name": "allAccessType", 178 | "values": [ 179 | { 180 | "name": { 181 | "value": "all access" 182 | } 183 | } 184 | ] 185 | } 186 | ] 187 | }, 188 | "dialog": { 189 | "intents": [ 190 | { 191 | "name": "GetCategoryFactIntent", 192 | "confirmationRequired": false, 193 | "prompts": {}, 194 | "slots": [ 195 | { 196 | "name": "factCategory", 197 | "type": "factType", 198 | "confirmationRequired": false, 199 | "elicitationRequired": true, 200 | "prompts": { 201 | "elicitation": "Elicit.Slot.339715283752.1159186034704" 202 | } 203 | } 204 | ] 205 | }, 206 | { 207 | "name": "ProductDetailIntent", 208 | "confirmationRequired": false, 209 | "prompts": {}, 210 | "slots": [ 211 | { 212 | "name": "productCategory", 213 | "type": "factType", 214 | "confirmationRequired": false, 215 | "elicitationRequired": true, 216 | "prompts": { 217 | "elicitation": "Elicit.Slot.592643468739.613787488648" 218 | } 219 | } 220 | ] 221 | } 222 | ] 223 | }, 224 | "prompts": [ 225 | { 226 | "id": "Elicit.Slot.339715283752.1159186034704", 227 | "variations": [ 228 | { 229 | "type": "PlainText", 230 | "value": "Would you like a science, space or history fact?" 231 | } 232 | ] 233 | }, 234 | { 235 | "id": "Elicit.Slot.592643468739.613787488648", 236 | "variations": [ 237 | { 238 | "type": "PlainText", 239 | "value": "Tell me the name of the product you would like to learn more about. For example, you could say, 'Tell me about the science pack'" 240 | } 241 | ] 242 | } 243 | ] 244 | } 245 | } -------------------------------------------------------------------------------- /lambda/py/lambda_function.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import random 3 | import logging 4 | 5 | from typing import Union, List 6 | 7 | from ask_sdk.standard import StandardSkillBuilder 8 | from ask_sdk_core.dispatch_components import ( 9 | AbstractRequestHandler, AbstractExceptionHandler, 10 | AbstractRequestInterceptor, AbstractResponseInterceptor) 11 | from ask_sdk_core.handler_input import HandlerInput 12 | from ask_sdk_core.utils import is_request_type, is_intent_name 13 | 14 | from ask_sdk_model.services.monetization import ( 15 | EntitledState, PurchasableState, InSkillProductsResponse, Error, 16 | InSkillProduct) 17 | from ask_sdk_model.interfaces.monetization.v1 import PurchaseResult 18 | from ask_sdk_model import Response, IntentRequest 19 | from ask_sdk_model.interfaces.connections import SendRequestDirective 20 | 21 | logger = logging.getLogger(__name__) 22 | logger.setLevel(logging.INFO) 23 | 24 | # Data for the skill 25 | 26 | # Static list of facts across 3 categories that serve as 27 | # the free and premium content served by the Skill 28 | all_facts = [ 29 | { 30 | "type": "science", 31 | "fact": "There is enough DNA in an average person's body to stretch from the sun to Pluto and back — 17 times." 32 | }, 33 | { 34 | "type": "science", 35 | "fact": "The average human body carries ten times more bacterial cells than human cells." 36 | }, 37 | { 38 | "type": "science", 39 | "fact": "It can take a photon 40,000 years to travel from the core of the sun to its surface, but only 8 minutes to travel the rest of the way to Earth." 40 | }, 41 | { 42 | "type": "science", 43 | "fact": "At over 2000 kilometers long, The Great Barrier Reef is the largest living structure on Earth." 44 | }, 45 | { 46 | "type": "science", 47 | "fact": "There are 8 times as many atoms in a teaspoonful of water as there are teaspoonfuls of water in the Atlantic ocean." 48 | }, 49 | { 50 | "type": "science", 51 | "fact": "The average person walks the equivalent of five times around the world in a lifetime." 52 | }, 53 | { 54 | "type": "science", 55 | "fact": "When Helium is cooled to absolute zero it flows against gravity and will start running up and over the lip of a glass container!" 56 | }, 57 | { 58 | "type": "science", 59 | "fact": "An individual blood cell takes about 60 seconds to make a complete circuit of the body." 60 | }, 61 | { 62 | "type": "science", 63 | "fact": "The human eye blinks an average of 4,200,000 times a year." 64 | }, 65 | { 66 | "type": "history", 67 | "fact": "The Hundred Years War actually lasted 116 years from thirteen thirty seven to fourteen fifty three." 68 | }, 69 | { 70 | "type": "history", 71 | "fact": "There are ninety two known cases of nuclear bombs lost at sea." 72 | }, 73 | { 74 | "type": "history", 75 | "fact": "Despite popular belief, Napoleon Bonaparte stood 5 feet 6 inch tall. Average height for men at the time." 76 | }, 77 | { 78 | "type": "history", 79 | "fact": "Leonardo Da Vinci designed the first helicopter, tank, submarine, parachute and ammunition igniter... Five hundred years ago." 80 | }, 81 | { 82 | "type": "history", 83 | "fact": "The shortest war on record was fought between Zanzibar and England in eighteen ninety six. Zanzibar surrendered after 38 minutes." 84 | }, 85 | { 86 | "type": "history", 87 | "fact": "X-rays of the Mona Lisa show that there are 3 different versions under the present one." 88 | }, 89 | { 90 | "type": "history", 91 | "fact": "At Andrew Jackson's funeral in 1845, his pet parrot had to be removed because it was swearing too much." 92 | }, 93 | { 94 | "type": "history", 95 | "fact": "English was once a language for “commoners,” while the British elites spoke French." 96 | }, 97 | { 98 | "type": "history", 99 | "fact": "In ancient Egypt, servants were smeared with honey in order to attract flies away from the pharaoh." 100 | }, 101 | { 102 | "type": "history", 103 | "fact": "Ronald Reagan was a lifeguard during high school and saved 77 people’s lives." 104 | }, 105 | { 106 | "type": "space", 107 | "fact": "A year on Mercury is just 88 days long." 108 | }, 109 | { 110 | "type": "space", 111 | "fact": "Despite being farther from the Sun, Venus experiences higher temperatures than Mercury." 112 | }, 113 | { 114 | "type": "space", 115 | "fact": "Venus rotates anti-clockwise, possibly because of a collision in the past with an asteroid." 116 | }, 117 | { 118 | "type": "space", 119 | "fact": "On Mars, the Sun appears about half the size as it does on Earth." 120 | }, 121 | { 122 | "type": "space", 123 | "fact": "Earth is the only planet not named after a god." 124 | }, 125 | { 126 | "type": "space", 127 | "fact": "Jupiter has the shortest day of all the planets." 128 | }, 129 | { 130 | "type": "space", 131 | "fact": "The Milky Way galaxy will collide with the Andromeda Galaxy in about 5 billion years." 132 | }, 133 | { 134 | "type": "space", 135 | "fact": "The Sun contains 99.86% of the mass in the Solar System." 136 | }, 137 | { 138 | "type": "space", 139 | "fact": "The Sun is an almost perfect sphere." 140 | }, 141 | { 142 | "type": "space", 143 | "fact": "A total solar eclipse can happen once every 1 to 2 years. This makes them a rare event." 144 | }, 145 | ] 146 | 147 | skill_name = "Premium Facts Sample" 148 | 149 | # Utility functions 150 | 151 | def get_all_entitled_products(in_skill_product_list): 152 | """Get list of in-skill products in ENTITLED state.""" 153 | # type: (List[InSkillProduct]) -> List[InSkillProduct] 154 | entitled_product_list = [ 155 | l for l in in_skill_product_list if ( 156 | l.entitled == EntitledState.ENTITLED)] 157 | return entitled_product_list 158 | 159 | def get_random_from_list(facts): 160 | """Return the fact message from randomly chosen list element.""" 161 | # type: (List) -> str 162 | fact_item = random.choice(facts) 163 | return fact_item.get("fact") 164 | 165 | def get_random_yes_no_question(): 166 | """Return random question for YES/NO answering.""" 167 | # type: () -> str 168 | questions = [ 169 | "Would you like another fact?", "Can I tell you another fact?", 170 | "Do you want to hear another fact?"] 171 | return random.choice(questions) 172 | 173 | def get_random_goodbye(): 174 | """Return random goodbye message.""" 175 | # type: () -> str 176 | goodbyes = ["OK. Goodbye!", "Have a great day!", "Come back again soon!"] 177 | return random.choice(goodbyes) 178 | 179 | def get_speakable_list_of_products(entitled_products_list): 180 | """Return product list in speakable form.""" 181 | # type: (List[InSkillProduct]) -> str 182 | product_names = [item.name for item in entitled_products_list] 183 | if len(product_names) > 1: 184 | # If more than one, add and 'and' in the end 185 | speech = " and ".join( 186 | [", ".join(product_names[:-1]), product_names[-1]]) 187 | else: 188 | # If one or none, then return the list content in a string 189 | speech = ", ".join(product_names) 190 | return speech 191 | 192 | def get_resolved_value(request, slot_name): 193 | """Resolve the slot name from the request using resolutions.""" 194 | # type: (IntentRequest, str) -> Union[str, None] 195 | try: 196 | return (request.intent.slots[slot_name].resolutions. 197 | resolutions_per_authority[0].values[0].value.name) 198 | except (AttributeError, ValueError, KeyError, IndexError): 199 | return None 200 | 201 | def get_spoken_value(request, slot_name): 202 | """Resolve the slot to the spoken value.""" 203 | # type: (IntentRequest, str) -> Union[str, None] 204 | try: 205 | return request.intent.slots[slot_name].value 206 | except (AttributeError, ValueError, KeyError, IndexError): 207 | return None 208 | 209 | def is_product(product): 210 | """Is the product list not empty.""" 211 | # type: (List) -> bool 212 | return bool(product) 213 | 214 | def is_entitled(product): 215 | """Is the product in ENTITLED state.""" 216 | # type: (List) -> bool 217 | return (is_product(product) and 218 | product[0].entitled == EntitledState.ENTITLED) 219 | 220 | def in_skill_product_response(handler_input): 221 | """Get the In-skill product response from monetization service.""" 222 | # type: (HandlerInput) -> Union[InSkillProductsResponse, Error] 223 | locale = handler_input.request_envelope.request.locale 224 | ms = handler_input.service_client_factory.get_monetization_service() 225 | return ms.get_in_skill_products(locale) 226 | 227 | # Skill Handlers 228 | 229 | class LaunchRequestHandler(AbstractRequestHandler): 230 | """Handler for Launch Requests. 231 | 232 | The handler gets the in-skill products for the user, and provides 233 | a custom welcome message depending on the ownership of the products 234 | to the user. 235 | User says: Alexa, open . 236 | """ 237 | def can_handle(self, handler_input): 238 | # type: (HandlerInput) -> bool 239 | return is_request_type("LaunchRequest")(handler_input) 240 | 241 | def handle(self, handler_input): 242 | # type: (HandlerInput) -> Response 243 | logger.info("In LaunchRequestHandler") 244 | 245 | in_skill_response = in_skill_product_response(handler_input) 246 | if isinstance(in_skill_response, InSkillProductsResponse): 247 | entitled_prods = get_all_entitled_products(in_skill_response.in_skill_products) 248 | if entitled_prods: 249 | speech = ( 250 | "Welcome to {}. You currently own {} products. " 251 | "To hear a random fact, you could say, 'Tell me a fact', " 252 | "or you can ask for a specific category you have " 253 | "purchased, for example, say 'Tell me a science fact'. " 254 | "To know what else you can buy, say, 'What can i buy?'. " 255 | "So, what can I help you with?").format( 256 | skill_name, 257 | get_speakable_list_of_products(entitled_prods)) 258 | else: 259 | logger.info("No entitled products") 260 | speech = ( 261 | "Welcome to {}. To hear a random fact you can say " 262 | "'Tell me a fact', or to hear about the premium categories " 263 | "for purchase, say 'What can I buy'. For help, say , " 264 | "'Help me'... So, what can I help you with?" 265 | ).format(skill_name) 266 | reprompt = "I didn't catch that. What can I help you with?" 267 | else: 268 | logger.info("Error calling InSkillProducts API: {}".format( 269 | in_skill_response.message)) 270 | speech = "Something went wrong in loading your purchase history." 271 | reprompt = speech 272 | 273 | return handler_input.response_builder.speak(speech).ask( 274 | reprompt).response 275 | 276 | class GetFactHandler(AbstractRequestHandler): 277 | """Handler for returning random fact to the user.""" 278 | def can_handle(self, handler_input): 279 | # type: (HandlerInput) -> bool 280 | return is_intent_name("GetFactIntent")(handler_input) 281 | 282 | def handle(self, handler_input): 283 | # type: (HandlerInput) -> Response 284 | logger.info("In GetFactHandler") 285 | 286 | fact_text = get_random_from_list(all_facts) 287 | return handler_input.response_builder.speak( 288 | "Here's your random fact: {} {}".format( 289 | fact_text, get_random_yes_no_question())).ask( 290 | get_random_yes_no_question()).response 291 | 292 | class YesHandler(AbstractRequestHandler): 293 | """If the user says Yes, they want another fact.""" 294 | def can_handle(self, handler_input): 295 | # type: (HandlerInput) -> bool 296 | return is_intent_name("AMAZON.YesIntent")(handler_input) 297 | 298 | def handle(self, handler_input): 299 | # type: (HandlerInput) -> Response 300 | logger.info("In YesHandler") 301 | return GetFactHandler().handle(handler_input) 302 | 303 | 304 | class NoHandler(AbstractRequestHandler): 305 | """If the user says No, then the skill should be exited.""" 306 | def can_handle(self, handler_input): 307 | # type: (HandlerInput) -> bool 308 | return is_intent_name("AMAZON.NoIntent")(handler_input) 309 | 310 | def handle(self, handler_input): 311 | # type: (HandlerInput) -> Response 312 | logger.info("In NoHandler") 313 | 314 | return handler_input.response_builder.speak( 315 | get_random_goodbye()).set_should_end_session(True).response 316 | 317 | class GetCategoryFactHandler(AbstractRequestHandler): 318 | """Handler for providing category specific facts to the user. 319 | 320 | The handler provides a random fact specific to the category provided 321 | by the user. If the user doesn't own the category, a specific message 322 | to upsell the category is provided. If there is no such category, 323 | then a custom message to choose valid categories is provided, rather 324 | than throwing an error. 325 | """ 326 | def can_handle(self, handler_input): 327 | # type: (HandlerInput) -> bool 328 | return is_intent_name("GetCategoryFactIntent")(handler_input) 329 | 330 | def handle(self, handler_input): 331 | # type: (HandlerInput) -> Response 332 | logger.info("In GetCategoryFactHandler") 333 | 334 | fact_category = get_resolved_value( 335 | handler_input.request_envelope.request, 'factCategory') 336 | logger.info("FACT CATEGORY = {}".format(fact_category)) 337 | 338 | if fact_category is not None: 339 | # If there was an entity resolution match for this slot value 340 | category_facts = [ 341 | l for l in all_facts if l.get("type") == fact_category] 342 | else: 343 | # If there was not an entity resolution match for this slot value 344 | category_facts = [] 345 | 346 | if not category_facts: 347 | slot_value = get_spoken_value( 348 | handler_input.request_envelope.request, "factCategory") 349 | if slot_value is not None: 350 | speak_prefix = "I heard you say {}.".format(slot_value) 351 | else: 352 | speak_prefix = "" 353 | speech = ( 354 | "{} I don't have facts for that category. You can ask for " 355 | "science, space or history facts. Which one would you " 356 | "like?".format(speak_prefix)) 357 | reprompt = ( 358 | "Which fact category would you like? I have science, space, " 359 | "or history.") 360 | return handler_input.response_builder.speak(speech).ask( 361 | reprompt).response 362 | else: 363 | in_skill_response = in_skill_product_response(handler_input) 364 | if in_skill_response: 365 | subscription = [ 366 | l for l in in_skill_response.in_skill_products 367 | if l.reference_name == "all_access"] 368 | category_product = [ 369 | l for l in in_skill_response.in_skill_products 370 | if l.reference_name == "{}_pack".format(fact_category)] 371 | 372 | if is_entitled(subscription) or is_entitled(category_product): 373 | speech = "Here's your {} fact: {} {}".format( 374 | fact_category, get_random_from_list(category_facts), 375 | get_random_yes_no_question()) 376 | reprompt = get_random_yes_no_question() 377 | return handler_input.response_builder.speak(speech).ask( 378 | reprompt).response 379 | else: 380 | upsell_msg = ( 381 | "You don't currently own the {} pack. {} " 382 | "Want to learn more?").format( 383 | fact_category, category_product[0].summary) 384 | return handler_input.response_builder.add_directive( 385 | SendRequestDirective( 386 | name="Upsell", 387 | payload={ 388 | "InSkillProduct": { 389 | "productId": category_product[0].product_id, 390 | }, 391 | "upsellMessage": upsell_msg, 392 | }, 393 | token="correlationToken") 394 | ).response 395 | 396 | 397 | class ShoppingHandler(AbstractRequestHandler): 398 | """ 399 | Following handler demonstrates how skills can handle user requests to 400 | discover what products are available for purchase in-skill. 401 | User says: Alexa, ask Premium facts what can I buy. 402 | """ 403 | def can_handle(self, handler_input): 404 | # type: (HandlerInput) -> bool 405 | return is_intent_name("ShoppingIntent")(handler_input) 406 | 407 | def handle(self, handler_input): 408 | # type: (HandlerInput) -> Response 409 | logger.info("In ShoppingHandler") 410 | 411 | # Inform the user about what products are available for purchase 412 | in_skill_response = in_skill_product_response(handler_input) 413 | if in_skill_response: 414 | purchasable = [l for l in in_skill_response.in_skill_products 415 | if l.entitled == EntitledState.NOT_ENTITLED and 416 | l.purchasable == PurchasableState.PURCHASABLE] 417 | 418 | if purchasable: 419 | speech = ("Products available for purchase at this time are {}. " 420 | "To learn more about a product, say 'Tell me more " 421 | "about' followed by the product name. If you are ready " 422 | "to buy say 'Buy' followed by the product name. So what " 423 | "can I help you with?").format( 424 | get_speakable_list_of_products(purchasable)) 425 | else: 426 | speech = ("There are no more products to buy. To hear a " 427 | "random fact, you could say, 'Tell me a fact', or " 428 | "you can ask for a specific category you have " 429 | "purchased, for example, say 'Tell me a science " 430 | "fact'. So what can I help you with?") 431 | reprompt = "I didn't catch that. What can I help you with?" 432 | return handler_input.response_builder.speak(speech).ask( 433 | reprompt).response 434 | 435 | 436 | class ProductDetailHandler(AbstractRequestHandler): 437 | """Handler for providing product detail to the user before buying. 438 | 439 | Resolve the product category and provide the user with the 440 | corresponding product detail message. 441 | User says: Alexa, tell me about pack 442 | """ 443 | def can_handle(self, handler_input): 444 | # type: (HandlerInput) -> bool 445 | return is_intent_name("ProductDetailIntent")(handler_input) 446 | 447 | def handle(self, handler_input): 448 | # type: (HandlerInput) -> Response 449 | logger.info("In ProductDetailHandler") 450 | in_skill_response = in_skill_product_response(handler_input) 451 | 452 | if in_skill_response: 453 | product_category = get_resolved_value( 454 | handler_input.request_envelope.request, "productCategory") 455 | all_access = get_resolved_value( 456 | handler_input.request_envelope.request, "allAccess") 457 | 458 | if all_access is not None: 459 | product_category = "all_access" 460 | 461 | # No entity resolution match 462 | if product_category is None: 463 | speech = ("I don't think we have a product by that name. " 464 | "Can you try again?") 465 | reprompt = "I didn't catch that. Can you try again?" 466 | return handler_input.response_builder.speak(speech).ask( 467 | reprompt).response 468 | else: 469 | if product_category != "all_access": 470 | product_category += "_pack" 471 | 472 | product = [l for l in in_skill_response.in_skill_products 473 | if l.reference_name == product_category] 474 | if is_product(product): 475 | speech = ("{}. To buy it, say Buy {}".format( 476 | product[0].summary, product[0].name)) 477 | reprompt = ( 478 | "I didn't catch that. To buy {}, say Buy {}".format( 479 | product[0].name, product[0].name)) 480 | else: 481 | speech = ("I don't think we have a product by that name. " 482 | "Can you try again?") 483 | reprompt = "I didn't catch that. Can you try again?" 484 | 485 | return handler_input.response_builder.speak(speech).ask( 486 | reprompt).response 487 | 488 | class BuyHandler(AbstractRequestHandler): 489 | """Handler for letting users buy the product. 490 | 491 | User says: Alexa, buy . 492 | """ 493 | def can_handle(self, handler_input): 494 | # type: (HandlerInput) -> bool 495 | return is_intent_name("BuyIntent")(handler_input) 496 | 497 | def handle(self, handler_input): 498 | # type: (HandlerInput) -> Response 499 | logger.info("In BuyHandler") 500 | 501 | # Inform the user about what products are available for purchase 502 | in_skill_response = in_skill_product_response(handler_input) 503 | if in_skill_response: 504 | product_category = get_resolved_value( 505 | handler_input.request_envelope.request, "productCategory") 506 | 507 | # No entity resolution match 508 | if product_category is None: 509 | product_category = "all_access" 510 | else: 511 | product_category += "_pack" 512 | 513 | product = [l for l in in_skill_response.in_skill_products 514 | if l.reference_name == product_category] 515 | return handler_input.response_builder.add_directive( 516 | SendRequestDirective( 517 | name="Buy", 518 | payload={ 519 | "InSkillProduct": { 520 | "productId": product[0].product_id 521 | } 522 | }, 523 | token="correlationToken") 524 | ).response 525 | 526 | class CancelSubscriptionHandler(AbstractRequestHandler): 527 | """ 528 | Following handler demonstrates how Skills would receive Cancel requests 529 | from customers and then trigger a cancel request to Alexa 530 | User says: Alexa, ask premium facts to cancel 531 | """ 532 | def can_handle(self, handler_input): 533 | # type: (HandlerInput) -> bool 534 | return is_intent_name("CancelSubscriptionIntent")(handler_input) 535 | 536 | def handle(self, handler_input): 537 | # type: (HandlerInput) -> Response 538 | logger.info("In CancelSubscriptionHandler") 539 | 540 | in_skill_response = in_skill_product_response(handler_input) 541 | if in_skill_response: 542 | product_category = get_resolved_value( 543 | handler_input.request_envelope.request, "productCategory") 544 | 545 | # No entity resolution match 546 | if product_category is None: 547 | product_category = "all_access" 548 | else: 549 | product_category += "_pack" 550 | 551 | product = [l for l in in_skill_response.in_skill_products 552 | if l.reference_name == product_category] 553 | return handler_input.response_builder.add_directive( 554 | SendRequestDirective( 555 | name="Cancel", 556 | payload={ 557 | "InSkillProduct": { 558 | "productId": product[0].product_id 559 | } 560 | }, 561 | token="correlationToken") 562 | ).response 563 | 564 | class BuyResponseHandler(AbstractRequestHandler): 565 | """This handles the Connections.Response event after a buy occurs.""" 566 | def can_handle(self, handler_input): 567 | # type: (HandlerInput) -> bool 568 | return (is_request_type("Connections.Response")(handler_input) and 569 | handler_input.request_envelope.request.name == "Buy") 570 | 571 | def handle(self, handler_input): 572 | # type: (HandlerInput) -> Response 573 | logger.info("In BuyResponseHandler") 574 | in_skill_response = in_skill_product_response(handler_input) 575 | product_id = handler_input.request_envelope.request.payload.get( 576 | "productId") 577 | 578 | if in_skill_response: 579 | product = [l for l in in_skill_response.in_skill_products 580 | if l.product_id == product_id] 581 | logger.info("Product = {}".format(str(product))) 582 | if handler_input.request_envelope.request.status.code == "200": 583 | speech = None 584 | reprompt = None 585 | purchase_result = handler_input.request_envelope.request.payload.get( 586 | "purchaseResult") 587 | if purchase_result == PurchaseResult.ACCEPTED.value: 588 | category_facts = all_facts 589 | if product[0].reference_name != "all_access": 590 | category_facts = [l for l in all_facts if 591 | l.get("type") == 592 | product[0].reference_name.replace( 593 | "_pack", "")] 594 | speech = ("You have unlocked the {}. Here is your {} " 595 | "fact: {} {}").format( 596 | product[0].name, 597 | product[0].reference_name.replace( 598 | "_pack", "").replace("all_access", ""), 599 | get_random_from_list(category_facts), 600 | get_random_yes_no_question()) 601 | reprompt = get_random_yes_no_question() 602 | elif purchase_result in ( 603 | PurchaseResult.DECLINED.value, 604 | PurchaseResult.ERROR.value, 605 | PurchaseResult.NOT_ENTITLED.value): 606 | speech = ("Thanks for your interest in {}. " 607 | "Would you like another random fact?".format( 608 | product[0].name)) 609 | reprompt = "Would you like another random fact?" 610 | elif purchase_result == PurchaseResult.ALREADY_PURCHASED.value: 611 | logger.info("Already purchased product") 612 | speech = " Do you want to hear a fact?" 613 | reprompt = "What can I help you with?" 614 | else: 615 | # Invalid purchase result value 616 | logger.info("Purchase result: {}".format(purchase_result)) 617 | return FallbackIntentHandler().handle(handler_input) 618 | 619 | return handler_input.response_builder.speak(speech).ask( 620 | reprompt).response 621 | else: 622 | logger.log("Connections.Response indicated failure. " 623 | "Error: {}".format( 624 | handler_input.request_envelope.request.status.message)) 625 | 626 | return handler_input.response_builder.speak( 627 | "There was an error handling your purchase request. " 628 | "Please try again or contact us for help").response 629 | 630 | class CancelResponseHandler(AbstractRequestHandler): 631 | """This handles the Connections.Response event after a cancel occurs.""" 632 | def can_handle(self, handler_input): 633 | # type: (HandlerInput) -> bool 634 | return (is_request_type("Connections.Response")(handler_input) and 635 | handler_input.request_envelope.request.name == "Cancel") 636 | 637 | def handle(self, handler_input): 638 | # type: (HandlerInput) -> Response 639 | logger.info("In CancelResponseHandler") 640 | in_skill_response = in_skill_product_response(handler_input) 641 | product_id = handler_input.request_envelope.request.payload.get( 642 | "productId") 643 | 644 | if in_skill_response: 645 | product = [l for l in in_skill_response.in_skill_products 646 | if l.product_id == product_id] 647 | logger.info("Product = {}".format(str(product))) 648 | if handler_input.request_envelope.request.status.code == "200": 649 | speech = None 650 | reprompt = None 651 | purchase_result = handler_input.request_envelope.request.payload.get( 652 | "purchaseResult") 653 | purchasable = product[0].purchasable 654 | if purchase_result == PurchaseResult.ACCEPTED.value: 655 | speech = ("You have successfully cancelled your " 656 | "subscription. {}".format( 657 | get_random_yes_no_question())) 658 | reprompt = get_random_yes_no_question() 659 | 660 | if purchase_result == PurchaseResult.DECLINED.value: 661 | if purchasable == PurchasableState.PURCHASABLE: 662 | speech = ("You don't currently have a " 663 | "subscription. {}".format( 664 | get_random_yes_no_question())) 665 | else: 666 | speech = get_random_yes_no_question() 667 | reprompt = get_random_yes_no_question() 668 | 669 | return handler_input.response_builder.speak(speech).ask( 670 | reprompt).response 671 | else: 672 | logger.log("Connections.Response indicated failure. " 673 | "Error: {}".format( 674 | handler_input.request_envelope.request.status.message)) 675 | 676 | return handler_input.response_builder.speak( 677 | "There was an error handling your cancellation " 678 | "request. Please try again or contact us for " 679 | "help").response 680 | 681 | class UpsellResponseHandler(AbstractRequestHandler): 682 | """This handles the Connections.Response event after an upsell occurs.""" 683 | def can_handle(self, handler_input): 684 | # type: (HandlerInput) -> bool 685 | return (is_request_type("Connections.Response")(handler_input) and 686 | handler_input.request_envelope.request.name == "Upsell") 687 | 688 | def handle(self, handler_input): 689 | # type: (HandlerInput) -> Response 690 | logger.info("In UpsellResponseHandler") 691 | 692 | if handler_input.request_envelope.request.status.code == "200": 693 | if handler_input.request_envelope.request.payload.get( 694 | "purchaseResult") == PurchaseResult.DECLINED.value: 695 | speech = ("Ok. Here's a random fact: {} {}".format( 696 | get_random_from_list(all_facts), 697 | get_random_yes_no_question())) 698 | reprompt = get_random_yes_no_question() 699 | return handler_input.response_builder.speak(speech).ask( 700 | reprompt).response 701 | else: 702 | logger.log("Connections.Response indicated failure. " 703 | "Error: {}".format( 704 | handler_input.request_envelope.request.status.message)) 705 | return handler_input.response_builder.speak( 706 | "There was an error handling your Upsell request. " 707 | "Please try again or contact us for help.").response 708 | 709 | class HelpIntentHandler(AbstractRequestHandler): 710 | """Handler for help message to users.""" 711 | def can_handle(self, handler_input): 712 | return is_intent_name("AMAZON.HelpIntent")(handler_input) 713 | 714 | def handle(self, handler_input): 715 | # type: (HandlerInput) -> Response 716 | logger.info("In HelpIntentHandler") 717 | in_skill_response = in_skill_product_response(handler_input) 718 | 719 | if isinstance(in_skill_response, InSkillProductsResponse): 720 | speech = ( 721 | "To hear a random fact you can say " 722 | "'Tell me a fact', or to hear about the premium categories " 723 | "for purchase, say 'What can I buy'. For help, say , " 724 | "'Help me'... So, what can I help you with?" 725 | ) 726 | reprompt = "I didn't catch that. What can I help you with?" 727 | else: 728 | logger.info("Error calling InSkillProducts API: {}".format( 729 | in_skill_response.message)) 730 | speech = "Something went wrong in loading your purchase history." 731 | reprompt = speech 732 | 733 | return handler_input.response_builder.speak(speech).ask( 734 | reprompt).response 735 | 736 | 737 | class FallbackIntentHandler(AbstractRequestHandler): 738 | """Handler for fallback intent. 739 | 740 | 2018-July-12: AMAZON.FallbackIntent is currently available in all 741 | English locales. This handler will not be triggered except in that 742 | locale, so it can be safely deployed for any locale. More info 743 | on the fallback intent can be found here: https://developer.amazon.com/docs/custom-skills/standard-built-in-intents.html#fallback 744 | """ 745 | def can_handle(self, handler_input): 746 | return is_intent_name("AMAZON.FallbackIntent")(handler_input) 747 | 748 | def handle(self, handler_input): 749 | # type: (HandlerInput) -> Response 750 | logger.info("In FallbackIntentHandler") 751 | speech = ( 752 | "Sorry. I cannot help with that. I can help you with " 753 | "some facts. " 754 | "To hear a random fact you can say " 755 | "'Tell me a fact', or to hear about the premium categories " 756 | "for purchase, say 'What can I buy'. For help, say , " 757 | "'Help me'... So, what can I help you with?" 758 | ) 759 | reprompt = "I didn't catch that. What can I help you with?" 760 | 761 | return handler_input.response_builder.speak(speech).ask( 762 | reprompt).response 763 | 764 | 765 | class SessionEndedHandler(AbstractRequestHandler): 766 | """Handler for session end request, stop or cancel intents.""" 767 | def can_handle(self, handler_input): 768 | # type: (HandlerInput) -> bool 769 | return (is_request_type("SessionEndedRequest")(handler_input) or 770 | is_intent_name("AMAZON.StopIntent")(handler_input) or 771 | is_intent_name("AMAZON.CancelIntent")(handler_input)) 772 | 773 | def handle(self, handler_input): 774 | # type: (HandlerInput) -> Response 775 | logger.info("In SessionEndedHandler") 776 | return handler_input.response_builder.speak( 777 | get_random_goodbye()).set_should_end_session(True).response 778 | 779 | # Skill Exception Handler 780 | class CatchAllExceptionHandler(AbstractExceptionHandler): 781 | """One exception handler to catch all exceptions.""" 782 | def can_handle(self, handler_input, exception): 783 | # type: (HandlerInput, Exception) -> bool 784 | return True 785 | 786 | def handle(self, handler_input, exception): 787 | # type: (HandlerInput, Exception) -> Response 788 | logger.error(exception, exc_info=True) 789 | 790 | speech = "Sorry, I can't understand the command. Please try again!!" 791 | handler_input.response_builder.speak(speech).ask(speech) 792 | 793 | return handler_input.response_builder.response 794 | 795 | # Request and Response Loggers 796 | class RequestLogger(AbstractRequestInterceptor): 797 | """Log the request envelope.""" 798 | def process(self, handler_input): 799 | # type: (HandlerInput) -> None 800 | logger.info("Request Envelope: {}".format( 801 | handler_input.request_envelope)) 802 | 803 | class ResponseLogger(AbstractResponseInterceptor): 804 | """Log the response envelope.""" 805 | def process(self, handler_input, response): 806 | # type: (HandlerInput, Response) -> None 807 | logger.info("Response: {}".format(response)) 808 | 809 | 810 | sb = StandardSkillBuilder() 811 | 812 | sb.add_request_handler(LaunchRequestHandler()) 813 | sb.add_request_handler(GetFactHandler()) 814 | sb.add_request_handler(YesHandler()) 815 | sb.add_request_handler(NoHandler()) 816 | sb.add_request_handler(GetCategoryFactHandler()) 817 | sb.add_request_handler(BuyResponseHandler()) 818 | sb.add_request_handler(CancelResponseHandler()) 819 | sb.add_request_handler(UpsellResponseHandler()) 820 | sb.add_request_handler(ShoppingHandler()) 821 | sb.add_request_handler(ProductDetailHandler()) 822 | sb.add_request_handler(BuyHandler()) 823 | sb.add_request_handler(CancelSubscriptionHandler()) 824 | sb.add_request_handler(HelpIntentHandler()) 825 | sb.add_request_handler(FallbackIntentHandler()) 826 | sb.add_request_handler(SessionEndedHandler()) 827 | 828 | sb.add_exception_handler(CatchAllExceptionHandler()) 829 | sb.add_global_request_interceptor(RequestLogger()) 830 | sb.add_global_response_interceptor(ResponseLogger()) 831 | 832 | lambda_handler = sb.lambda_handler() 833 | --------------------------------------------------------------------------------