Alexa × ChatGPT で楽しくおしゃべりしてみる

# 目次
* きっかけ
* 導入
* 詰まった点
* コード
* 使ってみる
* 最後に


# 1. きっかけ

#### 念願の！！
某インターシップに参加した際に、コンペの景品としてAmazon Echo Popをいただきました！！
実はずっと欲しかった！！！！！


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1936739/c3c04018-12ec-8114-2588-d7b67e1b101e.jpeg" width=50%>

どうやら[Alexa developer console](https://developer.amazon.com/ja-JP/alexa)を使ってAlexaで遊べちゃうらしい...

でも既にある程度の会話は出来るし....

そうだ！ChatGPTモードを勝手に作っちゃおう！！！

# 2. 構成図

構成図は以下のようになっています。

![Alexa構成図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1936739/908e6df0-4af7-7366-5ab1-c3cc87350bf2.png)



# 3. 導入
まず、[Alexaスキル](https://developer.amazon.com/alexa/console/ask)から、スキルの作成の画面に移ります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1936739/6f2b5029-1737-de03-44fb-8e1a7d6da024.png" width=70%>


次に、どのようなアプリを作るかを選択します。
今回はチャットアプリのため、**その他** を選択
モデルは **カスタム** を選択

次にホスティングサービスを選択します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1936739/8f464cfb-1396-0e41-3e15-41b399169f6a.png" width=70%>

今回は手軽に始められる **Alexa-hosted(python)** を選択します。

Alexa-hostedは独自のAWS環境を用意することなく、スキル開発を進めることが可能です。
また、テンプレートやVScodeの拡張機能も用意されているみたいです。
エンドポイント等も設定せずとも使えるため、複雑なスキル作成には向いていませんが、
簡単なスキル作成にはもってこいですね！！！

lambdaだけでなく、S3,Dynamodbも利用できるみたいです。

スキルを作成すると30秒ほどで開発環境が出来上がります。


次に、スキルの呼び出し名を設定していきます。
今回は以下のようにしました。

「チャットジーピーティーさんと会話したい」とユーザが話しかけることによって、
このスキルが呼び出されます。

![スクリーンショット 2023-12-29 午前5.23.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1936739/6372cdb9-fb4b-67bb-5a9c-95c0681d5a11.png)


### 保存とスキルのビルドを忘れずに！！


次に、インテントを作成していきます。
ここから用語が少し増えてきますが、

**インテント** : ユーザの要求、その発話の意図
**スロット** : 変数部分、その発話の中のキーワード

です。
今回は、[チャットジーピーティーさん]の後のワードがChatGPTに投げられるという構造になっているため、以下の設定になっています。ユースケースに合わせた設定にしてください。


![スクリーンショット 2023-12-29 午前5.25.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1936739/d587799d-6d8b-d0b8-cf29-67ea3e53245a.png)

これで準備OKです。
ここからLambdaの作成に移ります。

# 4. 詰まった点・懸念点
Alexa × ChatGPTをする上で詰まった箇所を３点ご紹介します。

## 4-1. Alexaスキルでは自由入力が面倒
Alexaスキルのインテントサンプル発話において、スロットだけで構成されることは許可されていません。これは、ユーザーが何かを言った時に、Alexaがその発話を適切なインテントにマッピングするために少なくとも何らかのキャリアフレーズ（固定された言葉や文）が必要とされるためです。

ちなみに、スロットのみでビルドした際のエラーは以下の通り。

![スクリーンショット 2023-12-29 午前5.34.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1936739/636e66e6-8208-6fce-dd42-aac05e2a3f1b.png)

ではどうするか？
色々対応策があるものの、今回はそれらの解決が記事の主旨ではないため、
スロットのみの使用をやめました。

ユーザとインタラクティブに会話をするためには、スロットのみの会話が必要そうですね。

他の方の対応は以下の通りです。ご参考までに。

https://zenn.dev/kun432/scraps/904ebc3874e0df

https://qiita.com/y_fujisawa/items/b443bd6c3667d8aa8443

## 4-2. Alexa-hostedを選んだ場合、エラーハンドリングが難しくなる
(間違っていたら指摘お待ちしております)

今回は、AWSアカウントを持たずとも簡単に開発できるAlexa-hostedを選びましたが、
うまくいかなかった際のエラー情報が極めて少ない状態でした。

**スキルがリクエストに正しく応答できませんでした**

レスポンスがjsonと上記のメッセージのみでした。
そのため、コードエディタの上部のcloudwatchを確認しようと思いました。そすると、

- この IAM ユーザーには、このアカウントでログ異常を表示するためのアクセス許可がありません。
- ロググループが存在しません
- CloudWatch メトリクスの取得中にエラーが発生しました。

うーんcloudwatchでエラーログが見れない、、
一時的なアカウントの為なのか、、

ということでコードでどこが間違っているかを探す羽目になりました、、
ホスティングサービスを独自のプロビジョニングにすると、カスタマイズ性が向上し、
エラーログも簡単に見れたでしょう、、

## 4-3. OpenAIのApikeyにクレジット追加が必要だった件
これに関してはAlexaSkillsKit関係ないです(笑)

ずっとコードに瑕疵があるものだと睨んでたら、Apikeyがダメだったっていうオチです、、、
要らんところに時間をかけてしまった！！

ローカルでちゃんとApikeyが動作するか確認すべきでした、、、

ちなみにクレジットカード登録していない場合どういうエラーになるかを以下に置いておきます。

```
You exceeded your current quota, please check your plan and billing details. For more information on this error, read the docs: https://platform.openai.com/docs/guides/error-codes/api-errors.
```





# 4. コード

今回はalexa-skills-kit-python-sdkを利用しました。
以下、ドキュメントを一読してからのコーティングをお勧めします。

https://alexa-skills-kit-python-sdk.readthedocs.io/ja/latest/REQUEST_PROCESSING.html

また、外部ライブラリはrequirements.txtに記載しています。
インテントは二種類とスロットが一種類です。

Alexa-hosted環境で、lambdaからS3にアップロードする方法は
私が書いた以下の記事を参考にしてください。

https://qiita.com/paupau/items/57cb7956620d1075a20a


```python
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.handler_input import HandlerInput
import openai
from datetime import datetime
import boto3
import json

# openaiのAPIキー
openai.api_key = "sk-" 

# Alexa-hosted skillのためのS3クライアント初期化
s3_client = boto3.client('s3')

# 利用するバケット名
bucket_name = "YOUR_BUCKET_NAME" 

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

class LaunchRequestHandler(AbstractRequestHandler):
    def can_handle(self, handler_input):
        return ask_utils.is_request_type("LaunchRequest")(handler_input)

    def handle(self, handler_input):
        speak_output = "スキルが起動しました。ChatGPTモードに入ります。"
        return (
            handler_input.response_builder
                .speak(speak_output)
                .ask("何をお話ししますか？")
                .response
        )

class ChatGPTHandler(AbstractRequestHandler):
    def can_handle(self, handler_input):
        return ask_utils.is_intent_name("ChatGPTIntent")(handler_input)

    def handle(self, handler_input):
        # セッション属性を取得
        session_attr = handler_input.attributes_manager.session_attributes

        # 以前の会話をセッションから取得
        previous_conversations = session_attr.get("conversations", [])

        user_utterance = handler_input.request_envelope.request.intent.slots["UserUtterance"].value

        # 現在のユーザー発話を会話に追加
        previous_conversations.append({"role": "user", "content": user_utterance})

        try:
            response = openai.ChatCompletion.create(
                model="gpt-3.5-turbo",
                messages=previous_conversations
            )
            # 応答からテキストを取得し、不要な空白を削除
            chat_response = response['choices'][0]['message']['content'].strip()
            # 新しい応答を会話に追加
            previous_conversations.append({"role": "assistant", "content": chat_response})
            
        except Exception as e:
            chat_response = f"エラーが発生しました。再度お試しください: {e}"

        # 会話をセッション属性に保存
        session_attr["conversations"] = previous_conversations

        # Alexaのレスポンスを生成
        return (
            handler_input.response_builder
                .speak(chat_response)
                .ask("何か他に聞きたいことはありますか？")
                .response
        )

class EndSessionHandler(AbstractRequestHandler):
    def can_handle(self, handler_input):
        return ask_utils.is_intent_name("EndSessionIntent")(handler_input)

    def handle(self, handler_input):
        # セッション属性から会話履歴を取得
        session_attr = handler_input.attributes_manager.session_attributes
        conversations = session_attr.get("conversations", [])

        # 現在の日付と時間をファイル名にする
        now = datetime.now()
        file_name = f"Media/トーク履歴/{now.strftime('%Y-%m-%d_%H-%M-%S')}.txt"
        
        # 会話履歴を整形して保存
        formatted_conversations = "\n".join([f"{c['role']}: {c['content']}" for c in conversations])

        # S3に会話履歴を保存
        try:
            s3_client.put_object(
                Bucket=bucket_name,
                Key=file_name,
                Body=formatted_conversations.encode("utf-8")
            )
            speak_output = "スキルを終了します。さようなら。会話履歴は保存されました。"
        except Exception as e:
            speak_output = f"保存に失敗しました: {e}"

        # 会話終了の応答
        return (
            handler_input.response_builder
                .speak(speak_output)
                .response
        )
    

# ハンドラの設定
sb = SkillBuilder()
sb.add_request_handler(LaunchRequestHandler())
sb.add_request_handler(ChatGPTHandler())
sb.add_request_handler(EndSessionHandler())
lambda_handler = sb.lambda_handler()
```


# 5. 使ってみる

うん、正しくChatGPTに質問を投げれていますし、会話の引き継ぎもできていますね！！

![Alexaテスト.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1936739/42639190-d00f-83ef-8225-498ed5624cfa.png)


S3を確認してみましょう！

![スクリーンショット 2023-12-29 午後8.08.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1936739/2f16e133-1828-d40d-47f0-bd5ca703b6d1.png)

中身も見てみましょう！
![スクリーンショット 2023-12-29 午後8.09.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1936739/336c7cfa-fe6d-b540-5a36-528e496747c3.png)

中身も会話内容が保存できていますね！

# 6. 最後に

最後までお読みいただき、ありがとうございました！
実用的ではないかも知れませんが、コードやアイデアの参考にしてくださいね！
**いいね** を是非よろしくお願いします！