Gradioでチャットアプリ
「Gradio」 聞いたことない方もいるかと思いますが、機械学習のために作られたPythonの簡易Webアプリ作成ライブラリです。
GPTのコードインタプリターが使えるこんなのも作れます。(オリジナル作成)
少ないコードで入出力を伴うアプリが簡単にできるので、最近では生成AIのデモなどに多く利用されています。ちなみに画像生成AI用ツールstable-diffusion-webuiもGradioでできています。
最近ではLLMに対応するためチャットUIが追加されました。
コードはこちらの公式サンプルから引用しています。
※Gradioインストール(pip install gradio)がいります。
import gradio as gr
import random
import time
with gr.Blocks() as demo:
chatbot = gr.Chatbot()
msg = gr.Textbox()
clear = gr.Button("Clear")
def user(user_message, history):
return "", history + [[user_message, None]]
def bot(history):
bot_message = random.choice(["How are you?", "I love you", "I'm very hungry"])
history[-1][1] = ""
for character in bot_message:
history[-1][1] += character
time.sleep(0.05)
yield history
msg.submit(user, [msg, chatbot], [msg, chatbot], queue=False).then(
bot, chatbot, chatbot
)
clear.click(lambda: None, None, chatbot, queue=False)
demo.queue()
demo.launch()
たったこれだけのコードでチャットアプリが作れるようになっています。(ただサンプルなので回答は"How are you?", "I love you", "I'm very hungry"のどれかしか返ってきません。)
これはすごい便利なのですが、現在のサンプルだとテキストを入力しない場合でもメッセージが送信できてしまいます。GPTのAPIを利用する場合は未入力で呼び出すとエラーとなるので入力チェックを付けたいところです。
そんなこと簡単…
と思いきや、この入力チェックが思いの他めんどうだったので記事にした次第です。
簡単なコードの説明
まずざっとこのサンプルについて説明します。
(Textboxなど基本的なことはGradio入門などを参照してください。)
"msg.submit"はメッセージ入力用のテキストボックスでEnterを押すと起動されます。
そして[msg, chatbot]2つの引数で関数"user"を呼び出し戻り値も同じく[msg, chatbot]にセットするということを行っています。
関数"user"ではuser_messageを""(初期化)、historyをhistory + [[user_message, None]]にして返しています。historyはチャットの履歴(ユーザメッセージ, アシスタントメッセージ)つまり問いかけと回答のペアを格納していくことで画面にチャット内容の表示が行えます。
(説明ではわかづらいと思います。すいませんがdemo.launch(debug=True) でPrintが使えるようになるので動かして確認してみてください。)
ということでエラーメッセージを表示するテキストボックスを追加し、userの戻り値にエラーメッセージを追加しhistoryは何もしないで返すことで未入力チェックが行えると思いました。
msg = gr.Textbox()
+ sys_msg = gr.Textbox(label="システムメッセージ") #追加
clear = gr.Button("Clear")
def user(user_message, history):
sys_msg = ""
if user_message == "":
sys_msg = "テキストが未入力です。"
return "", history ,sys_msg
return "", history + [[user_message, None]] ,sys_msg
が
これはエラーとなります。
File "<ipython-input-9-ad2fdba3eb44>", line 24, in bot
history[-1][1] = ""
簡単に言えば次の関数 "bot" に行ってしまいエラーとなっています。
thenとsuccess
先ほどは説明を飛ばしましたが、msg.submitの後に ".then" というのがあります。
これは前の処理が行われた直後に行う処理を指示することができます。
つまりこの場合、msg.submit(~)の2行分の処理を1つに記述することができます。
msg.submit(user, [msg, chatbot], [msg, chatbot], queue=False).then(
bot, chatbot, chatbot
)
# ↓ほぼ同じ
msg.submit(user, [msg, chatbot], [msg, chatbot], queue=False)
msg.submit(bot, chatbot, chatbot)
.thenに対し .success もあり、こちらは前の処理が成功した場合にだけ後の処理を実行します。.thenは失敗した場合でも、後の処理を実行します。
ですので、さっきのエラーはメッセージがないのに後処理のbotに進んだため発生してしまいました。
ということは.thenではなく.successを使えば解決!
とはいきません。なぜならGradioは成功か失敗かをエラーの発生でしか検知しないので、私がエラーメッセージを設定したところで、向こうはそんなのエラーとはわかりません。
ということで、無理やり例外を発生させてエラーにしてみます。
def user(user_message, history):
if user_message == "":
sys_msg = "テキストが未入力です。"
+ # 意図的に例外を送付
+ raise Exception()
return "", history ,sys_msg
しかし、これを実行するとこうなります
Gradioは関数内でエラーが発生した際、その関数のoutputに指定されたコンポーネントは全て画像のような"Error"とります。
これは致命的なエラーならいいですが、今回のような入力チェックでこのような仰々しい表示が出てしまうのは、あまりよいアプリとはいえません。(最近で言うユーザエクスペリエンスですかね)
またそもそも例外が起こるので肝心のエラーメッセージが返せてません。
ということで長くなりましたが、結論として今回はこのような対応を行いました。
import gradio as gr
import random
import time
with gr.Blocks() as demo:
chatbot = gr.Chatbot()
msg = gr.Textbox()
sys_msg = gr.Textbox(label="システムメッセージ") #追加
clear = gr.Button("Clear")
def user(user_message, history):
sys_msg = ""
if user_message == "":
sys_msg = "テキストが未入力です。"
return "", history ,sys_msg
return "", history + [[user_message, None]] ,sys_msg
def bot(history):
bot_message = random.choice(["How are you?", "I love you", "I'm very hungry"])
history[-1][1] = ""
for character in bot_message:
history[-1][1] += character
time.sleep(0.05)
yield history
def raise_exception(err_msg):
if err_msg:
# エラーメッセージがあるときは例外を起こしエラーに
raise Exception()
return
msg.submit(user, [msg, chatbot], [msg, chatbot, sys_msg], queue=False).then(
raise_exception, sys_msg, None).success(
bot, chatbot, chatbot
)
clear.click(lambda: None, None, chatbot, queue=False)
demo.queue()
demo.launch()
raise_exception という関数を作り、エラーメッセージ(システムメッセージ)があるときは例外を起こします。次の関数botへはsuccessで繋げているので、エラー時は進まずにシステムメッセージを表示するだけで処理を終了させることができます。outputもないので仰々しい表示もでません。
※デバッグ出力するとエラーとはなりますが、動作には問題ないです
おわりに
恐らく、期待されていたより面倒なことをしているかと思います。
(もっと楽なやり方あれば教えてください…)
「例外を起こさず、意図的に失敗フラグを立ててsuccessを通さない。」
なんてことが出来ればいいんですがね。
Gradioは難しいことが簡単にできて、簡単なことが難しい
気がするツンデレライブラリです。
今後もGradio関連の投稿していきたい思います!
宣伝
YouTubeチャンネルでGPTのAPIを利用方法などを紹介しています!
補足
記事の内容とは関係ないですが、関数botについて何も触れてないので補足します。
botはhistory(チャット履歴)を受け取りアシスタントの回答をセットします。
GPTやらのAPIなどを利用してここに回答をセットすることになります。
(userでは回答をNoneでセットするのはそのため)
なんで質問と回答を一気にセットしないかというとGradio曰くユーザエクスペリエンスのためとのことで、要はGPTからの回答待ちで質問を打ち込んでから表示されるまで時間がかかるのはユーザに対してよくない。質問した後は質問だけでも画面に表示させて回答を待っているという状態を表示させるべき。
ということで、ユーザから質問を受けたらすぐにhistoryを返してチャット画面に表示。その後にbotの処理を行っています。
その後もyieldとあまり見ない処理を行っていますが、これはチャットボットらしく文字を1文字ずつ表示する処理を行っています。(time.sleep(0.05)の0.05を大きくすればわかるかと思います)
history[-1][1]が最新のアシスタントからの回答になるので(リストの一番最後)これに回答すべきテキストを1文字ずつセットしている感じです。
サンプルが無かったら絶対に思いつかない実装ですね…