で、結局キャラクターBOTを作るには? ~完成編~ #006
前回、GoogleのCloud Functionsをオウム返しができるLINEBOTを作成しました。
”AI大谷翔平BOTをLINEで作る”を目標に色々とやっております。
→これまでをまとめたマガジンはこちら
いよいよ大詰めです!
あとはこれに#002で作成したGPT3.5のファインチューニングモデルを使って送られてきたメッセージに返答するようにすれば、完成となります。
ただ#003で述べましたようにGPTのAPIは過去のメッセージの履歴も渡してあげないと会話にならないので、ちょっとロジックを組み立てないといけません。
メッセージの保存・取得ロジック
メッセージのデータはFirebaseのFirestore Databaseに保存します。
#003で説明しましたが、これはドキュメント(以下の例だと東京都)とフィールドの項目(例だと人口、広さなど)を指定することで各値を取り出すことができます。
もちろんフィールド・値も更新できるので、メッセージをひたすらここに保存しようと思います。イメージはこんな感じです。
※”OOTANI_BOT”というコレクションは管理画面から事前に作成します
userXXがLINEユーザからのメッセージ、assistantXXがLINEBOTからのメッセージ(GPTの回答)となっています。user1が「身長は?」assistant1が「193㎝です。」となっているのでなんとなくわかって頂けるかと思います。
LINEBOTがメッセージを受け取った後、過去のメッセージの取得→GPTがユーザに回答を送信→メッセージの保存という流れで処理を行います。メッセージの取得・保存は以下のロジックで行います。
過去のメッセージの取得
ユーザIDでドキュメントを開く→なければ終了(新規のメッセージ)
項目"chat_count"を取得(メッセージの往復数)
chat_count分だけuser1~userN、assistant1~assistantNを取得
※ただしトークン制限があるため、過去のメッセージの取得数に上限を付ける
メッセージの保存
今回のやり取りをuserN+1、assistantN+1として保存
chat_countはchat_count+1としupdateに現在の時間を保存
ユーザIDはLINEユーザを識別する値で、送られてきたメッセージから簡単に取得できます。(LINE交換で使うユーザが設定するものとは異なります。)
"chat_count"という項目を作り何往復したかを保存しておき、その数だけお互いのメッセージを取得するロジックです。取得は全てのメッセージですが、保存は今回のやり取りだけです。
updateに現在の時間を保存としていますが、これは過去の履歴を削除するために設定しています。例えば24時間経過したものは削除として、このupdateの時間を元に外部から削除を行います。(削除は運用で行うものとして、今回作るLINEBOT内で削除処理は行いません)
コードを見てもらった方が早いかもしれないので、テスト的にGoogleColabで試せるようにすると、こんな感じとなります。(こちらからのメッセージはinput関数で入力)
import time
import openai
import firebase_admin
from firebase_admin import credentials
from firebase_admin import firestore
from firebase_admin import exceptions
# OpenAIキー・モデル設定
openai.api_key = "ここはOpenAIキー"
model_name = "ここはモデル名"
# Firestoreに接続
cred = credentials.Certificate('test-01.json')
firebase_admin.initialize_app(cred)
db = firestore.client()
# Firestore情報
COLLECTION = "OOTANI_BOT"
document = "USER01"
# 変数設定
CHAT_LIMIT = 10 # 会話の最大保存数(トークン)
response_message = "" # GPTからの返答用
start_count = 1 # 会話履歴を取得するスタート
# システムプロンプト
system_prompt = "あなたはMLBの野球選手として活躍する大谷翔平です。必ず回答は大谷翔平選手として返答してください。"
# メッセージ未入力でまでループ
while True:
# ループ内変数初期化
chat_count = 0 # 会話の往復数
messages = [] # GPTとのやり取りをセットするリスト変数
# systemプロンプトは固定
messages.append({"role": "system", "content": f"{system_prompt}"})
# Firestoreから指定ユーザの会話履歴を取得
doc_ref = db.collection(COLLECTION).document(document)
doc = doc_ref.get()
history = doc.to_dict()
if history is not None:
# 履歴が存在する場合データを取得
chat_count = history["chat_count"]
if chat_count > CHAT_LIMIT :
# 会話数がリミットを超えたら取得するスタートを変える
start_count = chat_count - CHAT_LIMIT + 1
for i in range(chat_count - start_count + 1):
# キー"userXX","assistantXX"からデータを取得
user = history["user" + str(i+start_count)]
assistant = history["assistant" + str(i+start_count)]
# 会話履歴をとしてmessagesにセット
messages.append({"role": "user", "content": f"{user}"})
messages.append({"role": "assistant", "content": f"{assistant}"})
# input関数で入力を促す
prompt = input("質問を入力してください。")
# 未入力で終了
if prompt == "":
break
# 入力されたプロンプトをmessagesに追加
messages.append({"role": "user", "content": f"{prompt}"})
# GPTに問い合わせ
response = openai.ChatCompletion.create(
model = model_name,
messages = messages
)
# 戻り値からメッセージと使用トークン数を取得
response_message = response.choices[0].message.content
# GPTからの回答を返信
print(response_message)
# カウントを増やす
chat_count = chat_count + 1
# 会話履歴を保存
data = {"user" + str(chat_count) : prompt, "assistant" + str(chat_count) : response_message, "chat_count" : chat_count, "update" : int(time.time())}
db.collection(COLLECTION).document(document).set(data,merge=True)
※ファイル(content)直下にFirebase認証情報の"test-01.json"を置きます
何も入力せずEnterを押すまで、会話ができるようになっています。ユーザIDについては今回はダミーとして"USER01"とし、ドキュメントを指定する変数documentにセットしています。
会話の最大数は10とし、これ以上会話が続いた場合は最新のものから取得するようにしています。また更新時間のupdateはUNIX時間というものを使用しています。ぱっと見て日付がわからないのですが、今回はとにかく早く処理を行うことが必要なため、この形式のままにしました。
もちろんこれは我流のロジックであり、会話履歴を保持しっぱなしにする場合は、メッセージを取得に時間がかかると思いますので用途に応じて変えて下さい。
LINEBOT用コード作成
あとは前回のLINEBOTでオウム返しのコードと合体させるだけです。
import os
import time
import openai
import firebase_admin
from firebase_admin import credentials
from firebase_admin import firestore
from firebase_admin import exceptions
from flask import abort, jsonify
from linebot import (
LineBotApi, WebhookParser
)
from linebot.exceptions import (
InvalidSignatureError
)
from linebot.models import (
MessageEvent, TextMessage, TextSendMessage
)
# LINE MessagingAPI設定
channel_secret = os.environ.get("CHANNEL_SECRET")
channel_access_token = os.environ.get("CHANNEL_ACCESS_TOKEN")
# OpenAIキー・モデル設定
openai.api_key = os.environ.get("OPENAI_KEY")
model_name = os.environ.get("GPT_MODEL")
# Firestore接続情報(private_keyは二重¥を戻す)
cert = {
"type":os.environ.get("FB_TYPE"),
"project_id":os.environ.get("FB_PROJECT_ID"),
"private_key":os.environ["FB_PRIVATE_KEY"],
"client_email":os.environ.get("FB_CLIENT_EMAIL"),
"token_uri":os.environ.get("FB_TOKEN_URI")
}
cert["private_key"] = cert["private_key"].replace("\\n","\n")
cred = credentials.Certificate(cert)
firebase_admin.initialize_app(cred)
db = firestore.client()
# Firestore情報
COLLECTION = "OOTANI_BOT"
# 変数設定
CHAT_LIMIT = 10 # 会話の最大保存数(トークン)
# 履歴からGPTへのメッセージを作る関数
def make_messages(prompt, user_id):
messages = [] # 返戻用メッセージリスト
chat_count = 0 # 会話履歴数(往復)
start_count = 1 # 会話履歴を取得するスタート
# システムプロンプト設定
system_prompt = "あなたはMLBで野球選手として活躍する大谷翔平です。質問に対し、大谷翔平選手として回答してください。"
# システムプロンプト追加
messages.append({"role": "system", "content": f"{system_prompt}"})
# Firestoreから指定ユーザの会話履歴を取得
doc_ref = db.collection(COLLECTION).document(user_id)
doc = doc_ref.get()
history = doc.to_dict()
if history is not None:
# 履歴が存在する場合データを取得
chat_count = history["chat_count"]
if chat_count > CHAT_LIMIT :
# 会話数がリミットを超えたら取得するスタートを変える
start_count = chat_count - CHAT_LIMIT + 1
for i in range(chat_count - start_count + 1):
# キー"userXX","assistantXX"からデータを取得
user = history["user" + str(i+start_count)]
assistant = history["assistant" + str(i+start_count)]
# 会話履歴をとしてmessagesにセット
messages.append({"role": "user", "content": f"{user}"})
messages.append({"role": "assistant", "content": f"{assistant}"})
# 最後に入力されたプロンプトをmessagesに追加
messages.append({"role": "user", "content": f"{prompt}"})
return chat_count, messages
# エントリポイントとなる関数
def reply_message(request):
line_bot_api = LineBotApi(channel_access_token)
parser = WebhookParser(channel_secret)
body = request.get_data(as_text=True)
signature = request.headers['X-Line-Signature']
try:
events = parser.parse(body, signature)
except InvalidSignatureError:
print("Invalid signature. Please check your channel access token/channel secret.")
return abort(405)
for event in events:
if not isinstance(event, MessageEvent):
continue
if not isinstance(event.message, TextMessage):
continue
# 送られてきたメッセージと履歴からリスト作成
chat_count, messages = make_messages(event.message.text, event.source.user_id)
print(messages)
# GPTに問い合わせ
response = openai.ChatCompletion.create(
model = model_name,
messages = messages
)
# GPTからの回答をユーザに送り返す
response_message = response.choices[0].message.content
line_bot_api.reply_message(
event.reply_token,
TextSendMessage(text=response_message)
)
# カウントを増やす
chat_count = chat_count + 1
# 会話履歴を保存
data = {"user" + str(chat_count) : event.message.text, "assistant" + str(chat_count) : response_message, "chat_count" : chat_count, "update" : int(time.time())}
db.collection(COLLECTION).document(event.source.user_id).set(data,merge=True)
return jsonify({ 'message': 'ok'})
主な修正点は以下です。
キーは全て環境変数にセット
”make_messages”という関数を作り、GPTに渡すメッセージリストを作成する部分をまとめる
ユーザへの返答をGPTからの回答にする(TextSendMessageの引数textのにセット)
またJSONファイルに入っていたFirebaseのキーは環境変数から取得するようにしています。(詳しく知りたい方は下記を見て下さい。)
Cloud Functionsへのデプロイ
最後の作業となります。あとは先ほどのコードを使ってfunctionを作成するだけです。やり方は基本的に#005のオウム返しと同じです。
今回は環境変数が多いです。
先ほどのコードをコピーして張り付け。
※今回はエントリポイントを”reply_message”としています
requirementsの設定。
OpenAIのパッケージなど以下を設定します。(バージョンは念のため指定しました。利用環境と合わせて下さい。)
flask == 2.3.3
line-bot-sdk == 3.5.0
openai == 0.28.0
firebase_admin == 6.2.0
そしてデプロイします。
エラーにならなければ、OKです。
(エラーになったときはログタブでエラーの内容を確認してください。)
あとはトリガーからURLコピー。
このURLをLINE Developersの画面に設定。
検証してOKなら後は試すだけです!
LINEからメッセージを送ってみてください。
ちょっとおかしいとこもありますが、これで完成です!
レスポンスの早さもまずまずです!
一定期間公開しておきますので、よかったら使ってみて下さい!
(もちろん個人情報など収集はしません。)
ということで長々とだらだらとやってきましたが、これで”AI大谷BOT”完成となります。問題点・費用・気づいたことなどは次回まとめたいと思います。ご覧頂いた方、ありがとうございました!