StreamlitとChatGPTを組み合わせてマルチモーダルなアプリを作る
はじめに
最近、LLMを活用する機会が増えており、簡単にプロンプトを変更してチャットができるアプリが欲しいと考えていました。そこで、Streamlit - Build a basic LLM chat appを参考にアプリを実装し、さらに画像やファイルの対応方法について書きました。
ソースコードの全体は、こちらのGitHubリポジトリで確認できます。
公式の手順を参考に実装
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からレスポンスが返ってくるまで表示されません。
すでにOpenAIが用意してくれてるのでそれを使用して、streamlitに合わせて実装します。
変更する必要があるのは、ユーザーの入力処理の部分のみです。
client.chat.completions.create
をstream=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_input
にaccept_file
とfile_type
を設定します。
accept_file
の初期値はFalse
となっていてファイルを受け付けない状態です。True
にすれば一つのファイルを"multiple"
であれば複数ファイルに対応します。file_type
は受け付けるファイル拡張子を書いてください。
if user_message := st.chat_input(
"送信するメッセージを入力",
accept_file="multiple",
file_type=["png", "jpg", "jpeg"],
):
...
こうすることによって、user_message.text
とuser_message.files
が取得できるようになります。これに合わせて、上手くの現状の処理を変更します。ほかの処理とほとんど一緒です。違いとしては、画像がある場合にbase64にしてstate
に保存しています。
詳しくは、OpenAIのドキュメントを見てください。
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で遊ぶのは楽しいですね。
✌('ω'✌ )三✌('ω')✌三( ✌'ω')✌
Discussion