はじめに
自分のマイクラサーバーでChatGPTやりたくて作りました。
OpenAIのJavaSDKを使って、Spigot用プラグインを作成します。
Java版のマイクラからこのように使えます。
間違った回答を出してますけどね。
また、Function Callingを使って、ワールドの情報についても答えられるようにしました。
自分のマイクラサーバー
はいってもすぐには使えません。
APIの利用料がかかるので、権限を付与されたプレイヤーしか使えません。
苦労した点
Java版マニュアルない!PythonとかJS版のマニュアル見て推測する!
使い方をChatGPTに聞いても古いのしか出てこない!
ポイント
マイクラ内からChatGPTと対話する。
最新のResponseAPIを使う。
OpenAIのJavaSDKを使う。
実装
Gradle
Gradleを使います。
dependencies {
compileOnly 'org.spigotmc:spigot-api:1.21.4-R0.1-SNAPSHOT'
implementation 'com.openai:openai-java:1.4.1'
}
リクエスト準備
クライアントを作成します。
OpenAIClient client = OpenAIOkHttpClient
.builder()
.apiKey("your-api-key")
.build();
ResponseAPI用のパラメータを作成。
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を渡します。
if (lastChatId) {
builder.previousResponseId(lastChatId);
}
Function Calling
Function Calling用の関数を作成し、パラメータに追加します。ここに挙げた関数は、ワールド名を引数にとり、ワールドの説明文を返すものになります。
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);
}
リクエスト送信
マイクラはシングルスレッドなので、別スレッドで以降のリクエスト処理を行います。
plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> {
// 以降の内容
});
リクエストを送信します。
var response = client.responses().create(builder.build());
トークン使用量などを取得します。ここでは載せませんが、これを保存して使用料制限などに使います。
long inputTokens = response
.usage()
.stream()
.mapToLong(ResponseUsage::inputTokens)
.sum();
long outputTokens = response
.usage()
.stream()
.mapToLong(ResponseUsage::outputTokens)
.sum();
ここで、Function Callingの呼び出しがあれば、それを実行します。
var functionResults = response
.output()
.stream()
.flatMap(item -> item.functionCall().stream())
.map(toolCall -> /* ここで指定された関数を実行する */)
.map(ResponseInputItem::ofFunctionCallOutput)
.toList();
実行結果がもしあれば、再度リクエストを送信します。ここでもトークン使用量を取得します。
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の返答を取得します。
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の返答を表示します。
plugin.getServer().getScheduler().runTask(plugin, () -> {
sender.sendMessage(ChatColor.AQUA + "[AI] " + ChatColor.RESET + result);
});
これで以上になります。
おわりに
苦労はしましたが、けっこう簡潔に作れました。
自分でAPIにリクエスト飛ばすよりかなり簡単だと思います。