28
23

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-09-17

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



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

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

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


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

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


動作の様子はこちらです


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


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


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

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

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

import PIL
# エラーなくPILが通るはず
# uninstallします
!pip uninstall pillow -y

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

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

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

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

image.png

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

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

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

# [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を使用します。

import traceback
traceback_str = traceback.format_exc()

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

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

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

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

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

# [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文の出力結果は次の通りです(実際は文字列です)。

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を直書きします(本番ではこんな危険なことは避けましょう)

# パッケージのインストール
!pip install openai==0.28
# 非常に危険。API_KEYを直書き

import os
import re
import openai

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

次に、ChatGPTの回答から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を用意します。

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関数に整理し、実行します。

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

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

# メインの実行
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はこちらになります。


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

小川 雄太郎


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

28
23
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
28
23

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?