[アップデート]Alexa-hostedスキルでPythonが利用できるようになりました

Alexa開発者コンソールにてスキルの作成、編集、公開が完結できる「Alexa-hostedスキル」Python が利用できるようになりました!

Alexa-hosted スキルとは

「Alexa-hostedスキル」(以下、hostedスキル)は、AlexaがホストするバックエンドのAWSリソースを利用して作成するAlexaスキルのことです。
開発者側でAWSアカウントを用意することなく、スキルの開発を行うことができます。

開発者コンソール上にてすべての作業が完結できることで、開発からリリースまで素早く行うことが可能です。

なお、hostedスキルでAWSリソースを利用するにあたっては、以下の利用制限があります。

Alexa-hostedスキルを使用してスキルをエンドツーエンドで作成するより引用)

AWSの無料利用枠内に制限されています。アカウントごとに次の制限が適用されます。
AWS Lambda: 月あたり、100万件のAWS Lambdaリクエスト、320万秒のコンピューティング時間を利用できます。
Amazon S3: 月あたり、5 GBのAmazon S3ストレージ、20,000件のGETリクエスト、2,000件のPUTリクエスト、15 GBのデータ転送を利用できます。
AWS CodeCommit: 月あたり、50 GBのストレージ、10,000件のGitリクエストを利用できます。

Pythonによるhostedスキル作成

これまで、hostedスキルでは Node.js でのみスキルの開発が可能でしたが、この度 Python でも開発が可能となりました!

それでは、早速hostedスキルを作っていきましょう。

Alexa Skills Kit開発者コンソールから スキルの作成 を選択。

適当なスキル名を入力して、 カスタム - Alexa-Hosted (Python) 、を選択します。

スキルを作成 をクリックしてhostedスキルを作成します。

しばらくすると、hostedスキルが作成されました。

コードエディタ タブをクリックして、作成されたスキルのテンプレートコードを見てみます。

コードは以下の3ファイルで構成されています。

  • lambda_function.py (リクエストハンドラなどが記述されたメインファイル)
  • utils.py (ユーティリティ関数が記述されたファイル)
  • requirements.txt (パッケージ管理設定ファイル)

lambda_function.py を見ると、デコレーターを利用した実装ではなく、ハンドラークラスを使用した実装になっています。

Pythonのhostedスキルは pip でパッケージ管理しているようなので、外部パッケージを利用したい場合は、requirements.txt に追加することでデプロイ時によしなにしてくれます。 楽チンですね!

スキルテンプレートの修正

スキル作成時、スキルのインテントおよびコード上の発話が英語になっているので、日本語に直してみます。

HelloWorldIntent を選択し、以下のように修正します。

または、JSONエディターより以下のコードをコピペしてモデルを保存します。

{
    "interactionModel": {
        "languageModel": {
            "invocationName": "ホステッドパイソン",
            "intents": [
                {
                    "name": "AMAZON.CancelIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.HelpIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.StopIntent",
                    "samples": []
                },
                {
                    "name": "HelloWorldIntent",
                    "slots": [],
                    "samples": [
                        "ハローワールド",
                        "調子はどう",
                        "やあ",
                        "どうも",
                        "こんちわ",
                        "こんにちは"
                    ]
                },
                {
                    "name": "AMAZON.NavigateHomeIntent",
                    "samples": []
                }
            ],
            "types": []
        }
    }
}

保存後、モデルをビルドします。

次に、バックエンド側のコードを修正します。
コードエディタを開いて、 lambda_function.pyを以下のように修正して保存し、デプロイをクリックします。

# -*- coding: utf-8 -*-

import logging
import ask_sdk_core.utils as ask_utils

from ask_sdk_core.skill_builder import SkillBuilder
from ask_sdk_core.dispatch_components import AbstractRequestHandler
from ask_sdk_core.dispatch_components import AbstractExceptionHandler
from ask_sdk_core.handler_input import HandlerInput

from ask_sdk_model import Response

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)


class LaunchRequestHandler(AbstractRequestHandler):
    """Handler for Skill Launch."""
    def can_handle(self, handler_input):
        # type: (HandlerInput) -> bool

        return ask_utils.is_request_type("LaunchRequest")(handler_input)

    def handle(self, handler_input):
        # type: (HandlerInput) -> Response
        speak_output = "ようこそ。こんにちは、またはヘルプと言ってみてください。"

        return (
            handler_input.response_builder
                .speak(speak_output)
                .ask(speak_output)
                .response
        )


class HelloWorldIntentHandler(AbstractRequestHandler):
    """Handler for Hello World Intent."""
    def can_handle(self, handler_input):
        # type: (HandlerInput) -> bool
        return ask_utils.is_intent_name("HelloWorldIntent")(handler_input)

    def handle(self, handler_input):
        # type: (HandlerInput) -> Response
        speak_output = "ハローワールド!"

        return (
            handler_input.response_builder
                .speak(speak_output)
                # .ask("add a reprompt if you want to keep the session open for the user to respond")
                .response
        )


class HelpIntentHandler(AbstractRequestHandler):
    """Handler for Help Intent."""
    def can_handle(self, handler_input):
        # type: (HandlerInput) -> bool
        return ask_utils.is_intent_name("AMAZON.HelpIntent")(handler_input)

    def handle(self, handler_input):
        # type: (HandlerInput) -> Response
        speak_output = "私に向かってこんにちはと言ってみてください!"

        return (
            handler_input.response_builder
                .speak(speak_output)
                .ask(speak_output)
                .response
        )


class CancelOrStopIntentHandler(AbstractRequestHandler):
    """Single handler for Cancel and Stop Intent."""
    def can_handle(self, handler_input):
        # type: (HandlerInput) -> bool
        return (ask_utils.is_intent_name("AMAZON.CancelIntent")(handler_input) or
                ask_utils.is_intent_name("AMAZON.StopIntent")(handler_input))

    def handle(self, handler_input):
        # type: (HandlerInput) -> Response
        speak_output = "さようなら!"

        return (
            handler_input.response_builder
                .speak(speak_output)
                .response
        )


class SessionEndedRequestHandler(AbstractRequestHandler):
    """Handler for Session End."""
    def can_handle(self, handler_input):
        # type: (HandlerInput) -> bool
        return ask_utils.is_request_type("SessionEndedRequest")(handler_input)

    def handle(self, handler_input):
        # type: (HandlerInput) -> Response

        # Any cleanup logic goes here.

        return handler_input.response_builder.response


class IntentReflectorHandler(AbstractRequestHandler):
    """The intent reflector is used for interaction model testing and debugging.
    It will simply repeat the intent the user said. You can create custom handlers
    for your intents by defining them above, then also adding them to the request
    handler chain below.
    """
    def can_handle(self, handler_input):
        # type: (HandlerInput) -> bool
        return ask_utils.is_request_type("IntentRequest")(handler_input)

    def handle(self, handler_input):
        # type: (HandlerInput) -> Response
        intent_name = ask_utils.get_intent_name(handler_input)
        speak_output = "You just triggered " + intent_name + "."

        return (
            handler_input.response_builder
                .speak(speak_output)
                # .ask("add a reprompt if you want to keep the session open for the user to respond")
                .response
        )


class CatchAllExceptionHandler(AbstractExceptionHandler):
    """Generic error handling to capture any syntax or routing errors. If you receive an error
    stating the request handler chain is not found, you have not implemented a handler for
    the intent being invoked or included it in the skill builder below.
    """
    def can_handle(self, handler_input, exception):
        # type: (HandlerInput, Exception) -> bool
        return True

    def handle(self, handler_input, exception):
        # type: (HandlerInput, Exception) -> Response
        logger.error(exception, exc_info=True)

        speak_output = "ごめんなさい。なんだかうまくいかないみたいです。もう一度お願いします"

        return (
            handler_input.response_builder
                .speak(speak_output)
                .ask(speak_output)
                .response
        )

# The SkillBuilder object acts as the entry point for your skill, routing all request and response
# payloads to the handlers above. Make sure any new handlers or interceptors you've
# defined are included below. The order matters - they're processed top to bottom.


sb = SkillBuilder()

sb.add_request_handler(LaunchRequestHandler())
sb.add_request_handler(HelloWorldIntentHandler())
sb.add_request_handler(HelpIntentHandler())
sb.add_request_handler(CancelOrStopIntentHandler())
sb.add_request_handler(SessionEndedRequestHandler())
sb.add_request_handler(IntentReflectorHandler()) # make sure IntentReflectorHandler is last so it doesn't override your custom intent handlers

sb.add_exception_handler(CatchAllExceptionHandler())

lambda_handler = sb.lambda_handler()

以上で最低限動作するhostedスキルを準備することができました。
開発者コンソールのテストやEchoを使って動作を確認してみましょう!

hostedスキルにおけるPythonバージョン

さて、hostedスキルではどのPythonのバージョンが動作しているのでしょうか?
以下のドキュメントを見ると、Pythonは 3.7 がサポートされているようです。

Build a Skill End-to-end Using an Alexa-hosted Skillより引用)

Alexa-hosted skills currently supports Node.js version 8.10 and Python version 3.7. When you create a skill from the Console or CLI, you will have the opportunity to select a runtime at skill creation. Once you choose a runtime for a specific skill, it cannot be changed.

せっかくなので、動作しているPythonのバージョンをAlexa自身に喋らせてみましょう。
lambda_function.py を以下のように修正します。
(LaunchRequestHandlerクラス以外の記述は省略しています)

# -*- coding: utf-8 -*-

import logging
import platform
import ask_sdk_core.utils as ask_utils

from ask_sdk_core.skill_builder import SkillBuilder
from ask_sdk_core.dispatch_components import AbstractRequestHandler
from ask_sdk_core.dispatch_components import AbstractExceptionHandler
from ask_sdk_core.handler_input import HandlerInput

from ask_sdk_model import Response

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)


class LaunchRequestHandler(AbstractRequestHandler):
    """Handler for Skill Launch."""
    def can_handle(self, handler_input):
        # type: (HandlerInput) -> bool

        return ask_utils.is_request_type("LaunchRequest")(handler_input)

    def handle(self, handler_input):
        # type: (HandlerInput) -> Response
        python_version = platform.python_version()
        speak_output = "こんにちは。私はPython" + python_version + "のおかげで喋ることができました。ごきげんよう"

        return (
            handler_input.response_builder
                .speak(speak_output)
                .ask(speak_output)
                .response
        )

結果は以下です。

2019/9/18現在、hostedスキル(Python)は Python 3.7.4 で動作しているようですね。

おわりに

Alexa-hostedスキルでPythonが利用できるようになったので、早速触ってみました。
hostedスキルはこれまでNode.jsのみサポートされていましたが、今回Pythonがサポートされたことで、よりスキル開発の間口が広くなったのではないでしょうか。

普段Pythonを書かれていてAlexaのスキル開発に興味がある方は、これを機にぜひチャレンジしてみてください。

参考