💡

Open Interpreterを誰でも䜿えるSlack botにしたした

に公開1件

TL;DR

はじめに

Open Interpreter楜しんでたすか

https://x.com/dory111111/status/1701930257253839288?s=20

「どうやらダバむや぀が爆誕したらしい」ずいう認識で間違いなさそうです。

でも、このダバさがわかっおるのはただ䞀郚の人だけ、もったいない
ずいうこずで今回はこのOpen Interpreterを誰でも䜿えるようにSlack botにする方法を玹介したす。

ファむルのアップロヌド、ダりンロヌドも可胜ですので、最䜎限必芁な機胜を党お備えたものが完成するず思いたす。





この蚘事を読めばわかるこず

  • Cloud Runを䜿っおサヌバヌレスなOpen Interpreter実行環境を構築する方法
  • Cloud Storageを䜿っおステヌトレスなCloud Runでも䌚話ずファむルを維持する方法
  • Slack Botを䜜成し、適切な暩限を蚭定する方法
  • Slack からファむルをアップロヌドしおOpen Interpreterに枡す方法
  • Open Interpreterの䜜成・線集したファむルをSlack経由でダりンロヌドする方法

この蚘事を読んでもわからないこず

  • Open Interpreterの掻甚事䟋
  • 今回のコヌドの詳现(リポゞトリをみおね)

Bot の仕様

今回䜜成するBotの仕様は以䞋のようになっおいたす。

  1. Botはメンションするこずで起動し、スレッド内に出力結果を送信する
  2. スレッド内でさらにメンションするこずで䌚話を継続できる
  3. スレッドごずに䌚話履歎ずOpen Interpreterの操䜜するファむルが分離しおいる
    • 新芏スレッドを立おればクリヌンな状態から凊理を開始できる
    • 耇数スレッドから党く同じ名前のファむルが䜜成されおもバッティングしない
  4. ファむルをアップロヌドし、それを読み蟌たせられる
    • アップロヌドしたCSVを分析させるなどが可胜
  5. Open Interpreterの䜜成したファむルをダりンロヌドできる
    • 䜜成したグラフなどを、党おSlack内で閲芧できる
  6. 定番のラむブラリを事前にむンストヌルしおいる
    • 䜙分なむンストヌル時間が発生するのを防ぐ

工倫ポむント

ファむルや䌚話履歎を保持したい

同じスレッドからは同じファむルにアクセスできるようにするため「凊理の前にGCSから読みずり」「凊理が終わったらGCSに保存」を行いたす。

少しでもわかりやすいようにGCSのバケット名をopen-interpreter-<UserId>のようにし、スレッドごずにGCSのパス(prefix)も分けたす。

gcloud_storage.py
def get_bucket_name(user_id: str) -> str:
    return f"open-interpreter-{user_id}".lower()

Open Interpreterの凊理が始たる前にGCSからCloud Runにファむルを読み取る。

gcloud_storage.py
def download_files_from_bucket(bucket_name: str, destination_dir_path: str, blob_prefix: str) -> List[str]:
    """
    Download files from GCS bucket to cloud run
    :param bucket_name: bucket name to download
    :param destination_dir_path: directory path to save files
    :param blob_prefix: prefix of blob to download
    """
    # Initialize the Cloud Storage client
    storage_client = storage.Client()

    if not os.path.exists(destination_dir_path):
        os.makedirs(destination_dir_path)

    # Check if the bucket exists
    if not storage_client.lookup_bucket(bucket_name):
        storage_client.create_bucket(bucket_name)
        return []

    # Get the bucket
    bucket = storage_client.get_bucket(bucket_name)

    file_paths = []

    # Loop through the blobs (files) and download them
    for blob in bucket.list_blobs(prefix=blob_prefix):
        file_name = os.path.basename(blob.name)
        destination_file_path = os.path.join(destination_dir_path, file_name)
        blob.download_to_filename(destination_file_path)
        file_paths.append(destination_file_path)
    return file_paths

Open Interpreterの凊理が終わった埌にCloud RunからGCSにファむルを保存する。

gcloud_storage.py
def upload_files_to_bucket(local_directory_path: str, bucket_name: str, blob_prefix: str):
    """
    Upload files from cloud run to GCS bucket
    :param local_directory_path: directory path to upload files in cloud run
    :param bucket_name: bucket name to upload
    :param blob_prefix: prefix of blob to upload
    """
    # Initialize the Cloud Storage client
    storage_client = storage.Client()

    # Get the bucket
    bucket = storage_client.get_bucket(bucket_name)

    # Check if the directory exists
    if not os.path.exists(local_directory_path):
        return

    ignore_patterns = get_ignore_patterns(".gitignore")

    # Loop through each file in the temporary directory
    for root, _, files in os.walk(local_directory_path):
        for filename in files:
            if any(fnmatch.fnmatch(filename, pattern) for pattern in ignore_patterns):
                continue

            source_file_path = os.path.join(root, filename)
            relative_path = os.path.relpath(source_file_path, local_directory_path)
            blob_name = os.path.join(blob_prefix, relative_path)

            # Create a blob
            blob = bucket.blob(blob_name)

            # Upload the file
            blob.upload_from_filename(source_file_path)

䜕でもかんでも保存する必芁はないので.gitignoreに蚘茉されおいるファむルはアップロヌドしないようにしおいたす。
ただし、今回はCloud Run䞊にtmpフォルダを䜜っお䜜業するのですが(埌述)、tmpフォルダ内には無駄なものは䜜られないらしく、この凊理は䞍芁っぜいです。

gcloud_storage.py
def get_ignore_patterns(ignore_file_path):
    if os.path.exists(ignore_file_path):
        with open(ignore_file_path, "r") as f:
            return f.read().splitlines()
    else:
        return []

Open Interpreterではこれたでの䌚話履歎の取埗、読み蟌み機胜が搭茉されおいたす。
そこでこのmessagesをmessages.jsonずしお保存し、これもGCSにアップするこずで䌚話履歎を保存したす。

README
messages = interpreter.chat("My name is Killian.", return_messages=True) # Save messages to 'messages'
interpreter.reset() # Reset interpreter ("Killian" will be forgotten)

interpreter.load(messages) # Resume chat from 'messages' ("Killian" will be remembered)
interpreter_helper.py
...
    def read_messages_json(self) -> list:
        """
        Read messages history from temp directory
        :return: list of messages
        """
        messages_file_path = os.path.join(self.temp_dir_path, "messages.json")
        if os.path.exists(messages_file_path):
            with open(messages_file_path, "r") as f:
                messages = json.load(f)
        else:
            messages = []
        logger.info({"message": "Read messages json.", "messages": messages})
        return messages

    def save_messages_json(self, messages: List[dict]):
        """
        Save messages history to temp directory
        :param messages: list of messages
        """
        messages_json = json.dumps(messages, indent=4, ensure_ascii=False)
        with open(os.path.join(self.temp_dir_path, "messages.json"), "w", encoding="utf-8") as f:
            f.write(messages_json)
        logger.info({"message": "Save messages json.", "messages": messages})
...

ナヌザヌからのファむルアップロヌドを受け付けたい

ナヌザヌがSlack䞊でファむルをアップロヌドするずevent["files"]にその情報が入るので、fileのidを元にSlack APIを䜿っおCloud Runにロヌドしたす。
その䞊で、ナヌザヌメッセヌゞに「<ファむル名>を<アップロヌド先のパス>にアップしたした。」ず远加しおOpen Interpreterに認識させおあげたす。

なお、この凊理もBotぞのメンションをトリガヌずしおいるためファむルをアップロヌドする際もメンションが必須になりたす。

main.py
@slack_app.event("app_mention")
def mentioned(body, say: Say):
    slack_token = os.environ["SLACK_BOT_TOKEN"]
    client = WebClient(token=slack_token)
    event = body["event"]

    ...

    files = event.get("files", [])
    file_paths = []
    for file in files:
        file_path = slack_api.load_file(client, file["id"], temp_dir)
        file_paths.append(file_path)
        message_by_user = f"I uploaded {file['name']} to {temp_dir}\n{message_by_user}"

    ...
slack_api.py
def load_file(client: WebClient, file_id: str, save_dir_path: str) -> str:
    try:
        # Call the files.info method using the WebClient
        result = client.files_info(file=file_id)

        file_info = result["file"]

        file_url = file_info["url_private"]
        file_name = file_info["name"]

        # Download the file
        opener = urllib.request.build_opener()
        opener.addheaders = [("Authorization", "Bearer " + os.environ.get("SLACK_BOT_TOKEN", ""))]
        urllib.request.install_opener(opener)
        if not os.path.exists(save_dir_path):
            os.makedirs(save_dir_path)
        file_path = os.path.join(save_dir_path, file_name)
        urllib.request.urlretrieve(file_url, file_path)
        return file_path
    except SlackApiError as e:
        raise e

次に、ナヌザヌが凊理結果をダりンロヌドできるようにしたす。
できればChatGPTに䞊手いこず認識しおほしいですが、ここではハヌドコヌディングで察応したす。
ナヌザヌが「@Open Interpreter ダりンロヌド」ずメッセヌゞを送信した時だけ分岐し、Cloud Run䞊のファむルをスレッドに送信したす。
なおmessage.jsonは基本的に䞍芁なので陀いおいたす。

main.py
    ...
    loaded_file_paths = gcloud_storage.download_files_from_bucket(parent_message_user_id, thread_ts)

    if message_by_user == "ダりンロヌド":
        for file_path in loaded_file_paths:
            if file_path.endswith("/messages.json"):
                continue
            slack_api.upload_file_to_thread(client, channel_id, thread_ts, file_path)
        return
    ...
slack_api.py
def upload_file_to_thread(client: WebClient, channel_id: str, thread_ts: str, file_path: str):
    try:
        response = client.files_upload(channels=channel_id, thread_ts=thread_ts, file=file_path)
        assert response["file"]  # the uploaded file
    except Exception as e:
        logger.error({"message": "SlackApiError", **e.__dict__})
        raise e

同時リク゚スト問題

Cloud Runでは、1コンテナが同時に捌くリク゚ストの数を指定するこずができたす。
ここで1を指定すれば、おそらくこの問題は無芖できたすが、今回はデフォルト蚭定のたた、別の方法で察応したす。

やるべきこず2぀ありたす。

  1. スレッドごずにCloud Run内での䜜業フォルダを分割する
  2. Open Interpreterの呌び出し方を倉える

スレッドごずにCloud Run内での䜜業フォルダを分割する

スレッドごずにナニヌクCloud Run内の䜜業フォルダを䜜成し、GCSからのダりンロヌド・アップロヌドもそのフォルダのみで行いたす。
user_idも含めおナニヌクにしおいたすが、実際はthread_tsのみで良いはずです。

helpers.py
def get_temp_dir(user_id: str, thread_ts: str) -> str:
    return f"/tmp/{user_id}/{thread_ts}"

Open Interpreterの呌び出し方を倉える

Slack bot経由でOpen Interpreterに同時に耇数のリク゚ストを送信するず、メッセヌゞが混ざっおしたうずいう珟象が発生したした。

た1ず.、デ マヌヌタクをダ読りみン圢蟌匏みの、そのファ抂む芁ルをを䜜確成したす認。
する2こ.ずから フ始めたァむしルにテょキうスト。をそのた曞めきに蟌はみ以䞋たす。
のたスずテ、ッマプヌクをダりン実圢行匏したすの。
ファ1ã‚€.ル pandasをラ䜜む成しブラたリをしむンストょヌルう。したすファむ既ルに名むンはスト䜕にしたすかヌルたた、されファむおルいにる曞堎合きは蟌スキむッテプキでストきはたす。
2䜕.ですか CSVファむルを読み蟌みたす。

詳现な原因や仕組みはわかっおいないのですが、呌び出し方の倉曎によっおずりあえず治ったので、そのたたにしおいたす。

pythonコヌド内でOpen Interpreterを䜿う時、通垞はこのようにしお呌び出したす。

README
import interpreter

interpreter.chat("Plot AAPL and META's normalized stock prices") # Executes a single command
interpreter.chat() # Starts an interactive chat

この時interpreter/__init__.pyが読み蟌たれるのですが、その䞭身はこうなっおいたす。

__init__.py
from .interpreter import Interpreter
import sys

# This is done so when users `import interpreter`,
# they get an instance of interpreter:

sys.modules["interpreter"] = Interpreter()

# **This is a controversial thing to do,**
# because perhaps modules ought to behave like modules.

# But I think it saves a step, removes friction, and looks good.

#     ____                      ____      __                            __
#    / __ \____  ___  ____     /  _/___  / /____  _________  ________  / /____  _____
#   / / / / __ \/ _ \/ __ \    / // __ \/ __/ _ \/ ___/ __ \/ ___/ _ \/ __/ _ \/ ___/
#  / /_/ / /_/ /  __/ / / /  _/ // / / / /_/  __/ /  / /_/ / /  /  __/ /_/  __/ /
#  \____/ .___/\___/_/ /_/  /___/_/ /_/\__/\___/_/  / .___/_/   \___/\__/\___/_/
#      /_/                                         /_/

sys.modules["interpreter"] = Interpreter()のせいで同時に生成されたmessageが混ざったのだず思われたす。

そこでInterpreterクラスをこのように呌び出しお䜿うこずで察応したす。

from interpreter.interpreter import Interpreter

interpreter = Interpreter()
interpreter.chat("His name is Killian.")

䜿いそうなラむブラリを事前にむンストヌルしたい

コンテナ内に事前むンストヌルし、むンストヌル枈みであるこずをこのようなシステムメッセヌゞに远蚘したす。
これによりコンテナのデプロむ時間がものすごく長くなりたすが、しゃヌなしずしたす。

You can use the following libraries without installing:
- pandas
- numpy
- matplotlib
- seaborn
- scikit-learn
- pandas-datareader
- mplfinance
- yfinance
- requests
- scrapy
- beautifulsoup4
- opencv-python
- ffmpeg-python
- PyMuPDF
- pytube
- pyocr
- easyocr
- pydub
- pdfkit
- weasyprint

Slack ずの繋ぎこみ

FlaskずSlack Boltを䜿っお接続したす。

Slack Botは3秒以内にリク゚ストを返さないずリク゚ストのリトラむが行われおしたいたす。
そこで、これを無理やり察凊するためにmiddlewareを蚭定しおいたす。

main.py
from flask import Flask, request
from slack_bolt import App, BoltResponse, Say
from slack_bolt.adapter.flask import SlackRequestHandler
from slack_sdk import WebClient


app = Flask(__name__)
slack_app = App(token=os.environ["SLACK_BOT_TOKEN"], signing_secret=os.environ["SLACK_SIGNING_SECRET"])
handler = SlackRequestHandler(slack_app)


@app.route("/slack/events", methods=["POST"])
def slack_events():
    return handler.handle(request)


@slack_app.middleware
def handle_retry(req, next):
    if "x-slack-retry-num" in req.headers and req.headers["x-slack-retry-reason"][0] == "http_timeout":
        return BoltResponse(status=200, body="success")

    next()


@slack_app.event("app_mention")
def mentioned(body, say: Say):
    ...

これに加え、コンテナのCPU垞時起動蚭定も行う必芁がありたす。蚭定方法はデプロむず合わせお説明したす。

詳现はこちらの蚘事をご芧ください。
https://zenn.dev/bisque/articles/slack-bolt-on-google-cloud

デプロむする

Slack Botの蚭定ずCloud Runの蚭定を行き来する必芁がありたすが、そんなに難しくは無いです。

Slack Botを䜜成する。

こちらのペヌゞからアプリを䜜成したす。

OAuthの蚭定, Tokenの取埗

OAuth & PermissionsペヌゞのScopesからBotに以䞋の暩限を䞎えたす。

蚭定したら、Botをワヌクスペヌスにむンストヌルしたす。
Install to Workspaceずいう緑のボタンをクリックしたす。

その埌xoxb-から始たるBot User OAuth Tokenが䜜成されるので、これをどこかにメモしおおきたす。

Secretの取埗

Basic InformationペヌゞからSigning Secretを取埗し、これもどこかにメモしおおきたす。

コンテナをCloud Runにデプロむする

Bot User OAuth TokenずSigning Secretが取埗できたずころで、Cloud Runのビルド、蚭定を行いたす。
環境倉数はGCPのコン゜ヌルで蚭定するため.envファむル等はありたせん。
$PROJECT $REGIONなどは適宜曞き換えおください。

ラむブラリをむンストヌルしたくっおいるため、かなり時間がかかりたす。

gcloud run deploy open-interpreter-slack-bot \
  --source . \
  --region $REGION \
  --memory 2048Mi \
  --project $PROJECT \
  --allow-unauthenticated

デプロむが完了するずこのようにCloud Runの゚ンドポむントが衚瀺されたすので、コピヌしおおきたす。

https://open-interpreter-slack-bot-XXXXXXXXXXX.a.run.app

コン゜ヌル䞊での蚭定

続いおGCPコン゜ヌルでCloud Runを開き、先ほどデプロむしたサヌビスを開きたす。
䞊蚘デプロむコマンドをそのたた䜿った堎合open-interpreter-slack-botずいう名前になっおいるはずです。

新しいリビゞョン線集ずデプロむを遞択し、以䞋の環境倉数を蚭定したす。

  • OPENAI_API_KEY
  • SLACK_BOT_TOKEN (先ほど取埗したもの)
  • SLACK_SIGNING_SECRET (先ほど取埗したもの)

さらに、CPU の割り圓おず料金からCPU を垞に割り圓おる遞択したす。
蚭定し終わったらそのたたデプロむをクリックしたす。

Event Subscriptionの蚭定

最埌に、再びSlackの蚭定に戻り、SlackのEventをSubscribeしたす。

Event SubscriptionsペヌゞでEnable Eventsから先ほどCloud Runのデプロむ埌に取埗したURLをペヌストしたす。
この時、URLの末尟に/slack/eventsを远加したす。

するずSlackからこの゚ンドポむントにヘルスチェックが行われたす。
これが通らない堎合Cloud Runで䜕らかの゚ラヌが起きおいたす。

Subscribeするむベントにはapp_mentionを遞択すればOKです。

これにお䜜成完了です
どこかのチャンネルにBotを招埅し、メンションしおみたしょう。

終わりに

良きOpen Interpreterラむフを

Discussion

nn

こんにちは。䟿利なツヌルの玹介のみならずCloud Runの解説にもなっおいる蚘事の公開、ありがずうございたす。
ぜひ自分の環境でも䜿っおみたいず思ったのですが、ラむブラリのむンストヌルやCPU垞時起動ずいうこずで、費甚がそこそこかかるんじゃないかず心配です。
もし差し支えなければ、毎日ないし毎月単䜍でどれくらいコストがかかりそうか(あるいはかかっおいないか)教えおいただけたせんか

1