Gradioでの入力チェックは難しい？ Gradioチャットアプリ ～thenとsuccess～

# Gradioでチャットアプリ

**「Gradio」** 聞いたことない方もいるかと思いますが、機械学習のために作られたPythonの簡易Webアプリ作成ライブラリです。

https://www.gradio.app/

<br>

GPTの[**コードインタプリター**が](https://www.watch.impress.co.jp/docs/topic/1520290.html)使えるこんなのも作れます。（オリジナル作成）

https://huggingface.co/spaces/nekoniii3/GPT_Chat_Code_Interpreter


<br>少ないコードで入出力を伴うアプリが簡単にできるので、最近では生成AIのデモなどに多く利用されています。ちなみに画像生成AI用ツール[**stable-diffusion-webui**](https://github.com/AUTOMATIC1111/stable-diffusion-webui)もGradioでできています。

最近ではLLMに対応するためチャットUIが追加されました。
コードは[こちらの公式サンプルから](https://www.gradio.app/guides/creating-a-custom-chatbot-with-blocks)引用しています。
※Gradioインストール（pip install gradio）がいります。
<br>
```Python:gradio_chat
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()
```

![スクリーンショット 2023-10-29 213416.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3549938/759c98a5-3633-48bc-7ebc-6bd5739468b5.png)

<br>
たったこれだけのコードでチャットアプリが作れるようになっています。（ただサンプルなので回答は"How are you?", "I love you", "I'm very hungry"のどれかしか返ってきません。）

これはすごい便利なのですが、現在のサンプルだとテキストを入力しない場合でもメッセージが送信できてしまいます。GPTのAPIを利用する場合は未入力で呼び出すとエラーとなるので入力チェックを付けたいところです。<br>
そんなこと簡単…<br>
と思いきや、この入力チェックが思いの他めんどうだったので記事にした次第です。

# 簡単なコードの説明

まずざっとこのサンプルについて説明します。<br>（Textboxなど基本的なことは[Gradio入門](https://note.com/npaka/n/nb9d4902f8f4d)などを参照してください。）<br>
"msg.submit"はメッセージ入力用のテキストボックスでEnterを押すと起動されます。
そして[msg, chatbot]2つの引数で関数"user"を呼び出し戻り値も同じく[msg, chatbot]にセットするということを行っています。<br>
関数"user"ではuser_messageを""（初期化）、historyをhistory + [[user_message, None]]にして返しています。historyはチャットの履歴(ユーザメッセージ, アシスタントメッセージ)つまり問いかけと回答のペアを格納していくことで画面にチャット内容の表示が行えます。
（説明ではわかづらいと思います。すいませんが**demo.launch(debug=True)** でPrintが使えるようになるので動かして確認してみてください。）

<br>ということでエラーメッセージを表示するテキストボックスを追加し、userの戻り値にエラーメッセージを追加しhistoryは何もしないで返すことで未入力チェックが行えると思いました。

```diff_python:システムメッセージ用Textbox追加
msg = gr.Textbox()
+ sys_msg = gr.Textbox(label="システムメッセージ")  #追加
clear = gr.Button("Clear")
```

```diff_Python:userの修正後
def user(user_message, history):

    sys_msg = ""

    if user_message == "":

        sys_msg = "テキストが未入力です。"

        return "", history ,sys_msg

    return "", history + [[user_message, None]] ,sys_msg
```


### が

<br>
これはエラーとなります。

`File "<ipython-input-9-ad2fdba3eb44>", line 24, in bot`
`    history[-1][1] = ""`

簡単に言えば次の関数 **"bot"** に行ってしまいエラーとなっています。

# thenとsuccess

先ほどは説明を飛ばしましたが、msg.submitの後に **".then"** というのがあります。
これは前の処理が行われた直後に行う処理を指示することができます。
つまりこの場合、msg.submit(～)の2行分の処理を1つに記述することができます。
```Python:
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は失敗した場合でも、後の処理を実行します。<br>
ですので、さっきのエラーはメッセージがないのに後処理のbotに進んだため発生してしまいました。<br>
### ということは.thenではなく.successを使えば解決！

<br>
とはいきません。なぜならGradioは成功か失敗かをエラーの発生でしか検知しないので、私がエラーメッセージを設定したところで、向こうはそんなのエラーとはわかりません。

ということで、無理やり例外を発生させてエラーにしてみます。

```diff_Python:
def user(user_message, history):

    if user_message == "":
    
        sys_msg = "テキストが未入力です。"
    
+        # 意図的に例外を送付
+        raise Exception()

        return "", history ,sys_msg
```

しかし、これを実行するとこうなります

![スクリーンショット 2023-10-30 210752.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3549938/ecd624fa-013e-d1ae-ec3e-3371f2970698.png)
<br>
Gradioは関数内でエラーが発生した際、その関数のoutputに指定されたコンポーネントは全て画像のような"Error"とります。

これは致命的なエラーならいいですが、今回のような入力チェックでこのような仰々しい表示が出てしまうのは、あまりよいアプリとはいえません。（最近で言うユーザエクスペリエンスですかね）

またそもそも例外が起こるので肝心のエラーメッセージが返せてません。

ということで長くなりましたが、結論として今回はこのような対応を行いました。

```Python:入力チェック追加後のソース
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を利用方法などを紹介しています！

https://www.youtube.com/watch?v=tFmedAM1FM8&list=PLrHCgm9wxXCjT_vZySexearxZutrtG5_b


# 補足
記事の内容とは関係ないですが、関数**bot**について何も触れてないので補足します。
botはhistory（チャット履歴）を受け取りアシスタントの回答をセットします。
GPTやらのAPIなどを利用してここに回答をセットすることになります。
（userでは回答をNoneでセットするのはそのため）


なんで質問と回答を一気にセットしないかというとGradio曰く[**ユーザエクスペリエンスのため**](https://seolaboratory.jp/84435/)とのことで、要はGPTからの回答待ちで質問を打ち込んでから表示されるまで時間がかかるのはユーザに対してよくない。質問した後は質問だけでも画面に表示させて回答を待っているという状態を表示させるべき。


ということで、ユーザから質問を受けたらすぐにhistoryを返してチャット画面に表示。その後にbotの処理を行っています。

その後も**yield**とあまり見ない処理を行っていますが、これはチャットボットらしく文字を１文字ずつ表示する処理を行っています。（time.sleep(0.05)の0.05を大きくすればわかるかと思います）

history[-1][1]が最新のアシスタントからの回答になるので（リストの一番最後）これに回答すべきテキストを１文字ずつセットしている感じです。

サンプルが無かったら絶対に思いつかない実装ですね…