Python Tips: OpenAI の GPT モデルの Structured Outputs 機能を使って WordPress プラグインを自動生成

2024/08/06 にリリースされた OpenAI の GPT モデルの Structured Outputs 機能を使って WordPress プラグインを自動生成してみました。

改善の余地はたくさんありますが、いい感じに動いたので手順をまとめておきます。

前提

  • Python >=3.12
  • OpenAI API キーを取得済み

準備

venv を作って PyPI パッケージ openaiclick をインストールします。

# venv を作成して有効化:
python -m venv .venv
. .venv/bin/activate

# `openai` と `click` をインストール:
pip install 'openai~=1.40' 'click~=8.1' 

click は CLI でパラメータを渡しやすくするためだけに導入しました。 OpenAI の API の利用に必要なわけではありません。

スクリプト作成

スクリプトを作成します。 名前は何でもよいですが仮に main.py とします。

main.py:

"""OpenAI の GPT モデルに WordPress プラグインを生成してもらう"""

import os
import sys
from pathlib import Path

import click
from openai import OpenAI
from pydantic import BaseModel

MODEL_DEFAULT = "gpt-4o-2024-08-06"

SYSTEM_PROMPT = """
You are an experienced WordPress developer tasked with creating a new plugin. Please provide all files for a WordPress plugin.

Assuming the current directory is the plugin directory `wp-content/plugins`, please specify the paths of each file using relative paths.
""".strip()

DIR_PARENT = Path(__file__).parent
DIR_OUT = DIR_PARENT / "out"


class File(BaseModel):
    path: str
    content: str


class PluginResponse(BaseModel):
    name: str
    description: str
    files: list[File]


@click.command()
@click.option("--name", required=True)
@click.option("--prompt", required=True)
@click.option("--model", default=MODEL_DEFAULT)
def main(name: str, prompt: str, model: str):
    if (api_key := os.environ.get("OPENAI_API_KEY")) is None:
        sys.exit("Environment variable `OPENAI_API_KEY` is required.")

    print(f"Asking {model}...")
    messages = [
        {"role": "system", "content": SYSTEM_PROMPT},
        {"role": "user", "content": f"Create a plugin named {name}"},
        {"role": "user", "content": prompt},
    ]
    client = OpenAI(api_key=api_key)
    completion = client.beta.chat.completions.parse(
        model=model,
        messages=messages,
        response_format=PluginResponse,
    )

    message = completion.choices[0].message
    if message.refusal:
        sys.exit(str(completion))

    plugin = message.parsed
    assert plugin
    pprint_plugin(plugin)

    click.confirm("Proceed to generate files?", default=None, abort=True)

    parent = DIR_OUT
    if not parent.is_dir():
        parent.mkdir(parents=True)
    make_plugin_files(parent, plugin)

    print("Plugin generated.")


def pprint_plugin(plugin: PluginResponse):
    print(f"Name: {plugin.name}")
    print(f"Descpription: {plugin.description}")
    print(f"Files to generate:\n", "\n".join(x.path for x in plugin.files))


def make_plugin_files(parent: Path, plugin: PluginResponse):
    assert parent.is_dir(), f"Parent is not a dir: {parent}"

    for file in plugin.files:
        path = parent / file.path
        if not path.parent.is_dir():
            path.parent.mkdir(parents=True)
        path.write_text(file.content)


if __name__ == "__main__":
    main()

Structured Outputs ( response_format でのフォーマット指定)を使って、プラグインの生成に必要なファイルのパスと中身を指定してもらうようにしました。

Structured Outputs の導入前は、出力フォーマットを厳密に守ってもらうためには長文のプロンプトを記述する必要がありました。 また、せっかくプロンプトを書いても指示を守ってくれないことがあり、試行錯誤を繰り返す必要がありました。 Structured Outputs を使うことで出力フォーマットに関するプロンプトエンジニアリングの手間が大幅に削減できます。

実行

環境変数 OPENAI_API_KEY に API キーをセットした上でスクリプトを実行します。

# 環境変数に API キーをセット:
export OPENAI_API_KEY=...

# スクリプトを実行:
python main.py \
  --name='disable-search' \
  --prompt='Make a plugin to disable the WordPress core search function.'

実行後少し待てば生成すべきプラグインの情報とファイルの一覧が表示されます。 そして

Proceed to generate files? [y/n]

というダイアログが表示されるので、 y を入力すれば一連のファイルが生成されます。 出力先は main.py の隣の out/ ディレクトリ以下です。

出力サンプル:

Asking gpt-4o-2024-08-06...
Name: Disable Search
Descpription: A WordPress plugin to disable the core search functionality by intercepting search queries and returning 404 status code for search results pages.
Files to generate:
 disable-search/disable-search.php
disable-search/readme.txt
Proceed to generate files? [y/n]: y
Plugin generated.

生成されたプラグインのサンプル:

<?php
/**
 * Plugin Name: Disable Search
 * Description: Disables the core search functionality in WordPress.
 * Version: 1.0.0
 * Author: Your Name
 * License: GPL2
 * Text Domain: disable-search
 */

// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

/**
 * Main plugin class.
 */
class Disable_Search {

    /**
     * Constructor
     */
    public function __construct() {
        add_action( 'template_redirect', array( $this, 'disable_search_results' ) );
    }

    /**
     * Disable search results.
     *
     * If a search query is detected, sends a 404 status header and loads the 404 template.
     */
    public function disable_search_results() {
        if ( is_search() && ! is_admin() ) {
            global $wp_query;
            $wp_query->set_404();
            status_header( 404 );
            nocache_headers();
            include( get_404_template() );
            exit;
        }
    }
}

// Initialize the plugin
new Disable_Search();

システムプロンプトを改善したコードを GitHub に置きました。 参考にされる方は覗いてみてください。

dynojp/gpt-wordpress-plugin-generator: A script to generate WordPress plugins using the OpenAI ChatGPT API.

参考