OpenAIのJavaSDKを使って、マインクラフトJava版のサーバーSpigotでChatGPTが使えるプラグインを作る

# はじめに

自分のマイクラサーバーでChatGPTやりたくて作りました。

OpenAIのJavaSDKを使って、Spigot用プラグインを作成します。

Java版のマイクラからこのように使えます。

![2025-06-15_20.54.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/27bf9997-7665-4411-9141-80786b817151.png)

間違った回答を出してますけどね。

また、Function Callingを使って、ワールドの情報についても答えられるようにしました。

![2025-06-15_21.37.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/24266dba-058d-4bce-b453-2d6a77160f72.png)


# 自分のマイクラサーバー

<a href="https://minecraft.jp/servers/houkiserver.com"><img src="https://minecraft.jp/servers/houkiserver.com/banner/1/560x95.png"/></a>

はいってもすぐには使えません。

APIの利用料がかかるので、権限を付与されたプレイヤーしか使えません。

# 苦労した点

Java版マニュアルない！PythonとかJS版のマニュアル見て推測する！

使い方をChatGPTに聞いても古いのしか出てこない！

# ポイント

マイクラ内からChatGPTと対話する。

最新のResponseAPIを使う。

OpenAIのJavaSDKを使う。

# 実装

## Gradle

Gradleを使います。

```build.gradle
dependencies {
    compileOnly 'org.spigotmc:spigot-api:1.21.4-R0.1-SNAPSHOT'
    implementation 'com.openai:openai-java:1.4.1'
}
```

## リクエスト準備

クライアントを作成します。

```java
OpenAIClient client = OpenAIOkHttpClient
        .builder()
        .apiKey("your-api-key")
        .build();
```

ResponseAPI用のパラメータを作成。

```java
long maxTokens = 500;
String systemPrompt = "あなたはマインクラフトの相談役ですホニャラララ";
String prompt = "プレイヤーが入力した内容";
ResponseCreateParams.Builder builder = ResponseCreateParams.builder()
        .instructions(systemPrompt)
        .input(prompt)
        .model(ChatModel.GPT_4_1_MINI)
        .maxOutputTokens(maxTokens);
```


次に、会話の続きであるならば、前回のやりとりのIDを渡します。

```java
if (lastChatId) {
    builder.previousResponseId(lastChatId);
}
```

### Function Calling

Function Calling用の関数を作成し、パラメータに追加します。ここに挙げた関数は、ワールド名を引数にとり、ワールドの説明文を返すものになります。

```java
builder.addTool(createTool());

public Tool createTool() {
    // WorldHelpはenum
    var worldList = WorldHelp.values();
    var worldListText = Arrays.stream(worldList).map(Enum::name).collect(Collectors.joining(", "));
    var param = FunctionTool
            .Parameters
            .builder()
            .putAdditionalProperty("type", JsonValue.from("object"))
            .putAdditionalProperty(
                    "properties",
                    JsonValue.from(Map.of("world_name", Map.of("type", "string", "enum", worldList))))
            .putAdditionalProperty("required", JsonValue.from(List.of("world_name")))
            .putAdditionalProperty("additionalProperties", JsonValue.from(false))
            .build();
    var func = FunctionTool
            .builder()
            .name(name)
            .description("Get world help. Available world is: " + worldListText)
            .strict(true)
            .parameters(param)
            .build();

    return Tool.ofFunction(func);
}
```

## リクエスト送信

マイクラはシングルスレッドなので、別スレッドで以降のリクエスト処理を行います。

```java
plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> {
    // 以降の内容
});
```

リクエストを送信します。

```java
    var response = client.responses().create(builder.build());
```

トークン使用量などを取得します。ここでは載せませんが、これを保存して使用料制限などに使います。

```java
    long inputTokens = response
            .usage()
            .stream()
            .mapToLong(ResponseUsage::inputTokens)
            .sum();
    long outputTokens = response
            .usage()
            .stream()
            .mapToLong(ResponseUsage::outputTokens)
            .sum();
```

ここで、Function Callingの呼び出しがあれば、それを実行します。

```java
    var functionResults = response
            .output()
            .stream()
            .flatMap(item -> item.functionCall().stream())
            .map(toolCall -> /* ここで指定された関数を実行する */)
            .map(ResponseInputItem::ofFunctionCallOutput)
            .toList();
```

実行結果がもしあれば、再度リクエストを送信します。ここでもトークン使用量を取得します。

```java
    if (!functionResults.isEmpty()) {
        builder.inputOfResponse(functionResults);
        builder.previousResponseId(response.id());
        response = client.responses().create(builder.build());

        inputTokens = response
                .usage()
                .stream()
                .mapToLong(ResponseUsage::inputTokens)
                .sum();
        outputTokens = response
                .usage()
                .stream()
                .mapToLong(ResponseUsage::outputTokens)
                .sum()
    }
```

そして、AIの返答を取得します。

```java
    String result = response
            .output()
            .stream()
            .flatMap(item -> item.message().stream())
            .flatMap(message -> message.content().stream())
            .flatMap(content -> content.outputText().stream())
            .map(ResponseOutputText::text)
            .collect(Collectors.joining("\n"));
```

最後に、メインスレッドに処理を戻して、プレイヤーにAIの返答を表示します。

```java
    plugin.getServer().getScheduler().runTask(plugin, () -> {
        sender.sendMessage(ChatColor.AQUA + "[AI] " + ChatColor.RESET + result);
    });
```

これで以上になります。


# おわりに

苦労はしましたが、けっこう簡潔に作れました。

自分でAPIにリクエスト飛ばすよりかなり簡単だと思います。