1年経ったらgradioのAPIが変っていた

# はじめに

自宅でAI関連のコードを実行できる環境を準備して遊んでいるのですが、基本的に週末しか使わないので時間が経ってみたらこの前まで実行できてたコードが実行できなくなっていました。

調べてみると参考にしていた記事が公開されてからgradioのバージョンがv3からv5まで更新されているなど、システム側の変更点が大きいようでした。

2024年に作成された記事を検索してもgradio v3の環境を利用している例もあるようですが、ダイナミックに変化していく分野なのでできるだけ最新版を使おうと作業した時のメモを残しておきます。

# 参考資料

最初に環境を構築する時、参考にしていた資料です。

https://note.com/bakushu/n/n9b7b044655f6

ローカル環境で、何かを学習(ファインチューニング)させた特定用途向けのChatBotを作ってみたいと思って、その準備として動作させています。


この他には公式サイトの情報などをみています。

https://www.gradio.app/docs/gradio/chatinterface

https://github.com/gradio-app/gradio/issues/9463

Gradio 3から5に変更されているので、中間のGradio 4の変更点も必要に応じて参照しています。

https://github.com/gradio-app/gradio/issues/6339

# 環境

元々動作させていた環境からはアップグレードして次のような環境になっています。

* Ubuntu 24.04.1 amd64
* Python 3.12.3 with venv (Ubuntu 24.04の標準パッケージ)

``nvidia-smi``コマンドの出力は次のとおりです。

```text:
Sun Oct 27 17:32:37 2024       
+-----------------------------------------------------------------------------------------+
| NVIDIA-SMI 560.35.03              Driver Version: 560.35.03      CUDA Version: 12.6     |
|-----------------------------------------+------------------------+----------------------+
| GPU  Name                 Persistence-M | Bus-Id          Disp.A | Volatile Uncorr. ECC |
| Fan  Temp   Perf          Pwr:Usage/Cap |           Memory-Usage | GPU-Util  Compute M. |
|                                         |                        |               MIG M. |
|=========================================+========================+======================|
|   0  NVIDIA RTX A4000               Off |   00000000:08:00.0  On |                    0 |
| 41%   48C    P8             20W /  140W |    1621MiB /  15352MiB |     31%      Default |
|                                         |                        |                  N/A |
+-----------------------------------------+------------------------+----------------------+
```

Ubuntuのアップグレードなどで古いCUDA Toolkitも導入されていた形跡(/usr/local/cuda-11.7)はあるのですが、動作していなかったので最新のToolkit(12.5)を導入しています。

# LLAMAモデルの変更

GGUF形式だったら何でも良いのかと思ったら次のようなエラーが表示されてしまいました。

```text:エラーメッセージ
gguf_init_from_file: GGUFv1 is no longer supported. please use a more up-to-date version
llama_model_load: error loading model: llama_model_loader: failed to load model from ggml-model-q4_m.gguf
```

元々利用していたのはなんとなく選択した``ggml-model-q4_m.gguf``でしたが、GGUFにもフォーマット違いがあるとこの時に知りました。

このタイミングでELYZAが配布している最新の``Llama-3-ELYZA-JP-8B-q4_k_m.gguf``をダウンロードしました。

https://huggingface.co/elyza/Llama-3-ELYZA-JP-8B-GGUF

ollamaに認識させるために、Modelfileを準備する際に次の記事も参考にしています。

https://qiita.com/s3kzk/items/3cebb8d306fb46cabe9f

# 準備作業

ライブラリなどをvenvの環境に導入していきます。

```bash:
$ python3 -m venv venv/llama
$ . venv/llama/bin/activate
```

最終的に次のようなrequirements.txtを利用しています。

```python:requirements.txt
## for RAG
langchain_community
langchain-core

## for http client
requests

## for HTML Splitter
lxml

## for RAG embedding
uuid
langchain-chroma
langchain-ollama
```

ライブラリをインストールします。

```bash:
$ . venv/llama/bin/activate
(llama) $ pip install -f requirements.txt
```

# コードの変更

実行しながらエラーに対応していきます。

## 1. retry_btn などの仕様変更

参考にしていたコードをそのまま実行すると次のようなメッセージが表示されます。

```bash:python3 chat.py実行時のエラーメッセージ(1回目)
(llama) $ python3 chat.py
...
TypeError: ChatInterface.__init__() got an unexpected keyword argument 'retry_btn'
```

[Gradio ChatInterface Docs](https://www.gradio.app/docs/gradio/chatinterface)をみると、そもそも ここで利用していない``submit_btn``と``stop_btn``を除き、``retry_btn``を含めた``*_btn``に関する変数自体が存在しないようです。

[Migrating to Gradio 5](https://github.com/gradio-app/gradio/issues/9463)では、ボタンのUIは``gr.Chatbot``に統合されたとのことなので、これらの部分はとりあえず削除して実行していきます。

## 2. concurrencty_countが存在しない

```bash:python3 chat.py実行時のエラーメッセージ(2回目)
TypeError: Blocks.queue() got an unexpected keyword argument 'concurrency_count'
```

これも[Migrating to Gradio 5](https://github.com/gradio-app/gradio/issues/9463)を確認すると、Gradio 4で既にdeprecated扱いだったので削除したという内容が確認できるので、[Breaking Changes in Gradio 4.0](https://github.com/gradio-app/gradio/issues/6339)を確認します。

コード上はqueue()のパラメータとしてconcurrency_count=1が指定されているのですが、デフォルト値のようです。``concurrency_limit``に変更されてもデフォルトはそのままだということなので、queue()の引数から削除しています。

## 3. 初期メッセージをクリックするとerrorになる

一応ここまでの変更でGradioが起動して、対話的に動作するようになるのですが、examplesに指定している``日本の四国にある県名を挙げてください。``というプロンプトをクリックするとエラーになります。

Inputダイアログに入力すれば正しく回答が表示されますが、クリックしてエラーになるというのも気持ちが悪いので修正しておきます。

```bash:python3 chat.py実行時のエラー(3回目)
ValueError: A  function (example_clicked) didn't return enough output values (needed: 2, returned: 1).
    Output components:
        [chatbot, state]
    Output values returned:
        [('日本の四国にある県名を挙げてください。', '以下は日本の四国地方にある県名です。\n\n1. 愛媛県\n2. 高知県\n3. 香川県\n4. 徳島県\n\n以上、四国の4県を挙げました。')]
```

この問題は簡単に解決できるのかと思ったのですが、結構面倒でした。

## デフォルトのtype="tuples"はdeprecated

まず実行時に次のようなメッセージが表示されます。

```text:
.../venv/llama/lib/python3.12/site-packages/gradio/components/chatbot.py:229: UserWarning: The 'tuples' format for chatbot messages is deprecated and will be removed in a future version of Gradio. Please set type='messages' instead, which uses openai-style 'role' and 'content' keys.
```

このためtype="messages"をChatInterface()の引数に指定して最終的には解決を目指します。

## cache_examples=Trueがエラーを引き起す

とりあえずtype="tuples"のまま調査すると、cache_examples=Trueが次のようなエラーメッセージを引き起します。

```text:
FileNotFoundError: [Errno 2] No such file or directory: '.gradio/cached_examples/10/log.csv'
```

``cache_examples=False``を指定すると、問題なく動作するようになりました。

ひとまずはここまでで満足することにしました。

## type="messages" で動作させる

さて、最終的には type="messages" を指定して動作させたいので、動作するように変更していきます。

主な変更点は、``generate_text()``で参照している``history``リストの中身が、``{"role": "...", "content": "..."}``のような辞書型(dict)データ形式になるので``for interaction in history:``で参照しているinteractionオブジェクトの参照が``input_prompt = input_prompt + "\nUSER: " + str(interaction["role"]) + "\nASSISTANT: " + str(interaction["content"])``のように変更になります。

また関数の最後で実行しているhistoryリストへの代入が、``history.append({"role": "user", "content": input_prompt})``のような形になります。

``cache_examples=False``であれば、ここまでで動作するようになります。

## cache_examples=Trueにすると動作しない

ここから``cache_examples=True``にすると動作しなくなります。

```text:
...
 File ".../venv/llama/lib/python3.12/site-packages/gradio/chat_interface.py", line 678, in example_clicked
    return self.examples_handler.load_from_cache(x.index)[0].root
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...
  Input should be a valid dictionary or instance of Message [type=model_type, input_value=['日本の四国にあ...方にあ
  ます。'], input_type=list]
    For further information visit https://errors.pydantic.dev/2.9/v/model_type

```

cacheから読み込まれるデータ形式が正しくなさそうなので、内容を確認します。

```python:.gradio/cached_examples/10/log.csv
Chatbot,timestamp
"[[""\u65e5\u672c\u306e\u56db\u56fd\u306b\u3042\u308b\u770c\u540d\u3092\u6319\u3052\u3066\u304f\u3060\u3055\u3044\u3002"", ""\u4ee5\u4e0b\u306f\u56db\u56fd\u5730\u65b9\u306e\u770c\u540d\u3067\u3059\u3002\n\n\u611b\u5a9b\u770c\u3001\u9999\u5ddd\u770c\u3001\u9ad8\u77e5\u770c\u3001\u5fb3\u5cf6\u770c\n\n\u4ee5\u4e0a4\u770c\u304c\u56db\u56fd\u5730\u65b9\u306b\u3042\u308a\u307e\u3059\u3002""]]",2024-10-28 06:58:59.567660
```
内容は質問のリスト形式になっているので、``type="messages"``で期待する``{"role": "...", "content": "..."}``形式にはなっていないようです。

cacheディレクトリ(.gradio/cached_examples/10)を削除すると、エラーメッセージが少し変化します。

```python:
  File "/home/yasu/project/llama_chatbot/venv/llama/lib/python3.12/site-packages/gradio/components/chatbot.py", line 323, in _check_format
    raise Error(
gradio.exceptions.Error: "Data incompatible with messages format. Each message should be a dictionary with 'role' and 'content' keys or a ChatMessage object."
```

該当するコードを確認すると、次のような判定ロジックが正しく動作していないようでした。

```python:venv/llama/lib/python3.12/site-packages/gradio/components/chatbot.pyからの抜粋
        if type == "messages":
            all_valid = all(
                isinstance(message, dict)
                and "role" in message
                and "content" in message
                or isinstance(message, ChatMessage | Message)
                for message in messages
            )
            if not all_valid:
                raise Error(
                    "Data incompatible with messages format. Each message should be a dictionary with 'role' and 'content' keys or a ChatMessage object."
                )
```

``for message in messages``の内容が正しいフォーマットになっていないようです。

もう少し調べると、``messages``はリスト形式のはずなのに要素のはずのdict形式の生データが入ってしまっているので、``message``の中身は分割された``('role', 'user')``のような2要素のタプルになることで``all()``の条件判定に失敗していることが原因でした。

``