Intern

Minecraft統合版(BE)でChatGPTに質問してみた

今回はNode-REDを通してゲーム内チャットからChatGPTにプロンプトを送る方法を紹介します。なお、一問一答仕様で対話はできません。

開発環境

PC:Windows 11
※Raspberry Pi 4 Model B (Linux)
開発ツール:Node.js (v18.19.0)
      Node-RED (v.3.1.3)
インターネット:社内の無線LAN
Minecraft:v1.20.51 (執筆時点)
ChatGPT:gpt-3.5-trubo

※Node-REDをRaspberry Piで動かしています。Windowsで動かしていれば不要です。

ChatGPTのAPIキーを取得する

無料か有料か

結論から言うと有料になります。
APIの利用には、最低 5 USD~ のクレジットチャージが必要です。
使用したトークンに応じて利用料が徴収されますが、オートチャージを設定しなければ
チャージが切れてもそれ以上自動で請求されることはありません。

APIキーの取得

OpenAIのAPI管理画面にてログイン後、ワンクリックで発行できます。
発行したAPIキーは再度表示されないので、必ずコピーしてください。
忘れた場合は再発行しましょう。

クレジットについて

残りのクレジットの確認や追加はタブにあるUsage画面から管理できます。
過去の決済履歴等もここに表示されています。
使用限度量に達したときの自動リチャージ設定がありますが、任意で設定してください。

中身の前に注意点

Minecraftのチャットに文字数制限

Minecraft統合版では、チャットに表示できる文字数に制限があり、
日本語入力だと170文字までしか送信できませんでした。これは、ChatGPTの出力テキストも同様で、160数文字でも送信できなかった事例も確認しました。

その影響でチャットが読みにくい

後述しますが、今回はChatGPTの返答を120文字で区切り、
複数回のチャットをNode-REDから飛ばします。
そのため、下記画像の通り文字が読みにくくなってしまっています。

フロー

Simple ChatGPTノードの追加

パレットの管理画面で「chatgpt」と検索し、
「node-red-contrib-simple-chatgpt-instruct」を追加します。

ノードのヘルプ画面を見てみると、msg.payloadからプロンプトを読み取り、
msg.payloadに値を出力していることがわかります。
また、入力されるトークン量の最大値も設定できるようです。

Simple ChatGPTノードの中身

Minecraftからのデータ取得や送信は以前の記事を参考にしてください。

Simple ChatGPTノードはその名の通りシンプルで、
ChatGPT APIキーを指定の欄にペーストするだけですぐに実行できます。
GPTのバージョン指定欄もあるので、変更したい場合は任意で変更しましょう。

フロー全体

はじめのSwitchノードでは、eventName が PlayerMessageであるかどうか。
次のSwitchノードでは、message に [gpt] が含まれているかどうかで分岐させています。
これはBOCCO emoをしゃべらせた時と同じ操作です。

次に続く functionノードも同様に、Minecraftから入力された文字列に手を加え、
結果的にChatGPTに送るプロンプトを書き足しています。
コードはこちらです。

const text = msg.payload.body.message;

const texts = text.replace("[gpt]","300字程度で簡潔に答えて。");

msg.payload = texts;

return msg;

不要な[gpt]部分に書き足したいプロンプトを置き換えています。

また、Minecraftへ1度にチャットを送信できる文字数が170文字以内だったため、
複数のmsgに格納して、分割してチャットを送信する形でノードを組みました。
次に続くfunctionのコードがこちらです。

const text = msg.payload;

let text1 = text.match(/.{1,120}/g);

msg.payload = text1[0];
msg.payload1 = text1;
msg.payload2 = text1;

return msg;

msg.payloadの文字列を120文字で配列として区切り、他名称のmsgに格納しています。

次に続くSwitchノードでは、分割し格納した文字列が入っていればコマンドリクエストを送信するよう「=空でない」を使用しています。

出力文字数を増やしたい場合は、Simple ChatGPTノードの制限と、
配列から取り出すmsgとSwitchノード、Minecraftへのコマンドリクエストとdelayノード
を増設すればより多くの文字数をチャットに流すことができます。

実行結果

ノードデータ

[ { "id": "8581dc05542e20d1", "type": "switch", "z": "bfb86209de0b0d33", "name": "message", "property": "payload.body.message", "propertyType": "msg", "rules": [ { "t": "cont", "v": "[gpt]", "vt": "str" } ], "checkall": "true", "repair": false, "outputs": 1, "x": 560, "y": 860, "wires": [ [ "a933a2296b6f900a" ] ] }, { "id": "b7de3665fba0b822", "type": "switch", "z": "bfb86209de0b0d33", "name": "eventName", "property": "payload.header.eventName", "propertyType": "msg", "rules": [ { "t": "eq", "v": "PlayerMessage", "vt": "str" } ], "checkall": "true", "repair": false, "outputs": 1, "x": 390, "y": 860, "wires": [ [ "8581dc05542e20d1" ] ] }, { "id": "69ac3b3e677021ab", "type": "json", "z": "bfb86209de0b0d33", "name": "", "property": "payload", "action": "", "pretty": true, "x": 230, "y": 860, "wires": [ [ "b7de3665fba0b822" ] ] }, { "id": "312823b8d88df0d2", "type": "websocket in", "z": "bfb86209de0b0d33", "name": "", "server": "c838b41f45982027", "client": "", "x": 90, "y": 860, "wires": [ [ "69ac3b3e677021ab" ] ] }, { "id": "32dcacbea5b59a46", "type": "uuid", "z": "bfb86209de0b0d33", "uuidVersion": "v4", "namespaceType": "", "namespace": "", "namespaceCustom": "", "name": "", "field": "payload", "fieldType": "msg", "x": 310, "y": 720, "wires": [ [ "d9575ffed77c89a6" ] ] }, { "id": "ad514618e3dbd7f6", "type": "inject", "z": "bfb86209de0b0d33", "name": "", "props": [ { "p": "payload" }, { "p": "topic", "vt": "str" } ], "repeat": "", "crontab": "", "once": false, "onceDelay": 0.1, "topic": "", "payload": "", "payloadType": "date", "x": 140, "y": 720, "wires": [ [ "32dcacbea5b59a46" ] ] }, { "id": "d9575ffed77c89a6", "type": "function", "z": "bfb86209de0b0d33", "name": "PlayerMessageCmd", "func": "const uuid = msg.payload;\n\nconst subscribeMessageJSON = {\n    \"header\": {\n        \"version\": 1, // プロトコルのバージョンを指定。1.18.2の時点では1で問題ない\n        \"requestId\": uuid, // UUIDv4を指定\n        \"messageType\": \"commandRequest\",  // \"commandRequest\" を指定\n        \"messagePurpose\": \"subscribe\", // \"subscribe\" を指定\n    },\n    \"body\": {\n        \"eventName\": \"PlayerMessage\" // イベント名を指定。イベント名は後述\n    },\n};\n\n    // イベント購読用のJSONをシリアライズ(文字列化)して送信\nmsg.payload= JSON.stringify(subscribeMessageJSON);\n\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 500, "y": 720, "wires": [ [ "5616ef5b6c1eaef1" ] ] }, { "id": "5616ef5b6c1eaef1", "type": "websocket out", "z": "bfb86209de0b0d33", "name": "", "server": "c838b41f45982027", "client": "", "x": 700, "y": 720, "wires": [] }, { "id": "a6fc2573247fcee8", "type": "comment", "z": "bfb86209de0b0d33", "name": "Minecraftからチャットデータを受信", "info": "", "x": 180, "y": 660, "wires": [] }, { "id": "989a4ef3ec605ab1", "type": "comment", "z": "bfb86209de0b0d33", "name": "GPTから返答", "info": "", "x": 110, "y": 800, "wires": [] }, { "id": "d77ab2035d0e1ea1", "type": "simple-gpt-instruct", "z": "bfb86209de0b0d33", "name": "", "Token": "", "Model": "", "lengthType": "num", "maxLength": "360", "x": 290, "y": 940, "wires": [ [ "1e43073aca2b156b" ] ] }, { "id": "32b89de47a7b5dd3", "type": "uuid", "z": "bfb86209de0b0d33", "uuidVersion": "v4", "namespaceType": "", "namespace": "", "namespaceCustom": "", "name": "", "field": "paypay", "fieldType": "msg", "x": 770, "y": 940, "wires": [ [ "b3204a070e66c0ce" ] ] }, { "id": "f74a92924d6a135e", "type": "websocket out", "z": "bfb86209de0b0d33", "name": "", "server": "c838b41f45982027", "client": "", "x": 1240, "y": 940, "wires": [] }, { "id": "a933a2296b6f900a", "type": "function", "z": "bfb86209de0b0d33", "name": "function 26", "func": "const text = msg.payload.body.message;\n\nconst texts = text.replace(\"[gpt]\",\"300字程度で簡潔に答えて。\");\n\nmsg.payload = texts;\n\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 110, "y": 940, "wires": [ [ "d77ab2035d0e1ea1" ] ] }, { "id": "1e43073aca2b156b", "type": "function", "z": "bfb86209de0b0d33", "name": "function 27", "func": "const text = msg.payload;\n\nlet text1 = text.match(/.{1,120}/g);\n\nmsg.payload = text1[0];\nmsg.payload1 = text1;\nmsg.payload2 = text1;\n\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 470, "y": 940, "wires": [ [ "32b89de47a7b5dd3", "0abe1b8f3880cf5a", "0519eb82d9321817" ] ] }, { "id": "0abe1b8f3880cf5a", "type": "switch", "z": "bfb86209de0b0d33", "name": "", "property": "payload1", "propertyType": "msg", "rules": [ { "t": "nempty" } ], "checkall": "true", "repair": false, "outputs": 1, "x": 630, "y": 1000, "wires": [ [ "70052c57ee7880f9" ] ] }, { "id": "70052c57ee7880f9", "type": "uuid", "z": "bfb86209de0b0d33", "uuidVersion": "v4", "namespaceType": "", "namespace": "", "namespaceCustom": "", "name": "", "field": "paypay", "fieldType": "msg", "x": 770, "y": 1000, "wires": [ [ "252470488644b358" ] ] }, { "id": "252470488644b358", "type": "function", "z": "bfb86209de0b0d33", "name": "function 28", "func": "const message = msg.payload1;\nconst uuid = msg.paypay;\n\nconst commandRequestMessageJSON = {\n    \"header\": {\n        \"version\": 1, // プロトコルのバージョン1.18.2時点では1でOK\n        \"requestId\": uuid, // UUIDv4を生成して指定\n        \"messageType\": \"commandRequest\", // commandRequestを指定\n        \"messagePurpose\": \"commandRequest\", // commandRequestを指定\n    },\n    \"body\": {\n        \"origin\": {\n            \"type\": \"player\" // 誰がコマンドを実行するかを指定(ただし、Player以外にどの値が利用可能かは要調査)\n        },\n        \"version\": 1, // プロトコルのバージョン1.18.2時点では1でOK\n        \"commandLine\": `say ${message}`, // マイクラで実行したいコマンドを指定(ここではニワトリをスポーンさせるコマンドを指定)\n    }\n};\n\nmsg.payload = JSON.stringify(commandRequestMessageJSON);\n\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 910, "y": 1000, "wires": [ [ "f5875070d577f1b5" ] ] }, { "id": "f5875070d577f1b5", "type": "delay", "z": "bfb86209de0b0d33", "name": "", "pauseType": "delay", "timeout": "0.2", "timeoutUnits": "seconds", "rate": "1", "nbRateUnits": "1", "rateUnits": "second", "randomFirst": "1", "randomLast": "5", "randomUnits": "seconds", "drop": false, "allowrate": false, "outputs": 1, "x": 1070, "y": 1000, "wires": [ [ "f74a92924d6a135e" ] ] }, { "id": "b3204a070e66c0ce", "type": "function", "z": "bfb86209de0b0d33", "name": "send chat", "func": "const message = msg.payload\nconst uuid = msg.paypay;\n\nconst commandRequestMessageJSON = {\n    \"header\": {\n        \"version\": 1, // プロトコルのバージョン1.18.2時点では1でOK\n        \"requestId\": uuid, // UUIDv4を生成して指定\n        \"messageType\": \"commandRequest\", // commandRequestを指定\n        \"messagePurpose\": \"commandRequest\", // commandRequestを指定\n    },\n    \"body\": {\n        \"origin\": {\n            \"type\": \"player\" // 誰がコマンドを実行するかを指定(ただし、Player以外にどの値が利用可能かは要調査)\n        },\n        \"version\": 1, // プロトコルのバージョン1.18.2時点では1でOK\n        \"commandLine\": `say ${message}`, // マイクラで実行したいコマンドを指定(ここではニワトリをスポーンさせるコマンドを指定)\n    }\n};\n\nmsg.payload = JSON.stringify(commandRequestMessageJSON);\n\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 900, "y": 940, "wires": [ [ "f74a92924d6a135e" ] ] }, { "id": "0519eb82d9321817", "type": "switch", "z": "bfb86209de0b0d33", "name": "", "property": "payload2", "propertyType": "msg", "rules": [ { "t": "nempty" } ], "checkall": "true", "repair": false, "outputs": 1, "x": 630, "y": 1060, "wires": [ [ "f2882f68d57ecab8" ] ] }, { "id": "f2882f68d57ecab8", "type": "uuid", "z": "bfb86209de0b0d33", "uuidVersion": "v4", "namespaceType": "", "namespace": "", "namespaceCustom": "", "name": "", "field": "paypay", "fieldType": "msg", "x": 770, "y": 1060, "wires": [ [ "656581e9b6319001" ] ] }, { "id": "656581e9b6319001", "type": "function", "z": "bfb86209de0b0d33", "name": "function 29", "func": "const message = msg.payload2;\nconst uuid = msg.paypay;\n\nconst commandRequestMessageJSON = {\n    \"header\": {\n        \"version\": 1, // プロトコルのバージョン1.18.2時点では1でOK\n        \"requestId\": uuid, // UUIDv4を生成して指定\n        \"messageType\": \"commandRequest\", // commandRequestを指定\n        \"messagePurpose\": \"commandRequest\", // commandRequestを指定\n    },\n    \"body\": {\n        \"origin\": {\n            \"type\": \"player\" // 誰がコマンドを実行するかを指定(ただし、Player以外にどの値が利用可能かは要調査)\n        },\n        \"version\": 1, // プロトコルのバージョン1.18.2時点では1でOK\n        \"commandLine\": `say ${message}`, // マイクラで実行したいコマンドを指定(ここではニワトリをスポーンさせるコマンドを指定)\n    }\n};\n\nmsg.payload = JSON.stringify(commandRequestMessageJSON);\n\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 910, "y": 1060, "wires": [ [ "b38b706a0b29505b" ] ] }, { "id": "b38b706a0b29505b", "type": "delay", "z": "bfb86209de0b0d33", "name": "", "pauseType": "delay", "timeout": "0.4", "timeoutUnits": "seconds", "rate": "1", "nbRateUnits": "1", "rateUnits": "second", "randomFirst": "1", "randomLast": "5", "randomUnits": "seconds", "drop": false, "allowrate": false, "outputs": 1, "x": 1070, "y": 1060, "wires": [ [ "f74a92924d6a135e" ] ] }, { "id": "c838b41f45982027", "type": "websocket-listener", "path": "ws", "wholemsg": "false" } ]