🤖

マイクラを学習するbotを作る!(1/n)

2024/06/15に公開

概要

目標

Minecraftのマルチサーバーでプレイヤーに教わりながら操作を学習するbotを作る。

方針

  • botの作成、マルチサーバーへのログイン、実際の動作はMineflayerを使用。
  • OpenAI APIのChatGPT4を使用してコードを生成、スキルライブラリに追加。
  • 随時スキルライブラリからコードを呼び出して実行することでマイクラの操作を行わせる。
  • 基本的な仕組みは先行研究であるNVIDIAのVoyagerを参考にする。

環境

  • ローカル端末:MacbookPro 1.4GHz クアッドコアIntel Corei5 メモリ8GB
  • レンタルサーバー:ConohaVPS メモリ8GB CentOS

今回

今回の目標

  • Mineflayerを用いたbotの作成・マルチサーバーへのログイン
  • OpenAI APIとの連携
  • OpenAI APIのChatGPT4を使用してbotを話せる機能の追加
  • その他標準搭載スキルの追加 ◀︎ここまで

今回の方針

  • botを生成・ログインさせる
  • チャット更新を検知して取得する
  • 先頭の文字が「bot,」から始まればbotへのメッセージとして処理する
  • メッセージを個別チャットの場合と全体チャットの場合で分けて処理する
  • 「bot,」に続く文言で各スキルの呼び出しを行う
  • どのスキルにも対応しない文言の場合、普通の会話とみなしてchatGPTに渡す
  • 普通の会話の場合、以前の会話履歴もchatGPTに渡すことで文脈に沿った会話を実現する

とりあえずできたもの

https://youtu.be/IUkIHpb4pB4

ファイル構成

ファイル構成
.
├── prompts
│   └── chat.ts
├── data
│   ├── groupChatData
│   │   └── groupChatHistory.txt
│   └── privateChatData
├── common
│   ├── alwaysRun.ts
│   ├── doTask.ts
│   ├── getResponseFromGPT.ts
│   ├── groupMessage.ts
│   └── privateMessage.ts
├── task
│   ├── attack.ts
│   ├── checkSaturation.ts
│   ├── eat.ts
│   ├── face.ts
│   ├── follow.ts
│   ├── groupChat.ts
│   ├── privateChat.ts
│   ├── run.ts
│   ├── slot.ts
│   └── throw.ts
├── .env
├── .eslintrc.js
├── .gitignore
├── .prettierrc.js
├── bot.ts
├── package-lock.json
├── package.json
├── README.md
└── tsconfig.json

botの生成を行う

botの生成を行うコード
bot.ts
const mineflayer = require('mineflayer');
import dotenv from 'dotenv';

dotenv.config();

const bot = mineflayer.createBot({
  host: process.env.HOST,
  port: Number(process.env.PORT),
  username: process.env.USER_NAME,
  auth: 'microsoft',
  password: process.env.PASSWORD,
});

bot.on('message', async (jsonMsg, position) => {
  //省略
});

各タスクの呼び出しを行う

個別チャットか全体チャットの処理を呼び出す(bot.ts)
bot.ts
import { groupMessage } from './common/groupMessage';
import { privateMessage } from './common/privateMessage';

bot.on('message', async (jsonMsg, position) => {
  const json = jsonMsg.json;
  if (json.translate !== '<%s> %s' && json.translate !== '%s whispers to you: %s') return;
  //全体チャットか個別チャットの形式ではない場合は即return
  const username = json.with[0].text;
  const message = json.with[1].text;

  if (username === bot.username) return;
  //チャット元が自分の場合は場合は即return
  if (json.translate === '<%s> %s') {
    groupMessage(bot, groupChatHistory, username, message, CHAT_SETTINGS);
  }
  if (json.translate === '%s whispers to you: %s') {
    privateMessage(bot, privateChatHistory, username, message, CHAT_SETTINGS);
  }
  return;
});
全体チャットの場合の処理を行う(groupMessage.ts)
groupMessage.ts
import { doTask } from './doTask';

export const groupMessage = async (bot, chatHistory, username, chatContents, CHAT_SETTINGS) => {
  if (chatContents.substring(0, 4) !== 'bot,') return;
  const message = chatContents.substring(4);

  if (message.startsWith('/code')) {
    const task = message.slice(4); // '/code'の部分を除去
    // ここでChatGPT APIを使ってコード生成の予定
  } else {
    doTask(bot, chatHistory, username, CHAT_SETTINGS, message);
  }
};
個別チャットの場合の処理を行う(privateMessage.ts)
privateMessage.ts
import { privateChat } from '../task/privateChat';

export const privateMessage = async (bot, chatHistory, username, message, CHAT_SETTINGS) => {
  if (message.startsWith('/code')) {
    const task = message.slice(4); // '/code'の部分を除去
    // ここでChatGPT APIを使ってコード生成の予定
  } else {
    doTask(bot, chatHistory, username, CHAT_SETTINGS, message);
  }
};
各タスクの呼び出しを行う(doTask.ts)
doTask.ts
import { groupChat } from '../task/groupChat';
import { follow } from '../task/follow';
...

export const doTask = (bot, chatHistory, username, CHAT_SETTINGS, message) => {
  if (message.startsWith('/follow')) {
    follow(bot, username);
  ...
  } else {
    groupChat(bot, chatHistory, username, message, CHAT_SETTINGS);
  }
};

chatGPTを呼び出す

会話履歴の処理を行う(groupChat.ts)
groupChat.ts
import { getResponseFromGPT } from '../common/getResponseFromGPT';
import fs from 'fs/promises';
import PROMPT from '../prompts/chat';

export const groupChat = async (bot, chatHistory, username, message, CHAT_SETTINGS) => {
  // 新しいメッセージをチャット履歴に追加
  chatHistory.push(`${username}: ${message}`);

  const promptList: string[] = [];
  promptList.push(...chatHistory);
  promptList.push(PROMPT);
  const prompt = promptList.join('\n');
  const role = 'user';
  const reply = await getResponseFromGPT(
    role,
    prompt,
    CHAT_SETTINGS.TEMPERATURE,
    CHAT_SETTINGS.MAX_TOKENS
  );

  // ボットの返答をチャット履歴に追加
  chatHistory.push(`you: ${String(reply).replace(/\r?\n/g, '  ')}`);

  while (chatHistory.length > CHAT_SETTINGS.MAX_LINES) {
    chatHistory.shift();
  }

  bot.chat(reply);

  console.log('message:\n', message);
  console.log('reply:\n', reply);

  const filePath = `Data/groupChatData/groupChatHistory.txt`;

  const appendLine = async (filePath, line) => {
    try {
      await fs.appendFile(filePath, `\n${line}`);
    } catch (err) {
      console.error('Error appending file:', err);
    }
  };

  appendLine(filePath, `${username}: ${message}`);
  appendLine(filePath, `you: ${reply}`);
};
chatGPTとの送受信を行う(getResponseFromGPT.ts)
getResponseFromGPT.ts
import dotenv from 'dotenv';
import axios from 'axios';
dotenv.config();

const API_URL = process.env.API_URL ? process.env.API_URL : '';
const MODEL_NAME = process.env.MODEL_NAME;
const API_KEY = process.env.API_KEY;

export const getResponseFromGPT = async (role, prompt, TEMPERATURE, MAX_TOKENS) => {
  const requestBody = {
    messages: [{ role: role, content: prompt }],
    model: MODEL_NAME,
    temperature: TEMPERATURE,
    max_tokens: MAX_TOKENS,
  };
  try {
    const response = await axios.post(API_URL, requestBody, {
      headers: {
        'Content-Type': 'application/json',
        Authorization: 'Bearer ' + API_KEY,
      },
    });
    const reply = response.data.choices[0].message.content;
    return reply;
  } catch (error) {
    console.error(error);
  }
};

github

https://github.com/AIRY-OAR/MINEBOT

Discussion