LINEでChatGPTと会話してみよう

はじめに

 こんにちは、システム情報学科3年の片山です。普段はWebアプリケーションの勉強をしています。

 ChatGPTのAPIが公開されてから数日経ちましたが、触れてみた方はいますか?無料枠でも十分遊べそうなことが分かったので、今回はLINEの公式アカウント(昔のLINE@で、誰でも作れる)とChatGPTをつなげて、LINEのトーク画面からチャットボットを利用できるようにしてみます。

LINE公式アカウントの開設

 まずは、LINE公式アカウントを開設してみます。

 このリンクから公式ページに飛び「LINE公式アカウントをはじめる」をクリックしてください。


LINE公式アカウント作成画面

 アカウントを作成をクリック。その後、画面の案内に従ってメールアドレス認証や名前、パスワードの登録を行ってください。


LINE公式アカウント情報入力画面

 ユーザ名やメールアドレスは伏せています。アカウント名というのが、LINEの友だち名になるので適切なものを名付けてください。今回はChatGPTテストとしました。


LINE公式アカウントホーム画面

 これがホーム画面です。ここからは各種設定を行っていきます。


LINE公式アカウント 友だちを増やす画面

 右のメニューから「友だちを増やす」をクリックして「友だち追加QRコードを作成」をクリックしてください。


友だち追加QRコード画面

 友だち追加QRコードが表示されるので、スマホのLINEから読み取り友達になってください。


LINEトーク画面

 あらかじめ用意されているテンプレートが返ってくるみたいです。LINE公式アカウントの開設は以上です。思ったより簡単ですね。
 次からはLINEのMessaging APIの設定を行っていきます。

Messaging API の設定

 今回作成した「ChatGPTテスト」にメッセージを送ると、LINEプラットフォーム->自分で用意した何かしらのサーバにWebhookイベントというものが送信されます。
 Webhookイベントを受け取るサーバの設定をする前に、LINE公式アカウントのMessaging API設定と、LINE DevelopersのMessaging API設定を行います。

LINE公式アカウント設定画面

 設定->応答設定画面で、チャットを有効化し「Messaging APIの設定画面を開く」をクリックしてください。


LINE公式アカウント設定画面

 「Messaging APIを利用する」をクリック。


Messagin API 開発者情報登録画面

 本名とメールアドレスを入力してください。


プロバイダー登録画面

 プロバイダーというものを作成するみたいです。今回は本名で登録してみます。


プライバシーポリシーと利用規約登録画面

 自サービスのプライバシーポリシーと利用規約が記述されているWebページのリンクを貼るみたいですが、今回は公開する予定もないので空白のままスキップします。


Messaging API 情報画面

 登録は完了しましたが、引き続き別の設定を行うため「LINE Developers」のリンクをクリックしてください。


LINE Developers ランディングページ

 右上のアイコンをクリックし「LINE Developers コンソール」をクリックしてください。


LINE Developers プロバイダー選択画面

 先ほど作成したプロバイダー(本名)をクリックしてください。


LINE Developers チャネル選択画面

 先ほど作成したLINE公式アカウントをクリックしてください。


LINE Developers Messaging API 設定画面

 Messaging API設定タブをクリックし、最下部までスクロールしてください。


チャネルアクセストークン発行画面

 チャネルアクセストークン(長期)を生成してコピーし、一時的にメモ帳などに貼り付けておいてください。

 これで、LINEプラットフォームからWebhookイベントを送信する準備が整いました。

FastAPIでAPIサーバを構築

 Webhookイベントというと難しそうに聞こえますが、要するにLINEプラットフォームからのHTTPSでのPOSTリクエストのことです。こちらは、適当なAPIサーバを用意してあげてステータスコードの200を返してあげるだけでいいということです。

 今回は、外部に公開されているサーバとドメインを用意していること前提で解説していきます。私の環境では、サーバにはOCIの無料枠インスタンスを、ドメイン関連はAWSのRoute53を、OSはUbuntuかDebian(どちらでもOK)を使用しています。
 サーバの立て方やドメイン取得方法は別途お調べください。

 FastAPIは軽量で高速なAPIを構築することができるPythonのフレームワークです。とにかくシンプルで分かりやすい!ということで、必要なライブラリをインストールしていきます。ついでにChatGPTのAPIを利用するのに必要なopenaiなどもインストールしています。

pip install fastapi uvicorn gunicorn python-dotenv pydantic typing openai

 作業ディレクトリは~/chatgptにしています。ご自由に。ディレクトリを用意したら、中に「main.py」「.env」「gunicornConfig.py」ファイルを作成してください。また「log」ディレクトリも作成してください。

ディレクトリ構造

 main.pyの中身は以下のようにしてください。

from fastapi import FastAPI
from dotenv import load_dotenv

import os

load_dotenv()

app = FastAPI()

@app.get("/")
async def root():
    return {"message": "Hello World"}

 load_dotenv部分では、同じディレクトリにある.envを読み込んでいます。.envにはパスワードやトークンなどの情報を記述していきます。
 @app.get("/")部分では、ルートにGETリクエストしてきた際の処理を書いています。今回はシンプルなJSONを返すようになっています。

 gunicornConfig.pyの中身は以下のようにしてください。

worker_class = "uvicorn.workers.UvicornWorker"

bind = "0.0.0.0:8000"

accesslog = "./log/access.log"
errorlog = "./log/error.log"

daemon = False

 ここもコピペで問題ありません。
 bindの:8000はポートです。自身の環境に合わせて指定してください。他にApacheなどのWebサーバソフトを起動していなければ80や443でもOKです。
 accesslogとerrorlogはそのまま、ログを記録するファイルのパスを指定します。先ほどlogディレクトリを作ったので、access.logとerror.logは自動で生成されます。
 daemonはそのまま、daemonで起動するかどうかの設定です。つけっぱなしにしたいときはここをTrueにしてください。

 ここで一旦、gunicornを起動してアクセスしてみます。起動用のコマンドはこちらです。

gunicorn -c ./gunicornConfig.py main:app

 -c オプションでコンフィグのパスを指定します。

 この状態で、事前に用意したサーバにHTTPでアクセスします。最終的にはLINEプラットフォーム(つまり外部のサーバ)からアクセスされることになるので、こちらも事前に用意したドメインを使用してアクセスできるかチェックしてください。(http://用意したドメイン:8000/)
 以下の画面が表示されれば成功です。

APIサーバアクセス成功

 ついでに/docsにもアクセスしてみてください、FastAPIはこのドキュメントを自動的に生成して表示してくれます。大変便利!

FastAPIのドキュメント画面

 次は、LINEプラットフォームからのWebhookイベントを受け取る場所を作ります。main.pyの中身を以下のコードに書き換えてください。コードの詳しい解説は後回しにします。

from fastapi import FastAPI
from pydantic import BaseModel
from typing import List

import os
import requests
import json

from dotenv import load_dotenv

load_dotenv()

class LineModel(BaseModel):
    destination: str
    events: List

app = FastAPI()

@app.get("/")
async def root():
    return {"message": "Hello World"}

@app.post("/linebot")
async def linebot(request: LineModel):
    return request

 HTTPS化のため、Certbotを用いて証明書を取得しますが、こちらも省略します。申し訳ございません!
 先ほどのFastAPI画面を表示できるのであれば、おそらく以下のコマンドで取得できるかと思います。(Webサーバアプリはいったん停止して、ポート80と443は開けてください)

apt install certbot
certbot certonly --standalone -d 事前に用意したドメイン

 main.pyを書き換え、証明書の発行が完了したら、以下のコマンドを使用してgunicornを起動してください。

gunicorn -c ./gunicornConfig.py main:app --certfile /etc/letsencrypt/live/事前に用意したドメイン/fullchain.pem --keyfile /etc/letsencrypt/live/事前に用意したドメイン/privkey.pem

 起動が成功したら。今度は先ほどのFastAPIドキュメントページにHTTPSでアクセスしてみてください。(https://用意したドメイン:8000/docs)
 以下の画面が表示され、HTTPS接続がされていればOKです。

HTTPSでFastAPIのドキュメントページを表示

Messaging API で応答メッセージを送信

 APIサーバを構築したので、LINE側で追加の設定を行っていきます。

Webhook URL 設定画面

 LINE Developersの設定画面から「Messaging API設定」をクリックして、Webhook設定というところの「Webhook URL」に先ほど用意したURLを指定します。URLは「https://事前に用意したドメイン:8000/linebot」のようになります。


Webhook URL 検証画面
Webhook URL 検証成功画面

 Webhook URLを入力し「検証」ボタンを押した後、成功と表示されれば無事にAPIサーバと接続できることを確認できます。


Webhookの追加設定

 接続が確認出来たら「Webhookの利用」と「エラーの統計情報」をONにしてください。これでLINE Developersの設定は完了です。

 次は、APIサーバ側の設定(プログラミング)を行っていきます。まずは.envファイルを開いて以下のように「チャネルアクセストークン(長期)」を記述してください。
 今回は、トークンの前に「Bearer 」を付けています。

LINE_CHANNEL_ACCESS_TOKEN="Bearer 0123456789abcdefghijklmnopqrstuvwxyz"

 .envへのトークン記述が終わったら、main.pyを開き、中身を以下のように書き換えてください。

from fastapi import FastAPI
from pydantic import BaseModel
from typing import List

import os
import requests
import json

from dotenv import load_dotenv

load_dotenv()

channelAccessToken = os.environ["LINE_CHANNEL_ACCESS_TOKEN"]

class LineModel(BaseModel):
    destination: str
    events: List

app = FastAPI()

@app.get("/")
async def root():
    return {"message": "Hello World"}

@app.post("/linebot")
async def linebot(request: LineModel):
    msg = request.events[0]["message"]["text"]
    replyToken = request.events[0]["replyToken"]
    
    print("ユーザ: %s" % msg)
        
    sendMsg = "そうなんだ!"
    
    sendPostHead = {
        "Authorization": channelAccessToken
    }
    
    sendPostData = {
        "replyToken": replyToken,
        "messages": [
            {
                "type": "text",
                "text": sendMsg
            }
        ]
    }
    
    res = requests.post("https://api.line.me/v2/bot/message/reply", headers=sendPostHead, json=sendPostData)
    
    print("linebotのレスポンス: %s" % res)
    
    return request

 部分ごとに解説していきます。

from fastapi import FastAPI
from pydantic import BaseModel
from typing import List

import os
import requests
import json

from dotenv import load_dotenv

 必要なライブラリなどを読み込んでいます。 BaseModelとListは、この後解説するLineModelというスキーマに使用します。

load_dotenv()

channelAccessToken = os.environ["LINE_CHANNEL_ACCESS_TOKEN"]

 この部分で先ほど記述した.envの変数を読み込んでいます。いわゆる「環境変数」の読み込みです。

class LineModel(BaseModel):
    destination: str
    events: List

 LINEプラットフォームからのWebhookイベント(HTTPSでのPOSTリクエスト)内の構造を、あらかじめ定義しておきます。公式のドキュメントによると、destinationとeventsというものが送られてくるみたいです。

@app.post("/linebot")
async def linebot(request: LineModel):
    msg = request.events[0]["message"]["text"]
    replyToken = request.events[0]["replyToken"]
    
    print("ユーザ: %s" % msg)

 Webhookイベントを、先ほど作成したLineModel型として受け取りrequestという変数に格納しています。
 どうやら、request.events[0]["message"]["text"]でユーザのメッセージを、request.events[0]["replyToken"]で応答メッセージの送信に必要なトークンを取得できるみたいです。

sendMsg = "そうなんだ!"
    
sendPostHead = {
    "Authorization": channelAccessToken
}
    
sendPostData = {
    "replyToken": replyToken,
    "messages": [
        {
            "type": "text",
            "text": sendMsg
        }
    ]
}
    
res = requests.post("https://api.line.me/v2/bot/message/reply", headers=sendPostHead, json=sendPostData)

 sendMsgにはユーザに返すメッセージを、sendPostHeadにはPOSTリクエストに必要なヘッダ情報を格納し、requests.post関数で公式で指定されているURLに対してPOSTリクエストを行います。

 これらのコードを記述しgunicornを再起動して、LINE側で「こんにちは」と送信してみるます。以下のようにChatGPTテストから「そうなんだ!」と返信されると成功です。

LINE 会話画面

 これだけでも面白そうなことができそうですが、今回はこれにChatGPTとの接続を行います。次からはChatGPTのAPIを利用するのに必要な設定を行っていきます。

ChatGPT API の設定

 ChatGPT APIを利用するには、OpenAIのアカウントやトークンが必要になります。

OpenAI Product ページ

 上記のリンクからOpenAIの公式サイトにアクセスし「Get started」をクリックしてください。


OpenAI カウント作成画面

 画面の案内に従ってOpenAIのアカウントを作成してください。


OpenAI アカウントメニュー

 アカウントを作成したら、画面右上のアイコンをクリックして「View API keys」をクリックしてください。


APIキー生成画面

 「Create new secret key」をクリックしてAPI keyを生成し、コピーしてください。
 OpenAI公式サイトでの設定は以上です。

 APIサーバ側の設定(プログラミング)に移ります。.envファイルを開いて、先ほどコピーしたAPIキーを以下のように記述してください。

LINE_CHANNEL_ACCESS_TOKEN="Bearer 0123456789abcdefghijklmnopqrstuvwxyz"
OPENAI_API_KEY="0123456789abcdefghijklmnopqrstuvwxyz"

 .envへのAPIキー記述が終わったら、main.pyを以下のように書き換えてください。

from fastapi import FastAPI
from pydantic import BaseModel
from typing import List

import os
import requests
import json

from dotenv import load_dotenv

import openai

load_dotenv()

channelAccessToken = os.environ["LINE_CHANNEL_ACCESS_TOKEN"]
openai.api_key = os.environ["OPENAI_API_KEY"]

class LineModel(BaseModel):
    destination: str
    events: List

app = FastAPI()

@app.get("/")
async def root():
    return {"message": "Hello World"}

@app.post("/linebot")
async def linebot(request: LineModel):
    msg = request.events[0]["message"]["text"]
    replyToken = request.events[0]["replyToken"]
    
    print("ユーザ: %s" % msg)
        
    try:
        response = openai.ChatCompletion.create(
            model = "gpt-3.5-turbo",
            messages = [
                {
                    "role": "system",
                    "content": "話す前に「あっ」と言って、敬語で返してください。話しなれていなく、なるべく短く返答してください。"
                },
                {
                    "role": "user",
                    "content": msg
                }
            ]
        )
        
        sendMsg = response["choices"][0]["message"]["content"]
        
        print("ChatGPT: %s" % sendMsg)
    except Exception as e:
        # こんな適当な例外処理はしないでください!
        print("何かしらのエラー")
        print(e)
    
    sendPostHead = {
        "Authorization": channelAccessToken
    }
    
    sendPostData = {
        "replyToken": replyToken,
        "messages": [
            {
                "type": "text",
                "text": sendMsg
            }
        ]
    }
    
    res = requests.post("https://api.line.me/v2/bot/message/reply", headers=sendPostHead, json=sendPostData)
    
    print("linebotのレスポンス: %s" % res)
    
    return request

 先ほどから変更した部分を詳しく解説します。

import openai

 ChatGPTのAPIと接続する際に必要なライブラリをインポートします。

load_dotenv()

channelAccessToken = os.environ["LINE_CHANNEL_ACCESS_TOKEN"]
openai.api_key = os.environ["OPENAI_API_KEY"]

 LINEのチャネルアクセストークン(長期)と同様に、openai.api_keyにOpen  AIのAPIキーを格納しています。

try:
    response = openai.ChatCompletion.create(
        model = "gpt-3.5-turbo",
        messages = [
            {
                "role": "system",
                "content": "話す前に「あっ」と言って、敬語で返してください。話しなれていなく、なるべく短く返答してください。"
            },
            {
                "role": "user",
                "content": msg
            }
        ]
    )
       
    sendMsg = response["choices"][0]["message"]["content"]
        
    print("ChatGPT: %s" % sendMsg)
except Exception as e:
    # こんな適当な例外処理はしないでください!
    print("何かしらのエラー")
    print(e)

 openai.ChatCompletion.create関数に必要な引数を渡してあげると、URLなどを指定しなくても勝手にChatGPTと接続してくれるみたいです。
 messagesに注目してください。1つ目は"role": "system"を、2つ目は"role": "user"を指定してます。どうやら、このsystemに対して事前に決めごとを記述することでChatGPTからの返答をカスタマイズできるみたいです。

 また、今回は実装していませんが、連続した会話を行うには以下のように過去の会話履歴を渡してあげる必要があるみたいです(未検証)。
 ChatGPTのAPIを利用するとトークンというものがカウントされ、トークンの数により課金がされるみたいです。今回は18$分の無料枠が利用できましたが、以下の例以上に長い会話をすると一瞬で無料枠を使い切りそうな気がします。

{
    "role": "system",
    "content": "元気よく返答して!!"
},
{
    "role": "user",
    "content": "こんにちは"
},
{
    "role": "assistant",
    "content": "やあ!こんにちは!!"
},
{
    "role": "user",
    "content": "1+1は?"
},
{
    "role": "assistant",
    "content": "1+1は2だよ!"
},
{
    "role": "user",
    "content": "それに3を足すと?"
},
{
    "role": "assistant",
    "content": "5だね!!"
},

 gunicornを再起動して、LINE側で会話をしてみます。今回は1問1答になってしまいましたが、以下のように会話が成立すれば成功です。このぼっとちゃんはフェンダーのストラトを使用しているみたいです!!!

LINEでのChatGPTとの会話画面

おわりに

 以上、LINEでChatGPTと会話する方法についての解説でした。公開サーバの構築方法やドメインの取得方法など省略した部分もありますが、この記事を参考にしてLINEBOTとChatGPTをつなげて遊んでみてください。

 また、今回紹介したコードはLINEプラットフォーム以外からのPOSTリクエストを受け取って返答してしまう危険な設計になっています。公式のドキュメントにもあるように、署名を検証する作業が必要になります。
 おまけとして、以下に署名検証機能付きのコードを用意したので、実装の際はこちらを参考にしてください。

LINE_CHANNEL_SECRET
# .env
LINE_CHANNEL_ACCESS_TOKEN="Bearer 123456789"
LINE_CHANNEL_SECRET="123456789"
OPENAI_API_KEY="123456789"
# main.py
from fastapi import FastAPI, Header, Request

import os
import requests
import json

import base64
import hashlib
import hmac

from dotenv import load_dotenv

import openai

load_dotenv()

channelAccessToken = os.environ["LINE_CHANNEL_ACCESS_TOKEN"]
openai.api_key = os.environ["OPENAI_API_KEY"]
channel_secret = os.environ["LINE_CHANNEL_SECRET"]

app = FastAPI()

@app.get("/")
async def root():
    return {"message": "Hello World"}

@app.post("/linebot")
async def linebot(request: Request, x_line_signature: bytes = Header(None)):
    # 署名検証部分
    requestBody = await request.body()
    hash = hmac.new(channel_secret.encode('utf-8'), requestBody, hashlib.sha256).digest()
    signature = base64.b64encode(hash)
    
    # リクエストボディのダイジェスト値と署名が一致すれば
    if x_line_signature == signature:
        requestBody = json.loads(requestBody)
        msg = requestBody["events"][0]["message"]["text"]
        replyToken = requestBody["events"][0]["replyToken"]
        
        print("ユーザ: %s" % msg)
            
        try:
            response = openai.ChatCompletion.create(
                model = "gpt-3.5-turbo",
                messages = [
                    {
                        "role": "system",
                        "content": "話す前に「あっ」と言って、敬語で返してください。話しなれていなく、なるべく短く返答してください。"
                    },
                    {
                        "role": "user",
                        "content": msg
                    }
                ]
            )
            
            sendMsg = response["choices"][0]["message"]["content"]
            
            print("ChatGPT: %s" % sendMsg)
        except Exception as e:
            print("何かしらのエラー")
            print(e)
        
        sendPostHead = {
            "Authorization": channelAccessToken
        }
        
        sendPostData = {
            "replyToken": replyToken,
            "messages": [
                {
                    "type": "text",
                    "text": sendMsg
                }
            ]
        }
        
        res = requests.post("https://api.line.me/v2/bot/message/reply", headers=sendPostHead, json=sendPostData)
        
        print("linebotのレスポンス: %s" % res)
        
    return request

 大きな違いとして、署名検証のためChannel secretを使用したり、LineModelスキーマの使用をやめてリクエストボディをそのまま処理したりしています。

 ほかにも、いわゆるコントローラ部分の記述が多くなってしまったり、例外処理が適当だったり、すべてのコードをmain.pyに書いてしまっていたりと、実運用可能なコードではありませんのであくまでも参考程度にお願いします。見てくださりありがとうございました。

参考


この記事が気に入ったらサポートをしてみませんか?