業界・業務から探す
導入目的・課題から探す
データ・AIについて学ぶ
News
Hakkyについて
ウェビナーコラム
◆トップ【AI・機械学習】
AI

執筆者:Handbook編集部

Streamlit と Firestore で 会話履歴機能付きの ChatGPT ライクな ChatBot を実装してみた

はじめに

ChatBot を実装する際には、Streamlit という python ライブラリが使われることが多いですが、ChatGPT のように過去の会話履歴を保存しておきたい場合は、会話履歴の管理機能を自前で実装する必要があります。

そこで本記事では、Streamlit で実装した ChatUI に、Firestore を使った会話履歴の管理機能を追加することで、 ChatGPT ライクな ChatBot を実装してみたいと思います。

参考: ChatGPT の UI

Streamlit の概要

Streamlit は、Web アプリケーションを手軽に実装できる python ライブラリです。 シンプルなコードで ChatUI が実装できるため、OpenAI API や LangChain と組み合わせることで、簡単に LLM と連携した ChatBot を実装することができます。

より詳しく知りたい方は、以下の記事を参考にしてください。

Firestore の概要

Firestore は、Google が提供するクラウドベースのデータベースサービスです。リアルタイムでデータを同期しやすい特徴があります。また、階層構造を柔軟に構築可能で、json と同じような形式でデータを保存しておくことができます。

詳細については、以下の記事を参考にしてください。

Firestore のデータモデルについて

Firestore は、以下のデータモデルで構成されています。

コレクション(Collections)

コレクションは、一連のドキュメントを含むデータのまとまりです。 コレクションはルートレベルにあり、その中にドキュメントが格納されます。 例えば、「users」という名前のコレクションがある場合、その中にはユーザーに関するドキュメントが含まれます。

ドキュメント(Documents)

ドキュメントは、キーと値のペアで構成される JSON 形式のデータです。 Firestore のドキュメントはコレクション内に保存され、ドキュメント ID を持ちます。このドキュメントの ID は指定することができますが、指定しない場合は、自動で生成されます。 また、ドキュメント内のデータはネストすることができ、フィールド内にさらにコレクションを含めることができます。

フィールド(Fields)

フィールドは、ドキュメント内の key と value のペアを指します。 例えば、"role": "user"のようなペアです。

Firestore の構造について

今回はチャット履歴を保存するにあたり、以下のような構造にしました。 usersコレクション内の user を表すドキュメントに関してはユーザー名を ID として指定し、作成しました。一方で、chatsコレクションやmessagesコレクション内のドキュメントについては ID を自動生成しています。

users (collection)
  "user1" (document)
  	chats (collection)
	  ct4iRoVwrDXfppBEjacE (document)
	    title: OpenAIの概要
	    created: "2023/11/26 0:39:41.659"
 	    messages (collection)
	  	  3IzNM5gOSNrG1pQ8Ikjk (document)
		    "role": "user"
		    "contents": "OpenAIについて教えてください"
		    "timestamp": "2023/11/26 0:40:11.826"
	  	  G85IF8Ux6pufutr86oIJ (document)
		    "role": "assistant"
            "contents": "はい、もちろんです! ..."
            "timestamp": "2023/11/26 0:40:19.437"
		  ...
	  B9l2FoOtVtEcakU570GE (document)
	    ...

  "user2"
    ...

データがどのように格納されているかについては、コンソールからも確認することができます。

実装例の紹介

以下に実装例を示します。基本的なコード構成についてはStreamlit の ChatUI 機能を使った簡単な実装例を参考にしていただければと思います。

import os

from dotenv import load_dotenv
import openai
import streamlit as st
from google.cloud import firestore

load_dotenv()
openai.api_key = os.environ["OPENAI_API_KEY"]
MODEL_NAME = os.environ["MODEL_NAME"]
MODEL_TEMPERATURE = os.environ["MODEL_TEMPERATURE"]

NEW_CHAT_TITLE = "New Chat"
CHATBOT_USER = "hakky"
GCP_PROJECT = "YOUR_GCP_PROJECT"

def create_new_chat():
    st.session_state.displayed_chat_title = NEW_CHAT_TITLE
    st.session_state.displayed_chat_messages = []


def change_displayed_chat(chat_doc):
    # Update titles
    st.session_state.titles = [
        doc.to_dict()["title"] for doc in st.session_state.chats_ref.order_by("created").stream()
    ]

    st.session_state.displayed_chat_ref = chat_doc.reference
    st.session_state.displayed_chat_title = chat_doc.to_dict()["title"]
    st.session_state.displayed_chat_messages = [
        msg.to_dict()
        for msg in chat_doc.reference.collection("messages").order_by("timestamp").stream()
    ]


def run():

    if "user" not in st.session_state:
        st.session_state.user = CHATBOT_USER

    if "chats_ref" not in st.session_state:
        db = firestore.Client(project=GCP_PROJECT)
        user_ref = db.collection("users").document(st.session_state.user)
        st.session_state.chats_ref = user_ref.collection("chats")

    if "titles" not in st.session_state:
        st.session_state.titles = [
                doc.to_dict()["title"]
                for doc in st.session_state.chats_ref.order_by("created").stream()
                ]

    if "displayed_chat_ref" not in st.session_state:
        st.session_state.displayed_chat_ref = None

    if "displayed_chat_title" not in st.session_state:
        st.session_state.displayed_chat_title = "New Chat"

    if "displayed_chat_messages" not in st.session_state:
        st.session_state.displayed_chat_messages = []

    # Sidebar
    with st.sidebar:
        new_chat_disable = st.session_state.displayed_chat_title == NEW_CHAT_TITLE
        st.button("新しい会話を始める", on_click=create_new_chat, disabled=new_chat_disable, type="primary")
        st.title("過去の会話履歴")
        for doc in st.session_state.chats_ref.order_by("created").stream():
            data = doc.to_dict()
            st.button(data["title"], on_click=change_displayed_chat, args=(doc, ))

    # Display messages
    for message in st.session_state.displayed_chat_messages:
        with st.chat_message(message["role"]):
            st.markdown(message["content"])

    if user_input_text := st.chat_input("質問を入力してください"):

        # User
        with st.chat_message("user"):
            st.markdown(user_input_text)

        # Process first message
        if len(st.session_state.displayed_chat_messages) == 0:

            # Create new chat title
            chat_title_prompt = f"""
            ChatBotとの会話を開始するためにユーザーが入力した文を与えるので、その内容を要約し会話のタイトルを考えてもらいます。
            出力は、会話のタイトルのみにしてください。
            ユーザーの入力文: {user_input_text} """

            response = openai.ChatCompletion.create(
                model=MODEL_NAME,
                messages=[{'role': 'system', 'content': chat_title_prompt}]
            )
            st.session_state.displayed_chat_title = response['choices'][0]['message']['content']

            # Create new chat on firestore
            _, st.session_state.displayed_chat_ref = st.session_state.chats_ref.add(
                {
                'title': st.session_state.displayed_chat_title,
                'created': firestore.SERVER_TIMESTAMP,
                }
            )

        user_input_data = {
            "role": "user",
            "content": user_input_text,
            "timestamp": firestore.SERVER_TIMESTAMP
        }
        st.session_state.displayed_chat_messages.append(user_input_data)
        st.session_state.displayed_chat_ref.collection("messages").add(user_input_data)

        with st.spinner("回答を生成中です..."):
            # Generate llm response
            response = openai.ChatCompletion.create(
                model=MODEL_NAME,
                messages=[
                    {"role":data["role"], "content":data["content"]}
                    for data in st.session_state.displayed_chat_messages
                ]
            )
            assistant_output_text = response['choices'][0]['message']['content']

            # Assistant
            with st.chat_message("assistant"):
                st.markdown(assistant_output_text)
            assistant_output_data = {
                "role": "assistant",
                "content": assistant_output_text,
                "timestamp": firestore.SERVER_TIMESTAMP
            }
            st.session_state.displayed_chat_messages.append(assistant_output_data)
            st.session_state.displayed_chat_ref.collection("messages").add(assistant_output_data)


if __name__ == "__main__":
    run()

起動方法

以下のコマンドで起動できます。

streamlit run <path/to/file>

起動するとこのような画面になります。

会話タイトルについて

最初のメッセージを送信すると、そのメッセージを要約することで、チャットのタイトルが決定されます。 そのタイトルと用いて、firestore 上でchatsコレクション内に新規のchatドキュメンントが作成されます。

表示中のチャットについて

表示しているチャットの内容は以下のオブジェクトで管理されます。

  • st.session_state.displayed_chat_ref: 表示中のチャットの chatドキュメントへの参照
  • st.session_state.displayed_chat_title: 表示中のチャットのタイトル
  • st.session_state.displayed_chat_messages: 表示中のチャットのメッセージリスト これらのオブジェクトの内容を、関数change_displayed_chatcreate_new_chatで更新することで、表示するチャットを変更しています。

表示するチャットの切り替え方法

サイドバーに設置されたボタンを押すと、そのチャットの履歴が読み込まれて表示されます。 また"新しい会話を始める"ボタンを押すと、新規のチャットを開始できます。 表示中のチャットが新規チャットである間は、新規チャットを新しく開始することはできません。

まとめ

本記事では、Streamlit と Firestore を組み合わせて、ChatGPT ライクな ChatBot を手軽に実装する方法を解説しました。 Streamlit を使用することで ChatUI を容易に実装することができ、Firestore をデータベースとして利用することでユーザーごとの会話履歴を管理できます。 ChatBot アプリを構築する際にはぜひ参考にしてみてください。

参考

info
備考

Hakky ではエンジニアを募集中です!まずは話してみたいなどでも構いませんので、ぜひお気軽に採用ページからお問い合わせくださいませ。

2025年06月15日に最終更新
読み込み中...