PythonではじめるAlexaスキル開発

# HelloAlexa!（読み飛ばしOK）
2022年のブラックフライデー、私はAlexaEchoShow5（以下、Alexa）をSwitchBotとともに購入し、生活が激変した。

タッチパネルに触れずとも、声だけで電気を点けたり、テレビのチャンネルを変更できる。これは私にとって大変目新しく非常に刺激的な体験だった。当初、声で端末を操作するなんて恥ずかしいと考えていたが、今では家電を声で操作する便利さにすっかり慣れ、「Alexaなくして我が生活なし」といった感じになっている。ところが所詮私も人間、慣れるとどんどんマンネリ化していき「最近、うちのAlexaは電気を点けたり消したりすることに終始していて、何か物足りないな」と感じるようになった。さて、この物足りなさを解消するにはどうすればよいのか？

解消のカギを握るのが今回紹介するAlexaスキルだろう。以前から「Alexaスキルの開発ができるとAlexaを使っていろいろできるようになるぞ（ざっくり）」と小耳にはさんでいたので、その存在自体は知っていた。しかし、ほとんどPythonしか触らないひよっこ日曜エンジニアの私には、何から手を付けてよいかわからず、右往左往するしかなかったため、なかなか取り組めずにいたのだが、今回、ついにその重い腰を上げてAlexaとのマンネリ生活にサヨナラを告げることにした（？）。そしてこの際、開発記録を公開し、迷える子羊たちにAlexaスキル開発のいろいろを共有したいと思う。本記事はその開発記録をまとめたものである。内容としてはAlexaスキルの基本的な開発方法に加え、開発過程で私がつまずいたポイントを全て記載している。この記事がAlexaとのマンネリ生活に悩む日曜エンジニアの一助となることを願ってやまない。（2024/2/24）

# はじめに
本記事ではAlexaスキルの基本的な開発方法および、開発過程で私がつまずいたポイントについて記載する。タイトルにもある通りPythonを使ってAlexaスキル開発に取り組むので、もし、「Pythonの書き方がわからない！」といった方は、まず、ほかの記事で勉強をされてから本記事をご覧いただければと思う。

**本記事の目的は、自作のAlexaスキルがあなたのAlexaデバイスで実行できるようになること**である。この記事の構成としては、まずAlexaスキルの開発手順について説明したのち、基礎的なアプリケーションであるHelloAlexaスキルの開発方法について説明する。最後に、Alexaスキルの応用として外部APIから気象情報を取得する機能を実装する。開発手順をすでに知っているという人はHelloAlexaスキル開発の章から見始めてもよいし、Alexaスキルの開発をすでに知っているという方は、応用の機能実装のみを見るのもよいと思う。各々のレベルに応じて本記事を閲覧していただけると嬉しい。早速、はじめよう。

# 開発手順
さて、まずはAlexaスキルはどういった手順で開発していくかを説明する。以下のフロー図を見てほしい。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/862467/dd8bbffd-7d57-c288-3dac-de365fc4ea86.png)
最初にやることはAlexa developer consoleにログインすることだ。
詳細は次章で説明する。次に、Alexaスキルの初期設定を行い、内部処理のコーディングを行う。その後、無事にビルドおよびテストが終わると、Alexaスキルの認証が行われ、認証を通過すると晴れて自作Alexaスキルが日本ないしは世界に公開されるというのが一連の流れである。ただし、本記事の目的は、自作のAlexaスキルがあなたのAlexaデバイスで実行できるようになることである。その場合、Alexaスキルを日本ないし世界に公開する必要はない。そのため、今回はAlexaスキルの公開方法について解説は行わないので注意してほしい。
ここからは実際にスキルを開発してAlexaとの素晴らしい生活の幕開けを感じてもらうことにしよう。

# HelloAlexaスキルの開発
HelloAlexaスキルは、ユーザが「ハロー」や「こんにちは」とAlexaに呼びかけると「Hello World!」と返してくれる単純なアプリケーションである。HelloAlexaスキルを開発することでAlexaスキルの基礎が身につき、手っ取り早く応用が可能になると思う。考えていても始まらない、ものは試しということで早速作っていこう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/862467/69011e7d-f1f1-cd08-38c2-a0c2f122c55d.png)

まずは、<a href="https://developer.amazon.com/alexa/console/ask">Alexa developer console</a>にログインし、console画面にあるスキルの作成をクリックする。ログインにはAlexaの購入に利用したAmazonのアカウント情報を入力すればよい。ログインすると、画面にはスキルの作成と書かれた青いボタンがあるので、これをクリックするとAlexaスキルの設定に進む。ちなみにこの時点で「すべての文字が英語じゃあないか！」と困っている人がいるかもしれない。安心してほしい。画面の左下に言語選択の場所があるので、そこをクリックして日本語を選択すれば解決するはずだ。ログイン後の画像を以下に示す。
![2024-02-22_16h13_21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/862467/b05733e1-1704-0a3d-32fe-f32d64c841e7.png)
スキルの作成をクリックすると初期設定の画面が現れる。まずは名前、ロケールについて入力、選択する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/862467/b7c3068c-3806-81df-01a1-1d50d1ccb073.png)
エクスペリエンスについても適当なものを選択する。今回はその他を選択した。
![スクリーンショット 2024-02-22 151311.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/862467/a164e9b9-1168-f6ca-c051-fe623a48cf90.png)
モデルは４つ準備されており、今回はカスタムを選択した。スマート家電を操作するのであればスマートフォーム、EchoShowでの動画コンテンツを作るならビデオといったように、目的に沿って選択するのがよいだろう。
![スクリーンショット 2024-02-22 151322.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/862467/e8fa8b53-1f4a-ba42-cb9c-cfba89b3eaec.png)
ホスティングサービスとしてはPythonを選択。
![スクリーンショット 2024-02-22 151345.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/862467/4ae22c41-1f20-3063-d426-1e62e606eaa9.png)
ホスト地域は米国東部（バージニア北部）を選択。
![スクリーンショット 2024-02-22 151412.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/862467/c89adfb3-a65b-5d9e-0d30-456bc86b8c6d.png)
テンプレートについてはスクラッチで作成を選択。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/862467/8e5e508f-0ea4-1d84-c470-369566fbaa3a.png)
最後に、選択内容を確認してスキルを作成をクリック。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/862467/68a12b1d-eefc-c18b-2e33-658a1756f692.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/862467/4bd50e20-fd89-7b44-88ae-54b9afec7196.png)

すべての設定が終わるとコーディングやテストを実施する画面に遷移する。ここではまず、呼び出しという選択項目があるのでこれをクリックし、スキル立ち上げ時の名前を設定しておこう。**設定後の保存を忘れないようにすること。**
![スクリーンショット 2024-02-22 154257.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/862467/bfdacb81-5486-4349-d060-8e8fa2858593.png)
![スクリーンショット 2024-02-22 155104.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/862467/67fb7cfe-5774-776e-a4c4-8d56719584f2.png)

さて、問題はコーディングだ。ソースコードにはAlexa特有のモジュールやクラスがでてくるので相当ややこしい部分である。ぶっちゃけ一番挫折するところではないかと思う。だが安心してほしい。知らなくてもよい部分が大半で、私たちが知るべきはモジュールやクラスの挙動だけである。挙動については私が幾度となく試行錯誤した結果理解した内容を記載していくので、参考にしていただければと思う。まずは以下にソースコード全体を示す。
``` python3:lambda_function.py
# -*- coding: utf-8 -*-
# This sample demonstrates handling intents from an Alexa skill using the Alexa Skills Kit SDK for Python.
# Please visit https://alexa.design/cookbook for additional examples on implementing slots, dialog management,
# session persistence, api calls, and more.
# This sample is built using the handler classes approach in skill builder. 
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 = "Welcome, you can say Hello or Help. Which would you like to try?"

        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 = "Hello World!"

        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 = "You can say hello to me! How can I help?"

        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 = "Goodbye!"

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

class FallbackIntentHandler(AbstractRequestHandler):
    """Single handler for Fallback Intent."""
    def can_handle(self, handler_input):
        # type: (HandlerInput) -> bool
        return ask_utils.is_intent_name("AMAZON.FallbackIntent")(handler_input)

    def handle(self, handler_input):
        # type: (HandlerInput) -> Response
        logger.info("In FallbackIntentHandler")
        speech = "Hmm, I'm not sure. You can say Hello or Help. What would you like to do?"
        reprompt = "I didn't catch that. What can I help you with?"

        return handler_input.response_builder.speak(speech).ask(reprompt).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 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 = "Sorry, I had trouble doing what you asked. Please try again."

        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(FallbackIntentHandler())
sb.add_request_handler(SessionEndedRequestHandler())
sb.add_exception_handler(CatchAllExceptionHandler())
lambda_handler = sb.lambda_handler()

```
ここまでソースコードをみて混乱している人もいるだろう。しかし、上述の通りすべてを理解する必要はない。私たちが知っておくべきことはクラスやモジュールの挙動である。まずはAlexaスキルのソースコード特有のクラスについて説明する。

| クラス名   |説明          |
|----------|----------------|
|`LaunchRequestHandler`|スキル起動時に実行する処理を記述する。|
|`HelloWorldIntentHandler`|HelloWorldインテントに対応する処理を記述する。今回の場合、「ハロー！」や「こんにちは」といった言葉に反応する。|
|`HelpIntentHandler`|Helpインテントに対応する処理を記述する。「ヘルプ」といった発言に反応する。|
|`CancelOrStopIntentHandler`|CancelOrStopインテントに対応する処理を記述する。「キャンセル」や「ストップ」といった発言に反応する。|
|`FallbackIntentHandler`|Fallbackインテントに対応する処理を記述する。発言を受け取れなかった場合に反応する。|
|`SessionEndedRequestHandler`|SessionEndedRequestインテントに対応する処理を記述する。セッションを終了する。|
|`CatchAllExceptionHandler`|CatchAllExceptionインテントに対応する処理を記述する。エラー時に反応する。|
|`SkillBuilder`|Alexaスキルの本体である。|

私たちがAlexaスキルのコーディングをする注意しなければいけないのは、インテントの設定およびインテントに対応するクラスの作成である。ここで最も重要なのは、**インテントの設定を忘れないようにする**ということだ。個人的には<b>インテント設定がNo.1つまずきポイント</b>である。インテント設定はビルド→対話モデルから可能である。ここでは、新しいインテントの設定およびクラスの作成（順不同）に取り組んでいこう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/862467/6cadacb2-8b37-42ba-9cca-cabd8f115508.png)

先にインテントの設定をしていこう。今回作成するインテントは「さすがですね！」「知らなかったです！」「素晴らしいですね！」「センスがありますね！」「そうなんですね！」といった誉め言葉をランダムに読み上げるものとする。設定ではインテント名と発言サンプルを決める。インテント名は`HomekotobaIntent`とし、発言サンプルは「ほめて」と「どう思う」の2つを設定する。**設定終了時の保存とビルドも忘れないようにしよう。**
![2024-02-24_00h14_55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/862467/4eaa462e-a9d7-2d0a-9829-fc0ff5aba17d.png)
![2024-02-24_00h19_35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/862467/0911b395-941d-3a67-b848-2d76795cf84a.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/862467/1f9a3a08-45c4-b810-b761-bd78d5a46a72.png)

次に、上記の`lambda_function.py`に`HomekotobaIntentHandler`クラスを追記していく。作成した`HomekotobaIntentHandler`クラスを以下に示す。この`HomekotobaIntentHandler`クラスは`lambda_function.py`のクラス定義部分に追記してあげるとよい。**コーディング後は保存とデプロイを忘れないようにしよう。**

```python3:lambda_function.py
# 標準のrandomモジュールをインストール
import random

# HomekotobaIntentHnadler
class HomekotobaIntentHandler(AbstractRequestHandler):
    """Handler for HomekotobaIntent."""
    def can_handle(self, handler_input):
        # type: (HandlerInput) -> bool
        # 【変更部分】"HelloWorldIntent"→"HomekotobaIntent"
        return ask_utils.is_intent_name("HomekotobaIntent")(handler_input)

    def handle(self, handler_input):
        # type: (HandlerInput) -> Response
        # 【変更部分】speak_outputの内容
        homekotoba_list = [
            "さすがですね！",
            "知らなかったです！",
            "素晴らしいですね！",
            "センスがありますね！",
            "そうなんですね！",
        ]
        # 上記リストからランダムに一つ要素を抽出
        speak_output = random.choice(homekotoba_list)

        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
        )

# 【注意】sb = SkillBuilder()の後ろに追記
sb.add_request_handler(HomekotobaIntentHandler())

```
記述した`HomekotobaIntentHandler`クラスの中身は`HelloWorldIntentHandler`とほとんど同じことに気づくだろう。`HelloWorldIntentHandler`との違いはhandlerの受取部分とspeak_outの部分を変えているだけである。かなり単純に感じるかもしれない。しかし、これが標準的なAlexaスキル開発の方法といっていい。

ここからは、先ほど作ったインテントが適切に動作するかテストしていこう。テスト画面を開き、スキルテストが有効になっているステージを開発中に変更して実行していく。
![2024-02-24_00h38_58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/862467/56ef93ae-c9c8-00fb-72b6-409ebd40192c.png)


テストではまずAlexaスキルの呼び出しを行う。Alexaスキルの呼び出しには先ほど呼び出し名に設定した「ハローアレクサ」を入力する。うまくいくと、Welcome, you can say Hello or Help. Which would you like to try?と表示されるはずだ。これは起動時処理のクラス`LaunchRequestHandler`がデフォルトのまま動作したためである。もし動かない場合、**スキル設定後に保存とビルドができているか**、もしくは、**コーディング後に保存とデプロイができているか**を確認してほしい。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/862467/37c74789-3cac-6df1-3900-de01b08af5e9.png)
次に、先ほどインテント設定で追加した「ほめて」や「どう思う」といった発言サンプルを入力する。「さすがですね！」や「素晴らしいですね！」と表示されただろうか？
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/862467/d741f619-fb8e-4847-9f49-69b5bb199c42.png)

congratulations！
実はこの「開発中」の時点で、あなたのAlexaデバイスからAlexaスキル「ハローアレクサ」を利用することができる。あなたは自身のAlaxaを使いこなす第一歩を踏み出したのだ。Alexaとの素晴らしい生活の幕開けである。ここからは、さらにAlexaスキルを応用していくときのポイントを示していきたいと思う。


# 応用
Alexaスキルの開発ができるようになったところで、様々な応用シーンを想像できたことと思う。ここからは、応用例として外部APIを利用した天気取得機能の実装と、その際に必要なポイントについて記述していく。
天気取得機能の実装前に、AlexaスキルにおけるPythonの様々なライブラリやモジュールの利用方法について説明する。Pythonには様々なライブラリやモジュールが存在し、それらを利用することで画像処理や自然言語処理といった煩雑な処理も数行のコードで記述できる特徴がある。ライブラリやモジュールを利用するときはソースコード内に`from`や`import`文を利用するのだが、その前に、実行環境がライブラリをインストールする必要がある。ここで必要となるのが`requirements.txt`ファイルである。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/862467/5b63c72e-7e35-8cfc-e6b4-decdd81f6dab.png)
`requirements.txt`にはソースコードで利用するライブラリやモジュールの名前とバージョンを指定する必要がある。例えば、Pythonでrequestsモジュールを利用したい場合、以下のように追記する。
```requirements.txt
requests==2.31.0
```
上記のように`requirements.txt`に記載することでrequestsモジュールがソースコード内で利用できるようになる。今回は実際にrequestsモジュールを利用しOpenWaetherが提供している<a href="https://openweathermap.org/current">Current weather dataAPI</a>から神戸市の天気情報を取得する機能の実装方法を紹介しよう。Alexaスキルの実装方法は先に述べたHelloAlexaスキルと同じである。なお、Current weather dataAPIの利用方法は各自ドキュメントから確認してほしい。
まず初めにインテントの作成から行う。今回のインテントはGetWeathreIntentと名付けた。
![スクリーンショット 2024-02-25 003151.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/862467/8c501db4-8782-ce34-9b53-425ff7fe096d.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/862467/067a92b9-1be8-1673-3108-d8df97adea5f.png)
内部処理を`GetWeatherIntentHandler`クラスとして`lambda_function.py`に追記する。
```python:lambda_function.py
import requests

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

    def handle(self, handler_input):
        # type: (HandlerInput) -> Response
        # kobe city
        id = "1859171"
        API_key = "{your API_KEY}"
        openweather_api = f"https://api.openweathermap.org/data/2.5/weather?id={id}&lang=ja&appid={API_key}"
        content = requests.get(openweather_api).json()
        weather = content["weather"][0]["description"]
        speak_output = f"現在の神戸市の気象状況は{wather}です"

        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
        )

# 【注意】sb = SkillBuilder()より後ろに追記
sb.add_request_handler(GetWeatherIntentHandler())

```
実際にテストした結果が以下だ。アレクサが神戸市の天気を教えてくれている。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/862467/df8f0d1b-e245-78c9-da7c-b10ab58f1c77.png)

# おわりに
いかがだっただろうか？この記事が少しでもAlexaスキルの開発の障壁を下げることに貢献できたなら嬉しい。そして何より、Alexaとのマンネリ生活に終わりを迎え、新たな生活の幕開けになったと信じたい。これからもAlexaとの素晴らしい日々が続くよう願っている。

# 参考
https://developer.amazon.com/ja-JP/alexa/alexa-skills-kit

https://openweathermap.org/

https://openweathermap.org/