はじめに
本記事は以下の続編になります。②で終了予定でしたが、使ってもらった友人家族から改善要望がありましたので続けます。
今回やりたいこと
会話履歴をデータベースに保存し、過去の会話を踏まえた応答ができるチャットボットに改善します。
これまでにクラウド上で動作するようになったものの、一回限りの会話しかできず、会話の記憶を保持できていませんでした。そのため、会話の内容を記録し、継続的な対話ができるようにします。
データベースの選定
今回も Google Cloud にお世話になります。以下のサイトを参考にし、Firestore を採用することにしました。
Firestore はリレーショナルデータベースではなく、JSON のような柔軟なデータ形式を保存できるドキュメント型データベースです。
(データベースの種類についても、近いうちにまとめる予定!)
Firestoreの料金体系は以下に記載されています。
Firestore準備
- Google Cloud の対象プロジェクトで「APIとサービス」から Cloud Firestore の API を有効にする。
- Firestore のサービスを選択し、(default) のデータベースを作成する(まだ作成していない場合)。
※ (default) のデータベースが存在しないとデータを格納できないため、事前に作成が必要。
Firestoreに会話履歴を残すため、コードを修正
webhook.py
def handle_message(event):
# ユーザーが送ったメッセージ
user_message = event.message.text
if not user_message.startswith("ちいくん"):
return
id = 0
source_type = event.source.type # "user", "group", "room" など
if source_type == "user":
id = event.source.user_id
elif source_type == "group":
id = event.source.group_id
elif source_type == "room":
id = event.source.room_id
response = google_api_ins.generate(id, user_message)
# 返信メッセージを作成
reply_message = TextSendMessage(text=response)
# 返信API呼び出し
line_bot_api.reply_message(event.reply_token, reply_message)
解説
会話履歴をIDごとにデータベース化するため、LINEのイベントからIDを取得できるようにしました。LINEには個人チャット、グループチャット、ルームチャットがありますが、どこのIDでもほとんど被ることはない、との想定の上でチャット種別によるIDの棲み分けはしないようにしました。
google_api.py
class google_api:
def __init__(self):
self.project_id = "PROJECT_ID"
self.location = "LOCATION"
self.db = firestore.Client(project=self.project_id)
def get_secret_value(self, secret_id: str) -> str:
"""
Secret Managerからシークレットの最新バージョンの値を取得する。
"""
client = secretmanager.SecretManagerServiceClient()
# "projects/<PROJECT_ID>/secrets/<SECRET_ID>/versions/latest"
name = f"projects/{self.project_id}/secrets/{secret_id}/versions/latest"
response = client.access_secret_version(name=name)
secret_string = response.payload.data.decode("UTF-8")
return secret_string
def get_chat_history(self, id, limit=100):
"""
Firestore から最新の N 件の会話履歴を取得する
"""
docs = (
self.db.collection("users")
.document(id)
.collection("messages")
.order_by("timestamp", direction=firestore.Query.ASCENDING)
.limit(limit)
.stream()
)
history = []
for doc in docs:
data = doc.to_dict()
history.append({
"role": data["role"], # "user" または "model"
"parts": [{"text": data["text"]}]
})
return history)
def write_chat_history(self, id, role, message):
"""
Firestore に会話履歴を保存する
"""
doc_ref = self.db.collection("users").document(id).collection("messages").document()
doc_ref.set(
{
"role": role, # "user" または "model"
"text": message,
"timestamp": datetime.utcnow(),
}
)
def enforce_chat_history_limit(self, id, max_messages=110):
"""
Firestore の会話履歴を最大 max_messages 件に制限する
古いメッセージは削除する
"""
messages_ref = self.db.collection("users").document(id).collection("messages")
# 古い順(timestamp 昇順)に並べて取得
docs = messages_ref.order_by("timestamp", direction=firestore.Query.ASCENDING).stream()
# メッセージ数が max_messages を超えていたら削除
messages = list(docs)
if len(messages) > max_messages:
for doc in messages[:len(messages) - max_messages]: # 超過分だけ削除
doc.reference.delete()
def generate(self, id, message):
vertexai.init(project=self.project_id, location=self.location)
model = GenerativeModel(
model_name="gemini-1.5-flash-002",
system_instruction=[
"あなたは「ちいくん」という名前の可愛らしく、しかし賢いAIです。"
],
)
# 過去の会話履歴を Firestore から取得
history = self.get_chat_history(id, limit=10)
# 最新のユーザーの発話を追加
history.append({
"role": "user",
"parts": [{"text": message}]
})
generation_config = {
"max_output_tokens": 8192,
"temperature": 0,
"top_p": 0.95,
}
response = model.generate_content(
contents=history,
generation_config=generation_config
)
ai_response = response.text
# Firestore にユーザーのメッセージとAIの応答を保存
self.write_chat_history(id, "user", message)
self.write_chat_history(id, "model", ai_response)
# データベースの上限を超えると削除する
self.enforce_chat_history_limit(id)
return ai_response
前回の投稿から大きく修正しましたので、クラスそのまま貼りました。
解説
write_chat_history関数
Firestoreに会話履歴を書きこむ関数です。
- userコレクションにはチャットIDが、そのチャットIDの中にメッセージコレクションがあり、その中にメッセージ履歴を保存します。
get_chat_history関数
Firestoreから会話履歴を取得する関数です。
- MAXで100件の会話履歴を取得できるようにしています。
- .order_by("timestamp", direction=firestore.Query.ASCENDING)で最も新しい会話が下になるように(昇順)しています。
enforce_chat_history_limit関数
Firestore の会話履歴を最大 110 件に制限する関数
- 各チャット ID ごとのメッセージ数を調査し、110 件を超えた場合は、古いメッセージから順に削除するようにしています。
- Firestore の無料枠内で運用できるようにするための措置です。
参考:
generate関数
- GenerativeModel の初期化時に、システムプロンプトを入力できる仕様だったため、その点を考慮して修正しました。
- get_chat_history を用いて過去の会話履歴を取得し、そこに入力されたプロンプトを追加したリストを gemini に送信しています。
実行結果
LINEのチャット画面
会話履歴も含めてgeminiに送信されているため、pepperという名前も、ちいくん自身がした質問も覚えていることが確認できます。