足りないパッケージを自分で判断して、インストールして再実行してくれる機能の実装例

OpenInterpreterやその他自律Agent系のOSSの、「足りないパッケージを自分で判断して許可を仰いだうえで、勝手にインストールする機能」を実装する一例を紹介します。

<br>

---

コードの自動生成を駆使する場合、どうしてもライブラリがインストールされていない事態は避けられず、自動でインストールしてくれるとありがたいです


何より、なんかかっこいいです！

（ただし基本的に最新バージョンがインストールされるので、個別の仮想環境での実施が前提となります）。

<br>

Google Colab上でこの機能を実現する実装例（あくまで一例）を構築したので記事にしてみます。

実装したGoogle Colabのnotebookはこちらになります。

https://github.com/YutaroOgawa/Qiita/blob/master/gpt_retry_pip_auto_20230918.ipynb

---

動作の様子はこちらです


https://x.com/ogawa_yutaro_22/status/1703460709492023314?s=20



---


本記事の流れは次の通りです。

<br>


**手順1: 不足しているパッケージを用意するために、あえて、PILをアンインストール**
**手順2: 自動生成されたコードをtry-exceptで挟む関数を用意する**
**手順3: スタックトレースから修正方法をLLM（ChatGPT）に教えてもらう**
**手順4: メイン関数の実装と実行**


<br>


## 手順1: 不足しているパッケージを用意するために、あえて、PILをアンインストール

Google Colabはいろいろとはじめからパッケージが入っていて、不足パッケージを探すのが面倒なので、逆にアンインストールします。

**今回は、画像のサイズを出力するコードを自動生成した""、という前提のもとで話を進めます。**

```python
import PIL
# エラーなくPILが通るはず
```

```python
# uninstallします
!pip uninstall pillow -y
```

```import PIL```をすると問題なく通ると思います。
そして```uninstall```をします。

**このuninstallを有効にするために、一度ランタイムを再起動してください**

そして、再度```import PIL```をすると、エラーが発生します。

これでパッケージを不足している状態になりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3328834/2c42a8e3-752e-f7ef-fbdb-13138d08a1a5.png)

## 手順2: 自動生成されたコードをtry-exceptで挟む関数を用意する

本記事ではコードの自動生成部分は省略します。

**以下のような、PILで画像を読み込んで、その幅と高さを出力するコードが生成されたとします。**





```python
# [1] 生成されたコードが例えば以下だったとします。
# 単純に画像を取得して、幅と高さを求めているだけです。

generated_code_txt="""
from PIL import Image
import requests
from io import BytesIO

# 適当な画像のURLを指定
image_url = 'https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3328834%2Ff8a26020-6ec7-aa41-9ff6-0c4d38cb8319.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&w=1400&fit=max&s=521ce0ce7dc4fe5865bc41b79a4b735f'

# 画像を取得
response = requests.get(image_url)
img = Image.open(BytesIO(response.content))

# 画像を取得
response = requests.get(image_url)
img = Image.open(BytesIO(response.content))

# 画像の幅と高さを取得
width, height = img.size

# 幅と高さを表示
print("幅:", width)
print("高さ:", height)
"""
```

このコードを実行したときに、```from PIL import Image```でエラーが発生するので、Try-Exceptで実行するコードを囲む関数を用意します。

さらに、例外（Exception）でどのような内容（スタックトレース）が表示されていたのかを取得するために、tracebackを使用します。

```python
import traceback
traceback_str = traceback.format_exc()
```

エラーメッセージを変数traceback_strに格納します。

**このエラーメッセージ（スタックトレース）を解決する方法をLLM（今回はGPT-3.5）に尋ね、その修正のPythonコードを実行するという流れになります。**

Try-Exceptで実行するコードを囲む関数（の一例）は次の通りです。

```python
def add_try_except_block(code_str):
    """try-exceptでコードを挟む関数です"""
    indented_code = "\n".join(["    " + line for line in code_str.splitlines()])

    try_except_code = f"""
try:
    traceback_str = None
{indented_code}
except Exception as e:
    import traceback
    traceback_str = traceback.format_exc()"""
    return try_except_code
```

さきほどの生成された（と仮定した）コードにこの関数の引数にして実行すると以下のように変換されます。


```python
# [1] 生成されたコードをtry-exceptで挟む
generated_code_txt_with_try = add_try_except_block(generated_code_txt)

# [2] try-exceptで挟んだ状態を確認
print(generated_code_txt_with_try)
```

print文の出力結果は次の通りです（実際は文字列です）。

```python
try:
    traceback_str = None
    
    from PIL import Image
    import requests
    from io import BytesIO
    
    # 適当な画像のURLを指定
    image_url = 'https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3328834%2Ff8a26020-6ec7-aa41-9ff6-0c4d38cb8319.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&w=1400&fit=max&s=521ce0ce7dc4fe5865bc41b79a4b735f'
    
    # 画像を取得
    response = requests.get(image_url)
    img = Image.open(BytesIO(response.content))
    
    # 画像を取得
    response = requests.get(image_url)
    img = Image.open(BytesIO(response.content))
    
    # 画像の幅と高さを取得
    width, height = img.size
    
    # 幅と高さを表示
    print("幅:", width)
    print("高さ:", height)
except Exception as e:
    import traceback
    traceback_str = traceback.format_exc()
```

## 手順3: スタックトレースから修正方法をLLM（ChatGPT）に教えてもらう

LLMは何でも良いですが、GPT3.5を使用します。

本記事では簡単のためにAPI_KEYを直書きします（本番ではこんな危険なことは避けましょう）


```python
# パッケージのインストール
!pip install openai==0.28
```

```python
# 非常に危険。API_KEYを直書き

import os
import re
import openai

KEY = "sk-hogehoge"
openai.api_key = KEY
```

次に、ChatGPTの回答からPythonのコード部分のみを抜き出す関数を定義します。

コードだけ教えてと伝えても、なんかごちゃごちゃ説明してくれるので、コード以外を無視することにします。


```python
def extract_python_code(text):
    """pythonの実装のみを抽出"""
    pattern = r'```python(.*?)```'
    result = re.findall(pattern, text, re.DOTALL)
    # result リスト内のすべての要素を結合して1つの文字列にする
    extracted_code = '\n'.join(result)
    return extracted_code
```

あとは、実行したPythonコード、発生したスタックトレース、そして回答として欲しい修正方法（事前に実行するPythonコード）を埋め込んだ、適切なPromptを用意します。



```python
def get_response_from_chatGPT(generated_code_txt_with_try, traceback_str):

    # Promptの作成
    user_content = f"""
以下のPythonコードを実行したところ、次のようなスタックトレースが表示されました。
再度このPythonコードを実行し、エラーなく完了させるためには、このスタックトレースの内容を解決するために、
最低限として事前にどのようなPythonコードを実行する必要がありますか？
Pythonのコード形式で教えてください。pipコマンドなどが必要な場合は、subprocess.runを使用し、標準出力をprintするように、

output = subprocess.check_output(command, shell=True, stderr=subprocess.STDOUT, universal_newlines=True)
print(output)
を使用してください。

### [Pythonコード]
{generated_code_txt_with_try}

###  [スタックトレース]
{traceback_str}

### [事前に実行するPythonコード]
"""

    messages = [
      {"role": "system","content":"You are an AI assistant that helps people find information."},
      {"role": "user", "content":user_content}
    ]


    # responseを得る
    response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo",
        messages = messages,
        temperature=0.0,
        top_p=0.0,
        max_tokens=800,
        )

    # Pythonコードのみを取り出す
    ret = response['choices'][0]['message']['content']  # 回答
    correction_code = extract_python_code(ret)

    return correction_code
```

## 手順4: メイン関数の実装と実行

以上で準備は完了です。最後に手順1-3で用意した内容をmain関数に整理し、実行します。

```python
def main(generated_code_txt):

  # [1] 何らかコードを生成したとします
  # generated_code_txt

  # [2] try-exceptで挟み、スタックトレースをとれるようにします
  generated_code_txt_with_try = add_try_except_block(generated_code_txt)

  # [3] エラーがなくなるまで回します（本当は制限回数を定めるべき）
  error_is = True

  while error_is:
      # [3-1] 空の辞書を作成。exec実行した中身の変数をここに格納します
      variables = {}
      traceback_str= None

      # [3-2] 実行 execを使用してコードを実行し、exec内の変数を辞書で取得します
      print("============================================")
      print("[1] 生成されたコードを実行します（今回は適当な画像のサイズを出力するコードです）\n")
      exec(generated_code_txt_with_try, globals(), variables)

      # [3-3] エラーなく終わったかを判定します
      if  not variables.get("traceback_str"):
          error_is = False  # loopから抜ける
          print("\n[Fin] 実行が完了しました")
          break

      # [3-4] エラーが発生し、スタックトレースが存在する場合、それを表示します
      if  variables.get("traceback_str"):
          print("[2] エラーが発生しました\n\n")
          print(traceback_str, variables.get("traceback_str"))

      # [3-5] 修正コマンドの実行
      print("\n[3] 修正方法を考えます。少々お待ちください。\n")
      correction_code = get_response_from_chatGPT(generated_code_txt_with_try, traceback_str)

      # [3-6] 実行して良いかの確認
      print("\n[4] 修正のために、以下のコマンドを実行しても良いですか？\n")
      print(correction_code)
      user_input = input("宜しければyを入力しEnterを押してください。実行を中止する場合はnを入力しEnterを押してください: ")

      # [3-7] 修正の実行
      if user_input.lower() == 'y':
          print("\n[5] それでは修正します。\n")
          exec(correction_code, globals())
          print("[6] 修正が完了しました\n\n 再実行に入ります・・・\n\n")

      else:
          print("ユーザーが実行中止を選択しました。")
          break
```

このメイン関数を実行します。


```python
# メインの実行
main(generated_code_txt_with_try)
```

すると、初回はPILパッケージの不足でエラーになります。

```
============================================
[1] 生成されたコードを実行します（今回は適当な画像のサイズを出力するコードです）

[2] エラーが発生しました


None Traceback (most recent call last):
  File "<string>", line 8, in <module>
ModuleNotFoundError: No module named 'PIL'


[3] 修正方法を考えます。少々お待ちください。
```

そして、修正方法を考えてくれます。

```
[4] 修正のために、以下のコマンドを実行しても良いですか？


import subprocess

command = 'pip install pillow'
output = subprocess.check_output(command, shell=True, stderr=subprocess.STDOUT, universal_newlines=True)
print(output)

宜しければyを入力しEnterを押してください。実行を中止する場合はnを入力しEnterを押してください: y
````

修正案を提案してくれるので、問題なければ、yを入力し、Enterを押します。

今回は、```pip install pillow```を実行したいと言っています。

足りないとスタックトレースに出てきたのはPILですが、きちんとpillowをインストールすると判断してくれています。

```
[5] それでは修正します。

Collecting pillow
  Downloading Pillow-10.0.1-cp310-cp310-manylinux_2_28_x86_64.whl (3.6 MB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 3.6/3.6 MB 13.7 MB/s eta 0:00:00
Installing collected packages: pillow
Successfully installed pillow-10.0.1

[6] 修正が完了しました

 再実行に入ります・・・
```

修正を実施し、pillowがインストールされ、再実行に入ります。


```
============================================
[1] 生成されたコードを実行します（今回は適当な画像のサイズを出力するコードです）

幅: 1322
高さ: 715

[Fin] 実行が完了しました
```


以上で、不足していたパッケージを自動で判断し、許可のもとインストールして、コードを再実行してくれる仕組みが作れました。

今回のものは非常に単純で、もっと注意すべき点やエラーのハンドリングにはいろいろありますが、ミニマムに動かす第一歩としては十分かと思います。

なんか、勝手にやってくれるのかっこいいです。

宜しければぜひお試しください。


（再掲）実装したGoogle Colabのnotebookはこちらになります。

https://github.com/YutaroOgawa/Qiita/blob/master/gpt_retry_pip_auto_20230918.ipynb


<br>

以上、ご一読いただき、ありがとうございました。


小川 雄太郎

 

<br>

【免責】
本記事の内容は執筆者の意見/発信であり、執筆者が属する企業等の公式見解ではございません