🤖

StreamlitとChatGPTを組み合わせてマルチモーダルなアプリを作る

2025/01/05に公開

はじめに

最近、LLMを活用する機会が増えており、簡単にプロンプトを変更してチャットができるアプリが欲しいと考えていました。そこで、Streamlit - Build a basic LLM chat appを参考にアプリを実装し、さらに画像やファイルの対応方法について書きました。

ソースコードの全体は、こちらのGitHubリポジトリで確認できます。

https://docs.streamlit.io/develop/tutorials/llms/build-conversational-apps
https://github.com/HEKUCHAN/streamlit_chatgpt_simple_interface

公式の手順を参考に実装

Streamlit - Build a basic LLM chat appのサンプルコードを見ながら実装していきます。

初期準備

アプリの環境変数を設定し、それを読み込んでOpenAIクライアントを作成します。

settings.py - 内容

pydantic-settingsを使用して、初期値の設定や環境変数の読み込みを行っています。

from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    openai_api_key: str
    openai_model: str = "gpt-4o-mini"
    title: str = f"Chatbot - {openai_model}"

    model_config = SettingsConfigDict(
        env_file=".env",
        env_file_encoding="utf-8",
    )

import streamlit as st
from openai import OpenAI

from settings import Settings


settings = Settings()
client = OpenAI(
    api_key=settings.openai_api_key
)

st.title(settings.title)
...

メッセージを保持するためのstateを初期化

st.session.stateが未作成の場合は、初期化を行います。

...

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

...

stateに保持されているメッセージをそれぞれ表示

st.session.stateにメッセージがすでに保持されていれば、それぞれのメッセージをst.chat_messageを使用することによってchatアプリのようなUIでメッセージを表示できます。

このコンポネントは、グラフ、表、画像なども表示できます。

import streamlit as st

with st.chat_message("user"):
    st.write("こんにちは!")

with st.chat_message("assistant"):
    st.write("こんにちは!😊 今日はどう過ごされていますか?")

ドキュメントを見ていただいたらわかりやすいと思います。

実際にアプリにstateを取得させて、表示させます。message["role"]には、"user""assistant"のだれかしか現状はないのでそのまま表示の判定に使用します。

...

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

...

ユーザーの入力処理

ユーザーの入力をst.chat_inputを使用します。これを使用するとページの下部に入力欄が固定されます。ユーザーから渡された値がpromptに格納されるので、それをstateに追加して保持できるようにしたのちに、先ほど使用したst.chat_messageで表示します。

if prompt := st.chat_input("送信するメッセージを入力"):
    st.session_state["messages"].append({"role": "user", "content": prompt})
    with st.chat_message("user"):
        st.markdown(prompt)

...

メッセージを生成処理

先ほどのコードに、OpenAIのAPIをたたく処理を書いたのにち、stateに保存する処理を書きます。
最終的には、ユーザーメッセージと同様に、st.chat_messageを使用して表示します。

...

if prompt := st.chat_input("送信するメッセージを入力"):
    st.session_state["messages"].append({"role": "user", "content": prompt})
    with st.chat_message("user"):
        st.markdown(prompt)

    response = client.chat.completions.create(
        messages=st.session_state["messages"],
        model=settings.openai_model,
    )

    assistant_message = response.choices[0].message.content
    st.session_state["messages"].append(
        {"role": "assistant", "content": assistant_message}
    )
    with st.chat_message("assistant"):
        st.markdown(assistant_message)
現状のmain.py
import streamlit as st
from openai import OpenAI

from settings import Settings

settings = Settings()
client = OpenAI(
    api_key=settings.openai_api_key
)

st.title(settings.title)

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

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

if prompt := st.chat_input("送信するメッセージを入力"):
    st.session_state["messages"].append({"role": "user", "content": prompt})
    with st.chat_message("user"):
        st.markdown(prompt)

    response = client.chat.completions.create(
        messages=st.session_state["messages"],
        model=settings.openai_model,
    )

    assistant_message = response.choices[0].message.content
    st.session_state["messages"].append(
        {"role": "assistant", "content": assistant_message}
    )
    with st.chat_message("assistant"):
        st.markdown(assistant_message)

ストリーミングを使用してリアルタイム感を出したい!

大体のLLMがリアルタイムで生成していることをわかりやすくさせるために、ストリーミングで変更を少しずつ反映させてますよね。現状のアプリだと、APIからレスポンスが返ってくるまで表示されません。

https://x.com/i/status/1875578871673974816

すでにOpenAIが用意してくれてるのでそれを使用して、streamlitに合わせて実装します。
変更する必要があるのは、ユーザーの入力処理の部分のみです。

client.chat.completions.createstream=Trueにすることで、ストリーミング機能が使えます。APIから受け取ったチャンクを順次に、full_responseに追加して表示させていくシンプルな処理です。更新するように変更しただけで、stateの扱いはストリーミングを使用していなかった時と一緒になります。

if user_message := st.chat_input("送信するメッセージを入力"):
    with st.chat_message("user"):
        st.markdown(user_message)

    with st.chat_message("assistant"):
        st.session_state.messages.append({"role": "user", "content": user_message})
        message_placeholder = st.empty()
        message_placeholder.markdown("AIが考え中...")

        full_response = ""

        stream = client.chat.completions.create(
            messages=st.session_state["messages"],
            model=settings.openai_model,
            stream=True,
        )

        for chunk in stream:
            full_response += chunk.choices[0].delta.content or ""
            message_placeholder.markdown(full_response)

    st.session_state["messages"].append(
        {"role": "assistant", "content": full_response}
    )

画像やファイルをアップロードできるようにしたい!

個人的に画像をアップロードして、その画像に合わせた返信をしてほしいので、その機能を作成します。

基本的にメッセージの表示や、stateは先ほどと一緒で変更せずに、ユーザーの入力処理を変更します。st.chat_inputaccept_filefile_typeを設定します。

accept_fileの初期値はFalseとなっていてファイルを受け付けない状態です。Trueにすれば一つのファイルを"multiple"であれば複数ファイルに対応します。file_typeは受け付けるファイル拡張子を書いてください。

if user_message := st.chat_input(
    "送信するメッセージを入力",
    accept_file="multiple",
    file_type=["png", "jpg", "jpeg"],
):
    ...

こうすることによって、user_message.textuser_message.filesが取得できるようになります。これに合わせて、上手くの現状の処理を変更します。ほかの処理とほとんど一緒です。違いとしては、画像がある場合にbase64にしてstateに保存しています。

詳しくは、OpenAIのドキュメントを見てください。
https://platform.openai.com/docs/guides/vision

if user_message := st.chat_input(
    "送信するメッセージを入力",
    accept_file="multiple",
    file_type=["png", "jpg", "jpeg"],
):
    with st.chat_message("user"):
        st.markdown(user_message.text)

        if user_message.files:
            for file in user_message.files:
                st.image(file)

    with st.chat_message("assistant"):
        st.session_state.messages.append({"role": "user", "content": user_message.text})
        message_placeholder = st.empty()
        message_placeholder.markdown("AIが考え中...")

        full_response = ""

        if user_message.files:
            message = {
                "role": "user",
                "content": [{"type": "text", "text": user_message.text}],
            }
            for file in user_message.files:
                file_bytes = file.read()
                file_encoded_base64 = base64.b64encode(file_bytes).decode("utf-8")
                message["content"].append(
                    {
                        "type": "image_url",
                        "image_url": {"url": f"data:{file.type};base64,{file_encoded_base64}"},
                    }
                )

            st.session_state.messages.append(message)
        else:
            st.session_state.messages.append(
                {"role": "user", "content": user_message.text}
            )

        stream = client.chat.completions.create(
            messages=st.session_state["messages"],
            model=settings.openai_model,
            stream=True,
        )

        for chunk in stream:
            full_response += chunk.choices[0].delta.content or ""
            message_placeholder.markdown(full_response)

    st.session_state["messages"].append({"role": "assistant", "content": full_response})

最後に

久しぶりに記事を書きました。最近は、streamlitに触っていなかったのでドキュメントを読み進めながら思い出して実装しました。

もし何か、改善点やほかの案についての情報があれば教えてください!
LLMで遊ぶのは楽しいですね。
✌('ω'✌ )三✌('ω')✌三( ✌'ω')✌

Linux Club - 東京工科大学

Discussion