AlexaとChatGPT APIを組み合わせて質問に回答するスキルを開発しよう！

こんにちは！今回は、Alexa Skills KitとChatGPT APIを組み合わせて、
ユーザーが質問をすると回答を返すスキルを開発する方法について説明します。

このスキルは、置物となっていることが多いAlexa君を実用的なものにしてくれる、、、はず。

## 開発環境の設定

まずは、開発環境の設定から始めましょう。

### 1. Alexa Skills Kitの設定

Alexa Skills Kitを使用するためには、開発者アカウントを作成する必要がある。
アカウントが作成されたら、Alexa Developer Consoleにログインし、スキルをつくっていく。

### 2. ChatGPT APIの設定

ChatGPT APIを使用するためには、OpenAIのAPIキーが必要です。
OpenAIのWebサイトからAPIキーを取得し、APIエンドポイントを設定。

参考: [OpenAI API キーを取得する方法](https://laboratory.kazuuu.net/how-to-get-an-openai-api-key/)


## Alexa Skills Kit側の実装
###  1. 新しいスキルを作成
今回は試しにChatGPTという名前にしました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3179263/e231b6e5-c6b0-2ce4-8cc7-c4871907f97e.png)
プライマリーロケールは日本語でいいと思います。後から変更可能です。


### 2. インテンドの作成
インテンドはユーザーがスキルを呼び出した後に実行する処理を選択するための文字です。
例えば
「アレクサ、Amazon musicで**音楽を流して**」とか
「アレクサ、COOKPADで**ポテトサラダのレシピを教えて**」とかの太字部の部分ですね。

そして、サンプル発話といわれているのが、
「**COOKPADでポテトサラダのレシピを教えて**」の全体の部分です。

このサンプル発話の中に{}でくくった**インテンドスロット名**の所を、Alexaは入力として変数に格納し、Lambdaに送信します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3179263/7ef1ebb9-ac8f-2e14-ce9d-dfe52542ea79.png)

ここでは、「アレクサ、ChatGPTで質問**ダイエットに向いている食事は？**」のような文章を想定しています。
そして、インテンド名"query"に"ダイエットに向いている食事は？"の文章を格納しています。
スロットタイプを**AMAZON.SearchQuery**にすることで、どのような文字でも自動的に認識して入力してくれるようにしています。


本当は”質問”というサンプル発話も無くして、ユーザが自在に音声を入力できるようにしたいのですが、Alexaの制限でできないようです。。

## Alexa Skills KitとChatGPT APIの連携

Alexa Skills KitとChatGPT APIを連携するためには、以下の手順が必要。

1. Alexa Skills Kitからの**リクエストをLambda関数で受け取る**。
2. リクエストからユーザーの**発話を抽出**する。
3. ChatGPT APIに発話を送信し、生成された**回答を取得**する。
4. Alexa Skills KitのレスポンスJSONに**回答を設定して返却**する。

と、やることはいたってシンプル。
次の項目で、Lambda側の実装について説明していきます。

## Lambda関数の作成
Alexaからのリクエストを受け付け、ChatGPT APIに処理を渡す関数を作成していきます。

まずは全体のコード例を載せて、その後に個々の部分について解説していきます。


```python
import openai
from ask_sdk_core.dispatch_components import AbstractRequestHandler
from ask_sdk_core.skill_builder import CustomSkillBuilder
from ask_sdk_core.utils import is_intent_name, is_request_type
from ask_sdk_model import Response
from ask_sdk_model.ui import SimpleCard

sb = CustomSkillBuilder()

class LaunchRequestHandler(AbstractRequestHandler):
    def can_handle(self, handler_input):
        # type: (HandlerInput) -> bool
        return is_request_type("LaunchRequest")(handler_input)

    def handle(self, handler_input):
        # type: (HandlerInput) -> Response
        speech_text = "ChatGPTです。質問は何ですか？"

        handler_input.response_builder.speak(speech_text).set_card(
            SimpleCard("ハローワールド", speech_text)).set_should_end_session(
            False)
        return handler_input.response_builder.response

class HelloWorldIntentHandler(AbstractRequestHandler):
    def can_handle(self, handler_input):
        # type: (HandlerInput) -> bool
        return is_intent_name("作成したインテンド名")(handler_input)

    def handle(self, handler_input):
        openai.api_key = <"your_api_key">
        try:
            # Alexaからのリクエストを取得
            request = handler_input.request_envelope.request
            query = request.intent.slots["query"].value + "。50字以内で回答して。"
            # ChatGPT APIにリクエストを送信
            data = {'text': query}
            response = openai.ChatCompletion.create(
                model="gpt-3.5-turbo",
                messages=[
                    {"role": "user", "content": query}
                ]
            )
            response_text = response["choices"][0]["message"]["content"]

            # レスポンスを生成
            handler_input.response_builder.speak(response_text).set_card(
                SimpleCard("質問の回答", response_text)).set_should_end_session(True)
            return handler_input.response_builder.response
            
        except Exception as e:
            logger.error(e)
            traceback.format_exc()
            error_response_text = "エラーが発生しました。"
            handler_input.response_builder.speak(error_response_text).set_card(
                SimpleCard("エラー", error_response_text)).set_should_end_session(True)
            return handler_input.response_builder.response

sb.add_request_handler(LaunchRequestHandler())
sb.add_request_handler(HelloWorldIntentHandler())

lambda_handler = sb.lambda_handler()

```

この例では、Alexaからのリクエストから発話内容を抽出し、ChatGPT APIに送信して、生成された回答を変数response_textに設定。次に、ASKのレスポンスを生成し、回答を設定している。

ただし、以下ライブラリはLambda標準では用意されていないため、別途Layerを作成する必要アリ。

* ask-sdk-core
* ask-sdk-model
* openai

レイヤーの作成方法は以下参照

> [AWS Lambda Layers作成から紐付けまでさらっと解説](https://qiita.com/shimajiri/items/e0e41f2c7a8ac99f3253)


まず、「アレクサ、ChatGPTを開いて」のように、スキル名だけが呼び出されたときの処理は以下（LaunchRequestHandlerクラス）
```python
class LaunchRequestHandler(AbstractRequestHandler):
    def can_handle(self, handler_input):
        # type: (HandlerInput) -> bool
        return is_request_type("LaunchRequest")(handler_input)

    def handle(self, handler_input):
        # type: (HandlerInput) -> Response
        speech_text = "ChatGPTです。質問は何ですか？"

        handler_input.response_builder.speak(speech_text).set_card(
            SimpleCard("ハローワールド", speech_text)).set_should_end_session(
            False)
        return handler_input.response_builder.response
```

次に、作成したインテンド名で呼び出されるのが以下。
```python
class HelloWorldIntentHandler(AbstractRequestHandler):
    def can_handle(self, handler_input):
        # type: (HandlerInput) -> bool
        return is_intent_name("作成したインテンド名")(handler_input)

    def handle(self, handler_input):
        openai.api_key = <"your_api_key">
        try:
            # Alexaからのリクエストを取得
            request = handler_input.request_envelope.request
            query = request.intent.slots["query"].value + "。50字以内で回答して。"
            # ChatGPT APIにリクエストを送信
            data = {'text': query}
            response = openai.ChatCompletion.create(
                model="gpt-3.5-turbo",
                messages=[
                    {"role": "user", "content": query}
                ]
            )
            response_text = response["choices"][0]["message"]["content"]

            # レスポンスを生成
            handler_input.response_builder.speak(response_text).set_card(
                SimpleCard("質問の回答", response_text)).set_should_end_session(True)
            return handler_input.response_builder.response
```
can_handle関数のis_intent_nameでインテンド名を判定し、その後のhandle関数に処理を渡しています。
スロットリングに格納された質問内容は以下で取得され、"query"変数に格納されています。

```python
request = handler_input.request_envelope.request
request.intent.slots["query"].value
```
そしてここがミソ。**Alexaはデフォルトで8秒レスポンスがないとタイムアウトしてしまう。** 
そこでChatGPTに文字制限をしてレスポンスの文章を返してもらう。（しないと平気で３０秒とかかかります。）
```python
query = request.intent.slots["query"].value + "。50字以内で回答して。"
```
あとは以下でChatGPT APIにリクエストを送信
```python
            data = {'text': query}
            response = openai.ChatCompletion.create(
                model="gpt-3.5-turbo",
                messages=[
                    {"role": "user", "content": query}
                ]
            )
```
レスポンスボディに値を設定して完了です。
```python
            response_text = response["choices"][0]["message"]["content"]

            # レスポンスを生成
            handler_input.response_builder.speak(response_text).set_card(
                SimpleCard("質問の回答", response_text)).set_should_end_session(True)
            return handler_input.response_builder.response
```
### 最後に
Alexa君の置物化がこれで何とかなればいいのですが、、
申請以降のフェーズに関しては需要があれば書こうかなぁ、、