fortran66のブログ

fortran について書きます。Amazonのアソシエイトとして収入を得ています。

【メモ帳】Fortran 実行時に AI に Python スクリプトを書かせて smolvm で実行する

Fortran で動的に AI にスクリプトを書かせて実行するデモ

自分でプログラムを考えるのが馬鹿らしいこの頃、Fortran ではめんどくさい作業を、自然言語のプロンプトを与えて、動的に AI に Python スクリプトを書いてもらい、それをそのまま実行することを考えます。

しかし、AI の反乱を恐れて、生成されたスクリプトは隔離された環境で行うのが流行りのようなので、その波に乗ることにします。smolvm という独立 Kernel をもつ Virtual Machine を Docker に近い身軽さで起動するものが、AI 生成プログラムなどを動かす Sandbox 的隔離環境として流行っているようです。ところで smolvm は同名のものが複数あって、何が何やらわからないのですが、WSL2 の環境で動くものを AI に選ばせたところ pip で入る smolvm を指名されたので、それを用います。WSL2 のせいか色々制限があるので AI によく聞かれるのがいいと思います。

smolvm のデフォルトの vm は何も入ってない Alpine Linux の最小イメージなので、Python/numpy/matplotlib インストールしたイメージを一回作って保存し、そのイメージを保存して利用することにします。これはすこし太ったイメージなので VM 起動がやや遅くなります。

VM はメモリー的に完全に隔離されて WSL2 では、ReadOnly でも disk 共有ができなかったので、scp でコピーすることで対応します。結果を持ってくるにはこれを使わざるおえないので、まぁいいでしょう。

実行結果

以下では Fortran が Bessel 関数を計算し、バイナリファイルに配列データとして出力します。

次に、curl を使って、AI (GPT-5.4-mini) の API 呼び出しを行い、matplotlib で図を描くプログラムを作れと命じます。API 呼び出しには json が必要になりますが、Linux の jq コマンド利用で切り抜けます。ここで python スクリプトが出来上がります。OpenAI の key を環境変数にセットしておく必要があります。漏洩すると、悪いおじさんが乱用してとんでもない額の請求書がやってくるようです。

最後に、いま生成したスクリプトを smolvm で実行します。あらかじめ smolvm の設定が必要になります。なかなか面倒なので、AI に言われるまま設定する必要となります。動くまでだいぶ苦労しました。smolvm は完全独立したメモリ空間にあるので、データの受け渡しは工夫が必要です。ここでは scp によるファイルコピーで解決しました。初めは、Disk の共有で行こうとしたのですが、WSL2 では無理なようでした。ip アドレスのマスカレード処理もする必要がありましたが、AI の言うがままに行ったため説明できません。気分的にはエドガー・ポーの「アモンティリャードの酒樽」の最後のごとく「アモンティリャード」と繰り返すのみです。

なお、プログラムは主に ChatGPT と Claude の chat mode で作り、時々 Gemini にも聞きました。自力では smolvm 設定や API 呼び出しの json 処理などは到底なしえなかったと思います。

AI に与えたプロンプトと、その指示から生成したスクリプト

見た感じ、複数回実行しても毎回改行以外は同じ Python スクリプトが生成されています。

Read data.bin as raw binary float64 data using numpy.fromfile. The file contains repeated pairs x,y: x0,y0,x1,y1,... . Reshape the array to (-1, 2). Use column 0 as x and column 1 as y. Plot y vs x and save to plot.png. Use matplotlib with the gg backend. Do not use pandas. Output ONLY raw Python code, no fences, no explanation.
import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt

data = np.fromfile("data.bin", dtype=np.float64)
xy = data.reshape(-1, 2)
x = xy[:, 0]
y = xy[:, 1]

plt.plot(x, y)
plt.xlabel("x")
plt.ylabel("y")
plt.savefig("plot.png", dpi=150, bbox_inches="tight")

#プログラム

module ai_openai_mod
    !! Module for generating code through the OpenAI Chat Completions API.
    !! Requires: curl, jq, and the OPENAI_API_KEY environment variable.
    implicit none
    private
    public :: ai_generate_python

    character(len=*), parameter :: DEFAULT_MODEL = "gpt-5.4-mini"
contains
    subroutine ai_generate_python(prompt, outfile, model)
        character(len=*), intent(in)           :: prompt
        character(len=*), intent(in)           :: outfile
        character(len=*), intent(in), optional :: model
        character(len=:), allocatable :: m
        integer :: u

        m = DEFAULT_MODEL
        if (present(model)) m = model

        ! Pass the prompt through a file and let jq read it with --rawfile.
        ! jq handles escaping for quotes, backslashes, and newlines.
        open(newunit=u, file="prompt.txt", status="replace", action="write")
        write(u,'(a)') prompt
        close(u)

        call execute_command_line(                                           &
            'jq -n --arg model "' // m // '" --rawfile content prompt.txt ' // &
            '''{model: $model, messages: [{role: "user", content: $content}]}''' // &
            ' > req.json')

        call execute_command_line(                                           &
            'curl -sS https://api.openai.com/v1/chat/completions '         // &
            '-H "Authorization: Bearer $OPENAI_API_KEY" '                  // &
            '-H "Content-Type: application/json" '                         // &
            '-d @req.json | jq -r ".choices[0].message.content" > ' // outfile)
    end subroutine ai_generate_python
end module ai_openai_mod

program demo
    !! Fortran -> AI (OpenAI) -> Python -> smolvm execution.
    !! This program writes J_0(x) data as raw binary, asks the AI to generate
    !! a plotting Python script, runs it inside a disposable smolvm VM,
    !! and retrieves plot.png from the VM.
    use iso_fortran_env, only : real64
    implicit none

    integer :: u, i, ios
    real(real64) :: x, y
    character(len=*), parameter :: fbin = 'data.bin'
    character(len=*), parameter :: pys  = 'gen.py'

    ! --- 1. Generate data in Fortran ---
    ! data.bin is raw binary float64 data:
    !   x0,y0,x1,y1,x2,y2,...
    open(newunit=u, file=fbin, status='replace', action='write', &
         access='stream', form='unformatted', iostat=ios)
    if (ios /= 0) stop 'cannot open data.bin'

    do i = 0, 200
        x = 0.1_real64 * i
        y = bessel_j0(x)
        write(u) x, y
    end do
    close(u)

    ! --- 2. Ask the AI to generate a Python script ---
 AI:block
        use ai_openai_mod, only: ai_generate_python

        character(len=*), parameter :: PROMPT = &
            "Read data.bin as raw binary float64 data using numpy.fromfile. "  // &
            "The file contains repeated pairs x,y: x0,y0,x1,y1,... . "         // &
            "Reshape the array to (-1, 2). "                                   // &
            "Use column 0 as x and column 1 as y. "                            // &
            "Plot y vs x and save to plot.png. "                               // &
            "Use matplotlib with the Agg backend. Do not use pandas. "         // &
            "Output ONLY raw Python code, no fences, no explanation."

        print *, 'Asking AI...'
        call ai_generate_python(PROMPT, pys)
        print *, '...Finished'
    end block AI

    ! --- 3-10. Run the generated Python script in smolvm and retrieve the result ---
smolvm:block
        character(len=*), parameter :: vm      = 'ftvm'
        character(len=*), parameter :: sshkey  = '~/.smolvm/keys/id_ed25519'
        character(len=*), parameter :: sshopts = &
            '-o StrictHostKeyChecking=no -o BatchMode=yes'

        character(len=4096) :: cmd
        character(len=16)   :: port
        integer :: exitstat

        ! --- 3. Stop and delete any existing VM with the same name ---
        call execute_command_line( &
            'smolvm stop ' // vm // ' >/dev/null 2>&1 || true', exitstat=exitstat)

        call execute_command_line( &
            'smolvm delete ' // vm // ' >/dev/null 2>&1 || true', exitstat=exitstat)

        ! --- 4. Restore the prepared VM disk image ---
        call execute_command_line( &
            'cp ~/.smolvm/images/alpine-with-python.ext4' // &
            ' ~/.local/state/smolvm/disks/' // vm // '.ext4', &
            exitstat=exitstat)

        if (exitstat /= 0) stop 'image restore failed'

        ! --- 5. Start the VM ---
        cmd = 'smolvm create --backend firecracker --name ' // vm // &
              ' --boot-timeout 30'

        print *, trim(cmd)
        call execute_command_line(trim(cmd), exitstat=exitstat)

        if (exitstat /= 0) stop 'smolvm create failed'

        ! --- 6. Detect the forwarded SSH port ---
        call execute_command_line( &
            'smolvm list --json | jq -r ''.data.vms[] | select(.name=="' // vm // &
            '") | .ssh_port'' > .port_tmp', exitstat=exitstat)

        if (exitstat /= 0) stop 'port detection failed'

        open(newunit=u, file='.port_tmp', status='old', action='read', iostat=ios)
        if (ios /= 0) stop 'cannot read port'

        read(u,'(A)') port
        port = adjustl(port)

        close(u)

        call execute_command_line('rm -f .port_tmp', exitstat=exitstat)

        print *, 'SSH port: ', trim(port)

        ! --- 7. Copy the binary data and generated Python script to the VM ---
        cmd = 'scp -i ' // sshkey // ' -P ' // trim(port) // ' ' // sshopts // &
              ' ' // fbin // ' ' // pys // ' root@127.0.0.1:/tmp/'

        print *, trim(cmd)
        call execute_command_line(trim(cmd), exitstat=exitstat)

        if (exitstat /= 0) stop 'scp to vm failed'

        ! --- 8. Run the Python script inside the VM ---
        cmd = 'ssh -i ' // sshkey // ' -p ' // trim(port) // ' ' // sshopts // &
              ' root@127.0.0.1' // &
              ' "cd /tmp && python3 ' // pys // '"'

        print *, trim(cmd)
        call execute_command_line(trim(cmd), exitstat=exitstat)

        if (exitstat /= 0) stop 'python in vm failed'

        ! --- 9. Retrieve the generated plot from the VM ---
        cmd = 'scp -i ' // sshkey // ' -P ' // trim(port) // ' ' // sshopts // &
              ' root@127.0.0.1:/tmp/plot.png ./plot.png'

        print *, trim(cmd)
        call execute_command_line(trim(cmd), exitstat=exitstat)

        if (exitstat /= 0) stop 'scp from vm failed'

        ! --- 10. Stop and delete the VM ---
        call execute_command_line('smolvm stop ' // vm, exitstat=exitstat)

        call execute_command_line( &
            'smolvm delete ' // vm // ' >/dev/null 2>&1', exitstat=exitstat)

        ! --- 11. Remove temporary work files ---
        call execute_command_line('rm -f prompt.txt req.json .port_tmp', exitstat=exitstat)
    end block smolvm

    print *, 'done. host file: plot.png'
end program demo