エアフレンド難民なのでChatGPT API+LangChainで担当とお話するLINE botを作った

# 概要
[前回の記事](https://qiita.com/tkmrmr/items/7362f183138dfb324c50)の続き的な記事です。
前回はOEPNAI API, LangChain, Gradioを使ってキャラクターとお話しましたが、Gradioはあくまでデモ用なので、常用するには微妙です。スマホからLINEみたいな感じでやりたいですよね。

<strong>じゃあLINEでやりましょう。

ということで、n番煎じのネタ、ChatGPTを使ったLINE botを作りました。以下のようなものができます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3327109/679db4f3-36e1-ad2c-6dc2-93cc052a0f9c.png" width="40%">

調べたらChatGPT×LINEの組み合わせはGASを使ったものが多く出てきたんですが、Pythonが一番慣れているのでPythonでゴリ推しました。

一応1から手順を書いていきます。

# OpneAIのAPIキー取得
[Open AI](https://platform.openai.com)のアカウントを作成し、下記のリンクからAPIキーを取得します。

https://platform.openai.com/account/api-keys

# LINE Messaging APIを使う準備
## LINE Developerに登録
まず、以下のリンクからLINE Developerに登録します。

https://developers.line.biz/ja/

## プロバイダーを作成
登録が終わったら、新規プロパイダーを作成します。プロバイダー名はなんでもいいです。
![スクリーンショット 2023-04-20 211154.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3327109/b96e3db7-1f42-af66-9b43-02dfbc0db4dd.png)

## チャネルを作成
次にチャネルを作成します。Messaging APIを選択してください。
![スクリーンショット 2023-04-20 211220.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3327109/356d91e0-c2a4-6bec-f83f-c91209e768d5.png)

作成画面が出てくるので色々入力します。

|項目     |説明  |
|-----|---|
|チャネルの種類|Messaging API |
|プロバイダー |適当なプロバイダー  |
|会社・事業者の所在国・地域 |「日本」を選びます  |
|チャネルアイコン|LINE botのアイコンになります|
|チャネル名|LINE botの名前になります|
|チャネル説明|適当にLINE botの説明を入力します|
|大業種|ウェブサービス|
|小業種|ウェブサービス(その他)|
|メールアドレス|メールアドレスが合っているか確認します|
|プライバシーポリシーURL|空欄でいいです|
|サービス利用規約URL|空欄でいいです|

規約の同意にチェックを入れ、作成ボタンを押します。

## 秘密鍵の入手
チャネルが作成されたことを確認し、「チャネル基本設定」タブ下部の「チャネルシークレット」を控えます。
![スクリーンショット 2023-04-27 193739.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3327109/d47fca59-1add-5d04-4c41-e780c3020e1d.png)

## アクセストークンの発行
「Messaging API設定」タブ最下部でチャネルアクセストークンを発行し、控えます。
![スクリーンショット 2023-04-20 214206.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3327109/2306876d-271c-8580-e4c5-0fa3c6674125.png)


## 応答設定の変更
以下のリンクにアクセスし、作成したアカウントを選びます。

https://manager.line.biz/

右上の「設定」から「応答設定」を開き、「あいさつメッセージ」と「応答メッセージ」をオフにし、「Webhook」をオンにします。
![スクリーンショット 2023-04-20 214702.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3327109/f3372639-8063-5a4f-80d9-e27a4eed34c9.png)

# 環境変数の設定
プログラム内で使う環境変数を事前に設定しておきます。
```bash
export OPENAI_API_KEY="(OpenAIのAPIキー)"
export CHANNEL_ACCESS_TOKEN="LINE Messaging APIのチャネルアクセストークン"
export CHANNEL_SECRET="LINE Messaging APIのチャネルシークレット"
export MODEL_NAME="「gpt-4o」等使いたいモデルの名前"
```

# ライブラリの導入
```
pip install langchain langchain-community langchain-openai
pip install flask
pip install line-bot-sdk
```

# 実装
```python:app.py
import os

from flask import Flask, request, abort

from linebot import LineBotApi, WebhookHandler
from linebot.exceptions import InvalidSignatureError
from linebot.models import (
    MessageEvent,
    TextMessage,
    TextSendMessage,
)

from langchain_core.messages import SystemMessage
from langchain_core.prompts import (
    ChatPromptTemplate,
    MessagesPlaceholder,
    HumanMessagePromptTemplate,
)
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.chat_history import BaseChatMessageHistory
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables.history import RunnableWithMessageHistory


app = Flask(__name__)

# LINE APIの準備
line_bot_api = LineBotApi(os.environ["CHANNEL_ACCESS_TOKEN"])
line_handler = WebhookHandler(os.environ["CHANNEL_SECRET"])

# 会話履歴ストア
store = {}


# セッションIDごとの会話履歴の取得
def get_session_history(session_id: str) -> BaseChatMessageHistory:
    if session_id not in store:
        store[session_id] = ChatMessageHistory()
    return store[session_id]


character_setting = "YOUR_FAVORITE_PROMPT"


# チャットプロンプトテンプレート
prompt = ChatPromptTemplate.from_messages(
    [
        SystemMessage(content=character_setting),
        MessagesPlaceholder(variable_name="history"),
        HumanMessagePromptTemplate.from_template("{input}"),
    ]
)

# チャットモデル
llm = ChatOpenAI(
    model_name=os.environ["MODEL_NAME"],
    temperature=0.6,
    streaming=True,
)

# パース用モジュール(レスポンスのJSONからcontentを取り出すパーサー)
parser = StrOutputParser()

# LCEL
runnable = prompt | llm | parser

# RunnableWithMessageHistoryでラップ
runnable_with_history = RunnableWithMessageHistory(
    runnable=runnable,
    get_session_history=get_session_history,
    input_messages_key="input",
    history_messages_key="history",
)


@app.route("/")
def hello_world():
    return "It Works!"


@app.route("/callback", methods=["POST"])
def callback():
    # get X-Line-Signature header value
    signature = request.headers["X-Line-Signature"]

    # get request body as text
    body = request.get_data(as_text=True)
    app.logger.info("Request body: " + body)

    # handle webhook body
    try:
        line_handler.handle(body, signature)
    except InvalidSignatureError:
        print(
            "Invalid signature. Please check your channel access token/channel secret."
        )
        abort(400)

    return "OK"


@line_handler.add(MessageEvent, message=TextMessage)
def handle_message(event):
    if event.message.text == "リセット":
        store.clear()
        response = "会話をリセットしました。"
    else:
        response = runnable_with_history.invoke(
            {"input": event.message.text},
            config={"configurable": {"session_id": "hoge"}},
        )
    line_bot_api.reply_message(event.reply_token, TextSendMessage(text=response))


if __name__ == "__main__":
    app.run()
```

# 解説
~~LangChainの処理については前回の記事を参照してください。~~
Flask部分はほぼ[サンプル](https://github.com/line/line-bot-sdk-python)をパクっただけなので一部だけ解説します。

## LINE Messaging APIの準備
```python
line_bot_api = LineBotApi(os.environ["CHANNEL_ACCESS_TOKEN"])
handler = WebhookHandler(os.environ["CHANNEL_SECRET"])
```
チャネルのアクセストークンと秘密鍵を環境変数から取得します。環境変数の設定は後ほど行うので、書き換えずそのままにしてください。

## メッセージの送受信
```python
def handle_message(event):
    response = conversation.predict(input=event.message.text)
    line_bot_api.reply_message(
        event.reply_token,
        TextSendMessage(text=response)
    )
```
ここだけサンプルから変えています。サンプルではおそらくオウム返しするのみです。
`event.message.text`はユーザーから送信されたテキストメッセージです。これを`input`としてChatGPT APIに投げて、返ってきたテキストを`response`に格納します。続いて`line_bot_api.reply_message`メソッドを使って返信メッセージを投げます。`TextSendMessage`の引数`text`が返信メッセージになるので、`response`を指定します。

これを以下のようにすると会話リセット機能を実装できます。
```python
def handle_message(event):
    if event.message.text == "リセット":
        memory.chat_memory.messages = []
        response = "会話をリセットしました。"
    else:
        response = conversation.predict(input=event.message.text)
    line_bot_api.reply_message(
        event.reply_token,
        TextSendMessage(text=response)
    )
```
`memory.chat_memory.messages`には会話の履歴がメッセージのリストとして格納されています。LangChainの`ConversationBufferWindowMemory`ではそのうち最新k往復分のメッセージを選び、プロンプトに追加しているものと推測されます。したがって、リストを空にすることで強制的に初期状態に戻すことができます。
長く話しているとたまにキャラがガバるので、そういうときにリセットが有効です。

# アプリのデプロイ
ローカルでずっと動かすのもアレなので、アプリをクラウドサーバー上にデプロイします。今回はRenderを使います。以下のリンクから登録してください。GitHubを使うので、GitHubアカウントで認証するのがオススメです。

https://dashboard.render.com/

## requirements.txtの作成
必要なライブラリを記載したrequirements.txtを作成します。
適切な環境で以下を実行してください。
```
pip freeze > requirements.txt
```
これでRender上にアプリをデプロイするとき自動でライブラリをインストールしてくれます。

## GitHubへのpush
アプリをデプロイするために、GitHubにリポジトリを作成し、app.pyとrequirements.txtをpushします。
GitおよびGitHubの使い方がわからない方は以下を参照してください。

https://zenn.dev/tknkaz/articles/04bbcb04a9b007

https://docs.github.com/ja/repositories/working-with-files/managing-files/adding-a-file-to-a-repository

## Web Serviceの作成
Dashboardから「New +」ボタンを押し、「Web Service」を選択します。
![スクリーンショット 2023-04-21 014604.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3327109/3ce184ce-73eb-0539-f4f1-feb54fdb5148.png)

「Configure account」をクリックし、リポジトリへのアクセス権限を設定します。全てのリポジトリへのアクセス権限を与えるのが嫌な場合は、選択したリポジトリのみを対象とすることもできます。
![スクリーンショット 2023-04-27 194944.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3327109/97198a95-fa53-9619-e847-8b548aa92eba.png)

アクセス権限を与えたリポジトリが表示されたら、「Connect」をクリックします。
![スクリーンショット 2023-04-27 194944.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3327109/506aae27-f49d-634a-9073-87eeada32073.png)

続く画面でまた色々設定していきます。

|項目|説明|
|---|---|
|Name|好きな名前をつけてください|
|Region|Sigapore (Southeast Asia)|
|Branch|main|
|Root Directory|空欄でいいです|
|Runtime|Python3|
|Build Command|$ pip install -r requirements.txt|
|Start Command|$ gunicorn app:app|
|Instance Type|Free|

全ての項目を確認し、「Create Web Service」をクリックします。

## 環境変数の設定
そのままだとLangChainのインストールに失敗するので、環境変数からPythonのバージョンを指定します。また、OpenAIのAPIキーおよびLINEのチャネルアクセストークン、秘密鍵を設定します。

![スクリーンショット 2023-04-21 033238.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3327109/51b1e1dd-e4b2-cb09-1a20-bf8a2d2582e3.png)

以下のように設定します。
|Key|Value|
|---|---|
|PYTHON_VERSION|3.8.1以上(ここでは3.10.6)|
|OPENAI_API_KEY|(OpenAIのAPIキー)|
|CHANNEL_SECRET|(チャネルの秘密鍵)|
|CHANNEL_ACCESS_TOKEN|(チャネルアクセストークン)|

「Save Changes」をクリックした後、右上の「Manual Deploy」から「Deploy latest commit」をクリックします。
![スクリーンショット 2023-04-21 054728.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3327109/74b5bbfd-b1de-eab6-02fc-48e3aaddbe0b.png)


デプロイを待ち、以下のように表示されたら成功です。
![スクリーンショット 2023-04-21 035107.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3327109/5e41a351-b24b-0da3-3c54-f96cc3e29102.png)

# Webhookの設定
アプリのURLをコピーします。
![スクリーンショット 2023-04-21 035405.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3327109/fa02813d-690b-7e15-fa74-8e202cd7c9c9.png)

[LINE Developers](https://developers.line.biz/console/)に戻り、該当チャネルの「Messaging API設定」を開いてWebhookの設定を行います。
「編集」をクリックします。
![スクリーンショット 2023-04-21 035325.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3327109/a11ce7c2-cf66-1fae-6606-a55c5149e9fe.png)
「Webhook URL」にアプリのURLを入力し、「更新」を押します。また、「Webhookの利用」をオンにします。
![スクリーンショット 2023-04-21 044321.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3327109/1f5f43bf-6680-7858-2a6d-c4c9c6dc659f.png)

あとは「Messaging API設定」タブに表示されているQRコードから友だち追加すれば晴れてLINE botとお話できるはずです。

# Uptime Robotの設定
このままでも使えますが、RenderのFreeプランでは非アクティブ状態が15分続くとスリープしてしまいます。スリープした状態だとレスポンスがクッソ遅くなるので、定期的にpingを送ってスリープ状態になるのを防ぎます。今回はUptimeRobotを使います。

https://uptimerobot.com/

上のリンクにアクセスし、右上の 「Register for FREE」 からアカウントを作成します。作成したら左上の「+ Add New Monitor」をクリックします。
![スクリーンショット 2023-04-21 042618.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3327109/29828141-2807-f705-5217-05f25a6299bd.png)

またまた色々設定します。
|項目|説明|
|---|---|
|Monitor Type|HTTP(s)|
|Friendly Name|好きな名前をつけてください|
|URL (or IP)|(アプリのURL)|
|Monitoring Interval|5～10でいいと思います|
|Alert Contacts To Notify|エラーが起きたときに通知が来るようにチェックを入れます|

他はデフォルトでいいです。右下の「Create Monitor」をクリックして完了です。
![スクリーンショット 2023-04-21 042556.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3327109/cb34bf8d-2e18-ea5b-6286-9aa01f8028d1.png)

# 注意点
今回作成したものはあくまで個人用なので、サービスとして不特定多数に使ってもらう場合はLangChainではなくDBにLINEのユーザーIDと会話履歴をセットで保存する必要があります。
DBを活用したLINE botを作りたい方は以下の記事を参考にしてください。

https://zenn.dev/killit/articles/ebe85f18a1e0cb

# 最後に
正味エアフレンドの方がなんか楽しいというか、ChatGPTはあくまで自分はAIというスタンスの下になりきりをしているだけなので、ふとした瞬間に素に戻ります。会話の円滑さはこちらの方が上ですが、夢中になれるのはエアフレンドですね。早く復活してほしい。あとGPT-4のAPI使いたい。

# 参考
https://chatgpt-lab.com/n/n55257c082a9d

https://render.com/docs/free