Langhain4jのコード実行エンジンを使ってPython/JavaScriptのコードを実行したい

このエントリは2024/06/26現在の情報に基づいています。将来の機能追加や変更に伴い、記載内容からの乖離が発生する可能性があります。

問い合わせ

LangChain4j (LC4J) をさわりはじめて間もない人から、以下のような問いあわせが届いた。

LC4Jを使って、Azure OpenAI Service (AOAI) が生成したコードを実行させることって可能なのか?

「いや、それアプリやし実行できるでしょうに」と思ったが、どうやら「コードインタープリタを実現したいんだけど、できるのかできないのか、どっちなんだい?」ということらしい。

結論から言うと以下の通り。

  • AOAI上でコードインタープリタを実現したいのであれば、現時点ではAOAIのSDKを使うしかない(LC4JにはまだSDKをラップしたメソッドがない)。
  • LC4Jの機能だけで実現したい場合、Python、JavaScriptの実行をサポートするコード実行エンジンが利用できるが、これはAOAI上でコードを実行するものではなく、アプリケーション実行環境の分離されたサンドボックスで動作する(Azure Container AppsのDynamic Sessionsを使って実行するような感じ)。

コード実行エンジンとコード実行ツール

LC4Jではコード実行エンジンとしてTruffleとJudge0を利用できる。言語は実行ツールによって異なる(Contributeすれば他の言語も使えるようになるはず、知らんけど)。

GraalVM Polyglot/Truffle
https://www.graalvm.org/latest/reference-manual/polyglot-programming/

APIPythonJavaScript
GraalVmPythonExecutionEngine
GraalVmPythonExecutionTool
GraalVmJavaScriptExecutionTool

Judge0
https://judge0.com/

APIPythonJavaScript
Judge0JavaScriptEngine
Judge0JavaScriptExecutionTool

以後ではGraalVM (Truffle) について取り上げる。

依存関係

GraalVMを使う場合、以下の依存関係を追加しておく。執筆時点でのLC4Jの最新は0.31.0。GraalVM関連のバージョンは23.1.3もしくは24.1.1。当然ながらビルド時のJDKもGraalVMでなければならない(Judge0であれば通常のJDKで問題ない)。

<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-code-execution-engine-graalvm-polyglot</artifactId>
    <version>{LC4Jのバージョン}</version>
</dependency>
<dependency>
    <groupId>org.graalvm.polyglot</groupId>
    <artifactId>python</artifactId>
    <version>{GraalVMのバージョン}</version>
    <type>pom</type>
</dependency>
<dependency>
    <groupId>org.graalvm.polyglot</groupId>
    <artifactId>polyglot</artifactId>
    <version>{GraalVMのバージョン}</version>
</dependency>

Low level APIの例(Pythonスクリプトの実行)

こんなプロンプトでAzure OpenAI ServiceにPythonスクリプトを生成してもらうことにする。

n=0のとき、つまり初項は0、n=1のときは1を返すようなフィボナッチ数列で、15番目のフィボナッチ数を計算するプログラムをPythonで作成してください。
プログラムはcodeという要素にJSON形式で返してください。

レスポンスをJSONで受け取れるように構成すべく、ChatLanguageModelを作成するときに以下のように構成しておく。

AzureOpenAiChatModel.builder()
                .endpoint(AZURE_OPENAI_ENDPOINT)
                .apiKey(AZURE_OPENAI_KEY)
                .deploymentName("gpt-4o")
                .temperature(0.0)
                .logRequestsAndResponses(true)
                .responseFormat(new ChatCompletionsJsonResponseFormat())
                .maxRetries(3)
                .build();

あとはふつうにChat Completionで投げ込んでLLMにPythonスクリプトを生成させる。応答はJSONで返ってくるので、Jacksonなどでオブジェクトマッピングするなどして、コードを取得しておく。なお、上記プロンプトで以下のようなコードを生成してくれる。

def fibonacci(n):
    if n == 0:
        return 0
    elif n == 1:
        return 1
    else:
        a, b = 0, 1
        for _ in range(2, n + 1):
            a, b = b, a + b
        return b

# 15番目のフィボナッチ数を計算
result = fibonacci(15)
print(result)

続いてGraalVMのコード実行エンジン上で、上記コードを実行するための準備をする。といっても大した話ではなく、GraalVmPythonExecutionEngine(JavaScriptの場合は GraalVmJavaScriptExecutionEngine)のインスタンスを生成して、executeメソッドにコードと引数をまとめた文字列を指定するだけ。ただし、以下の点に注意が必要。

  • executeメソッドは1個しか引数を受け付けない。つまり、Pythonアプリケーションをコマンドライン実行する感覚で呼び出すことはできない。
  • 戻り値は文字列のみ。
// 生成されたPythonコードを実行する
String code = "Chat Completionで取得したコード";
String result = new GraalVmPythonExecutionEngine().execute(code);

3行目のレスポンスを受け取って呼び出し元に応答を返せばおしまい。

High level APIの例

High level APIの場合、Assistantにお任せ(丸投げ、とも言う)。

まずはAssistantのインターフェースを定義しておく。今回はChat Completionを例にするので、非常にシンプル。

interface Assistant {
    String chat(String userPrompt);
}

Assistantを実体化するときに、Truffleベースの実行エンジンをツールとして利用できるように指定し、Assistantchatメソッドを呼び出すだけ。

// JavaScriptを実行させたいなら、以下の実行ツールを使う
GraalVmJavaScriptExecutionTool executionTool = new GraalVmJavaScriptExecutionTool();
// Pythonを実行させたいなら、以下の実行ツールを使う
GraalVmPythonExecutionTool executionTool = new GraalVmPythonExecutionTool();

Assistant assistant = AiServices.builder(Assistant.class)
        .chatLanguageModel(chatLanguageModel)
        .chatMemory(MessageWindowChatMemory.withMaxMessages(10))
        .tools(executionTool)
        .build();

String answer = assistant.chat(userPrompt);

これでおしまい。例えば以下のようなプロンプトを投げ込むとする。

n=0、つまり初項は0、n=1のときは1を返すようなフィボナッチ数列で、15番目のフィボナッチ数は何ですか? 

応答は610でもちろん正しい結果を返していた。この実行を追跡するとAOAIとの間で2往復(1回目でコード生成、2回目で結果を文章化)していることがわかる。AOAIから返ってきたコードは以下のよう。

// JavaScript
function fibonacci(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}
fibonacci(15);
# Python
def fibonacci(n):
    if n == 0:
        return 0
    elif n == 1:
        return 1
    else:
        a, b = 0, 1
        for _ in range(2, n + 1):
            a, b = b, a + b
        return b

fibonacci(15)

コード実行環境

例によって更問が届いた。

ゲストコードはアプリケーション呼び出し元で実行しているが、呼び出しているアプリケーションと同じ領域なのか、それとも隔離されているのか…。

オープンソースなので、コードを見ると一目瞭然である。

GraalVmJavaScriptExecutionEngine.java
https://github.com/langchain4j/langchain4j/blob/main/code-execution-engines/langchain4j-code-execution-engine-graalvm-polyglot/src/main/java/dev/langchain4j/code/graalvm/GraalVmJavaScriptExecutionEngine.java

public class GraalVmJavaScriptExecutionEngine implements CodeExecutionEngine {

    @Override
    public String execute(String code) {
        OutputStream outputStream = new ByteArrayOutputStream();
        try (Context context = Context.newBuilder("js")
                .sandbox(CONSTRAINED)
                .allowHostAccess(UNTRUSTED)
                .out(outputStream)
                .err(outputStream)
                .build()) {
            Object result = context.eval("js", code).as(Object.class);
            return String.valueOf(result);
        }
    }
}

GraalVmPythonExecutionEngine.java
https://github.com/langchain4j/langchain4j/blob/main/code-execution-engines/langchain4j-code-execution-engine-graalvm-polyglot/src/main/java/dev/langchain4j/code/graalvm/GraalVmPythonExecutionEngine.java

public class GraalVmPythonExecutionEngine implements CodeExecutionEngine {

    @Override
    public String execute(String code) {
        OutputStream outputStream = new ByteArrayOutputStream();
        try (Context context = Context.newBuilder("python")
                .sandbox(TRUSTED)
                .allowHostAccess(UNTRUSTED)
                .out(outputStream)
                .err(outputStream)
                .build()) {
            Object result = context.eval("python", code).as(Object.class);
            return String.valueOf(result);
        }
    }
}

JavaScript、Pythonとも、Polyglot.Contextを使って実行環境を生成しているので、極めて類似したコードである。設定に関するポイントは以下で、ゲストコードを信頼できないものとして分離し、ゲストコードからアプリケーション実行中のホストリソースへのアクセスができないようになっている。

メソッド指定値意味
allowHostAccessorg.graalvm.polyglot.HostAccess.UNTRUSTEDゲストコードが信頼されないものという前提で堅牢化
sandboxorg.graalvm.polyglot.SandboxPolicy.TRUSTEDホスト上のあらゆるリソースは、ゲストアプリケーションへのアクセスが可能(逆は不可)

コメントを残す

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください