DockerでCode Interpreter環境を作成してくれる「AgentRun」を試す
LLMに安全なコード実行(Code Interpreter)のための環境を提供するソリューションとしては以下のようなものがある
最初のやつは専用のクラウド、残りはAzureとAWSということで、基本的に全部クラウドベース(e2bはインフラ部分のレポジトリも公開されているが、そこそこの規模になりそう)。商用でやるならそういう選択もあるのだろうけども、クローズドにやりたい、手元で手軽にやりたいというケースもあると思う。
そういうケースで使えそうなOSSを見つけたので試してみたいと思う。
レポジトリ
ドキュメント
AgentRun: AIが生成したコードを安全に実行
AgentRunは、Pythonライブラリで、1行のコードで大規模言語モデル(LLM)からPythonコードを安全に実行することを容易にします。Docker Python SDKとRestrictedPythonを基盤として構築されており、分離されたコードの実行を管理するためのシンプルで透過的、かつユーザーフレンドリーなAPIを提供します。
AgentRunは、オプションでキャッシュ機能を備え、依存関係を自動的にインストールおよびアンインストールし、リソース消費を制限し、コードの安全性をチェックし、実行タイムアウトを設定します。完全な静的型付けとわずか2つの依存関係で、テストカバレッジは97%です。
なぜ?
LLMにコード実行能力を与えることは、大幅なアップグレードです。次のユーザーのクエリを考えてみましょう。
12345 * 54321はいくつですか?
あるいは、さらに野心的な質問として、Apple株の過去1週間の1日あたりの平均移動とは何ですか?
。コード実行機能があれば、LLMはコードを実行することで、両方の質問に正確に回答することができます。しかし、信頼できないコードを実行することは危険であり、潜在的な罠が数多く存在します。例えば、適切な安全対策がなければ、LLMは次のような有害なコードを生成する可能性があります。
import os # deletes all files and directories os.system('rm -rf /')
このパッケージは、危険なコードを防止および防御しながら、どんなLLMにもコード実行能力を1行のコードで付与します。
主な機能
- 安全なコード実行: AgentRunは実行前に生成されたコードに危険な要素がないか確認します
- 隔離された環境: コードは完全に隔離されたDockerコンテナ内で実行されます
- **設定可能なリソース管理:コードが消費できるコンピューティングリソースの量を、適切なデフォルト値で設定できます
- タイムアウト: スクリプトの実行に要する時間制限を設定
- 依存関係の管理: インストールを許可する依存関係を完全に制御
- 依存関係のキャッシュ: AgentRunは、パフォーマンスを最適化するために、依存関係を事前にDockerコンテナにキャッシュする機能を提供します。
- 自動クリーンアップ: AgentRunは生成されたコードによって作成されたすべてのアーティファクトをクリーンアップします。
- REST API を搭載: Docker の設定が面倒ですか?AgentRun には、セルフホスティング用の設定済みの Docker セットアップが搭載されています。
- 透過的な例外処理: AgentRunは、お客様のシステムでPythonを実行した場合と同じ出力結果を返します。例外やトレースバックも含まれます。不可解なDockerメッセージは表示されません。
独自のDocker構成を使用したい場合は、pipを使用してこのパッケージをインストールし、実行中のDockerコンテナでAgentRunを初期化するだけです。さらに、このリポジトリをクローンすることで、自己ホスティングが可能な構成済みのDocker ComposeセットアップとAPIを使用することもできます。
Docker に精通している場合は、スタンドアロンサービスとしてすでに設定済みの Docker と REST API を使用することを強くお勧めします。
Getting Startedに従って進める
セットアップ方法は2つ
- pip install
- agentrunパッケージをpipでインストールして、コードからコンテナに直接アクセス
- 事前にDockerコンテナを自分でセットアップしておく必要がある?
- REST API
- 用意されているdocker-compose.yamlで起動すると、APIコンテナとPython実行コンテナが起動、API経由でPython実行コンテナにアクセス
- 事前にDocker環境があればよい
- こちらがオススメらしい
ということで今回は後者でやってみる。余裕があれば前者も確認してみたい。
LAN内のUbuntu 22.04サーバでやってみる。Dockerはセットアップ済み。
レポジトリをクローン
$ git clone https://github.com/Jonathan-Adly/agentrun && cd agentrun
agentrun-api
ディレクトリに移動
$ cd agentrun-api
.envの雛形から.env.dev
を作成
$ cp .env.example .env.dev
中身はこれだけ。とりあえずこのままで進める。
# Container name - can change depending on your docker setup
CONTAINER_NAME="agentrun-api-python_runner-1"
docker-compose.yamlはこんな感じ。APIコンテナとPython実行コンテナが立ち上がるようになっている。
services:
api:
build:
context: ./
dockerfile: docker/api/Dockerfile
command: uvicorn api.main:app --host 0.0.0.0 --port 8000 --reload
volumes:
- ./src:/code
- /var/run/docker.sock:/var/run/docker.sock
ports:
- "8000:8000"
env_file:
- ./.env.dev
python_runner:
build:
context: ./
dockerfile: docker/code_runner/Dockerfile
volumes:
- code_execution_volume:/code
command: ["tail", "-f", "/dev/null"]
pids_limit: 10
security_opt:
- no-new-privileges:true
environment:
- OPENBLAS_NUM_THREADS=1 # this specifically for a numpy bug. See: https://github.com/Jonathan-Adly/AgentRun/issues/7#issue-2306842471
volumes:
code_execution_volume:
docker compose で起動。
$ docker compose up -d --build
$ docker compose ps
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
agentrun-api-api-1 agentrun-api-api "uvicorn api.main:ap…" api 27 seconds ago Up 26 seconds 0.0.0.0:8000->8000/tcp, :::8000->8000/tcp
agentrun-api-python_runner-1 agentrun-api-python_runner "tail -f /dev/null" python_runner 27 seconds ago Up 26 seconds
APIに投げてみる。curlだとエスケープが色々辛い。。。。
$ curl -X POST http://localhost:8000/v1/run/ \
-H "Content-Type: application/json" \
-d '{"code":"print(\"hello, world!\")"}' | jq -r .
{
"output": "hello, world!\n"
}
Pythonのスクリプトで。
$ pip install requests
import requests
import json
def execute_python_code(code: str) -> str:
response = requests.post("http://localhost:8000/v1/run/", json={"code": code})
output = response.json()["output"]
return output
code = """
print(2*3)
"""
result = execute_python_code(code)
print(result)
$ python calc.py
6
一応実行できているように思える。
ということで、これをFunction Callingで使えば、Code Interpreterが実現できるはず。せっかくなのでローカルのOllamaでOpenAI互換APIを使って試す。モデルはllama-3.1で。
$ ollama ps
NAME ID SIZE PROCESSOR UNTIL
llama3.1:latest 42182419e950 7.1 GB 100% GPU Forever
OpenAIパッケージのインストール
$ pip install openai
スクリプト
import json
import requests
from openai import OpenAI
def execute_python_code(code: str) -> str:
response = requests.post("http://localhost:8000/v1/run/", json={"code": code})
output = response.json()["output"]
return output
tools = [
{
"type": "function",
"function": {
"name": "execute_python_code",
"description": "Sends a python code snippet to the code execution environment and returns the output. The code execution environment can automatically import any library or package by importing.",
"parameters": {
"type": "object",
"properties": {
"code": {
"type": "string",
"description": "The code snippet to execute. Must be a valid python code. Must use print() to output the result.",
},
},
"required": ["code"],
},
},
},
]
model = "llama3.1:latest"
client = OpenAI(
base_url = 'http://localhost:11434/v1',
api_key='ollama',
)
messages=[
{"role": "system", "content": "あなたは親切な日本語のアシスタントです。Pythonプログラムを実行するツールを利用できます。"},
{"role": "user", "content": "100以下のフィボナッチ数列を教えて。"},
]
response = client.chat.completions.create(
model=model,
messages=messages,
tools=tools,
)
message = response.choices[0].message
tool_calls = message.tool_calls
if tool_calls:
available_functions = {
"execute_python_code": execute_python_code,
}
messages.append(message)
for tool_call in tool_calls:
function_name = tool_call.function.name
function_to_call = available_functions[function_name]
function_args = json.loads(tool_call.function.arguments)
print(f"\nExecuting code:\n--------------------")
print(json.dumps(function_args, indent=2, ensure_ascii=False))
function_response = function_to_call(**function_args)
print(f"\nResult:\n--------------------")
print(function_response)
print(f"--------------------")
messages.append(
{
"tool_call_id": tool_call.id,
"role": "tool",
"name": function_name,
"content": function_response,
}
)
response = client.chat.completions.create(
model=model,
messages=messages,
)
answer = response.choices[0].message.content
else:
answer = message.content
print(answer)
実行
$ python function_call.py
結果
Executing code:
--------------------
{
"code": "x=0\ny=1\nfor i in range(100):\n print(y)\n x, y = y, x + y"
}
Result:
--------------------
1
1
2
3
5
8
13
21
34
55
89
144
233
377
610
987
1597
2584
4181
6765
10946
17711
28657
46368
75025
121393
196418
317811
514229
832040
1346269
2178309
3524578
5702887
9227465
14930352
24157817
39088169
63245986
102334155
165580141
267914296
433494437
701408733
1134903170
1836311903
2971215073
4807526976
7778742049
12586269025
20365011074
32951280099
53316291173
86267571272
139583862445
225851433717
365435296162
591286729879
956722026041
1548008755920
2504730781961
4052739537881
6557470319842
10610209857723
17167680177565
27777890035288
44945570212853
72723460248141
117669030460994
190392490709135
308061521170129
498454011879264
806515533049393
1304969544928657
2111485077978050
3416454622906707
5527939700884757
8944394323791464
14472334024676221
23416728348467685
37889062373143906
61305790721611591
99194853094755497
160500643816367088
259695496911122585
420196140727489673
679891637638612258
1100087778366101931
1779979416004714189
2880067194370816120
4660046610375530309
7540113804746346429
12200160415121876738
19740274219868223167
31940434634990099905
51680708854858323072
83621143489848422977
135301852344706746049
218922995834555169026
354224848179261915075
--------------------
100以下のフィボナッチ数列:1、1、2、3、5、8、13、21、34、55、89。
こんな感じで、Code Interpreterとして機能していることがわかる。ただ結構失敗することも多いので、失敗した場合にを踏まえてループするようにしたほうが良いと思う。
Python実行用コンテナだが、以下の通り、ずっと起動したままになっている。
$ docker compose ps
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
agentrun-api-api-1 agentrun-api-api "uvicorn api.main:ap…" api 12 hours ago Up 12 hours 0.0.0.0:8000->8000/tcp, :::8000->8000/tcp
agentrun-api-python_runner-1 agentrun-api-python_runner "tail -f /dev/null" python_runner 13 hours ago Up 12 hours
本来のCode Interpreter環境は、任意のセッションごとに作成、セッション終了後は破棄されるというようなものだと思うが、現状では同じコンテナを使い続けることになりそう。
APIエンドポイントもとてもシンプルで、コンテナのコントロールもできなさそう。
ただし、ドキュメントのAPIリファレンスを斜め読みした限りは、もっと制御できる幅がありそうに思える。こちらは見た感じPythonパッケージでインストールした場合のように思えるので、REST APIの場合はできることに限界がありそう。
ということでPythonパッケージでのインストールも試してみる。一旦docker composeであげたコンテナは事前に落としておくこと。
$ docker compose down
今回はJupyter Labで実行する。なお、Python仮想環境は事前に適宜用意しておくこと。
$ pip install -U pip jupyterlab
Jupyter Lab起動
$ jupyter lab --ip="0.0.0.0"
以降はJupyter Lab上の作業。
agentrunパッケージのインストール
!pip install agentrun
コンテナでコードを実行してみる。
from agentrun import AgentRun
runner = AgentRun(container_name="my_container")
code = """
print('hello, world!')
"""
result = runner.execute_code_in_container(code)
print(result)
ValueError: Container my_container not found.
なるほど、やはりコンテナが事前に必要になりそう。ということでコンテナを立てる。/code
ディレクトリが必要になる。
$ docker run -d --name my_container python:3.12-slim /bin/bash -c "mkdir /code; sleep infinity"
コンテナはAgentRunのDockerfileからビルドしてもいいと思う。
で、コードを再度実行してみると、今度は正しく動作しているように思える。
hello, world!
改めて、どんな事ができるのかをAPIリファレンスで調べてみたのだけども、上記のexecute_code_in_container
メソッドは、
隔離されたDockerコンテナ内でPythonコードを実行します。これは、Dockerコンテナ内でPythonコードを実行するための主な機能です。次の手順を実行します。1. コードが実行しても安全かどうかを確認します。2. メモリ制限付きでコンテナを更新します。3. コードをコンテナにコピーします。4. コンテナに依存関係をインストールします。5. コンテナ内でコードを実行します。5. コンテナ内の依存関係をアンインストールし、クリーンアップします。
ということで、Dockerコンテナをインスタントに作って削除みたいな、ことはどうやら現状はできないようで、せいぜい中身を元に戻そうとする、という感じに見える。なので、REST APIとそれほど大きな違いはなさそうな感じかな。
あとは、Pythonパッケージの場合だと、コンテナに割り当てるリソースとかパッケージの制御が多少できるという感じ。
なので、コンテナ自体の操作(作って消すとか)をやりたい場合はこういうものを使ってやったほうが良さそう。
というかAgentRun自体がこれを使ってコンテナの操作をやっているみたい。
まとめ
既存のクラウドベースのソリューションに比べると、色々物足りないところがあるとは思うけど、まあそれは致し方ないかなというところ。
ただ、ローカルに閉じた形でCode Interpreter環境が手軽にDockerでできるというのはユースケースによってはメリットがある場合もあると思うし、ざっとコードを見てもシンプルかつコード量が少ないので、カスタマイズも容易なのではないかと思う。