mindcraft/src/agent/agent.js
Lawtro37 f827bb0a20
tell the AI what dimension it died in
hopefully this should help it not try to find its stuff in the nether in the otherworld.
2024-10-30 22:18:17 +10:00

317 lines
13 KiB
JavaScript

import { History } from './history.js';
import { Coder } from './coder.js';
import { Prompter } from './prompter.js';
import { initModes } from './modes.js';
import { initBot } from '../utils/mcdata.js';
import { containsCommand, commandExists, executeCommand, truncCommandMessage, isAction } from './commands/index.js';
import { NPCContoller } from './npc/controller.js';
import { MemoryBank } from './memory_bank.js';
import { SelfPrompter } from './self_prompter.js';
import { handleTranslation, handleEnglishTranslation } from '../utils/translator.js';
import { addViewer } from './viewer.js';
import settings from '../../settings.js';
export class Agent {
async start(profile_fp, load_mem=false, init_message=null, count_id=0) {
this.prompter = new Prompter(this, profile_fp);
this.name = this.prompter.getName();
this.history = new History(this);
this.coder = new Coder(this);
this.npc = new NPCContoller(this);
this.memory_bank = new MemoryBank();
this.self_prompter = new SelfPrompter(this);
await this.prompter.initExamples();
console.log('Logging in...');
this.bot = initBot(this.name);
initModes(this);
let save_data = null;
if (load_mem) {
save_data = this.history.load();
}
this.bot.once('spawn', async () => {
addViewer(this.bot, count_id);
// wait for a bit so stats are not undefined
await new Promise((resolve) => setTimeout(resolve, 1000));
console.log(`${this.name} spawned.`);
this.coder.clear();
const ignore_messages = [
"Set own game mode to",
"Set the time to",
"Set the difficulty to",
"Teleported ",
"Set the weather to",
"Gamerule "
];
const eventname = settings.profiles.length > 1 ? 'whisper' : 'chat';
this.bot.on(eventname, async (username, message) => {
if (username === this.name) return;
if (ignore_messages.some((m) => message.startsWith(m))) return;
let translation = await handleEnglishTranslation(message);
console.log('received message from', username, ':', translation);
this.shut_up = false;
this.handleMessage(username, translation);
});
// set the bot to automatically eat food when hungry
this.bot.autoEat.options = {
priority: 'foodPoints',
startAt: 14,
bannedFood: ["rotten_flesh", "spider_eye", "poisonous_potato", "pufferfish", "chicken"]
};
if (save_data && save_data.self_prompt) { // if we're loading memory and self-prompting was on, restart it, ignore init_message
let prompt = save_data.self_prompt;
// add initial message to history
this.history.add('system', prompt);
this.self_prompter.start(prompt);
}
else if (init_message) {
this.handleMessage('system', init_message, 2);
}
else {
const translation = await handleTranslation("Hello world! I am "+this.name);
this.bot.chat(translation);
this.bot.emit('finished_executing');
}
this.startEvents();
});
}
async cleanChat(message, translate_up_to=-1) {
let to_translate = message;
let remainging = '';
if (translate_up_to != -1) {
to_translate = to_translate.substring(0, translate_up_to);
remainging = message.substring(translate_up_to);
}
message = (await handleTranslation(to_translate)).trim() + " " + remainging;
// newlines are interpreted as separate chats, which triggers spam filters. replace them with spaces
message = message.replaceAll('\n', ' ');
return this.bot.chat(message);
}
shutUp() {
this.shut_up = true;
if (this.self_prompter.on) {
this.self_prompter.stop(false);
}
}
async handleMessage(source, message, max_responses=null) {
let used_command = false;
if (max_responses === null) {
max_responses = settings.max_commands === -1 ? Infinity : settings.max_commands;
}
if (max_responses === -1){
max_responses = Infinity;
}
let self_prompt = source === 'system' || source === this.name;
if (!self_prompt) {
const user_command_name = containsCommand(message);
if (user_command_name) {
if (!commandExists(user_command_name)) {
this.bot.chat(`Command '${user_command_name}' does not exist.`);
return false;
}
this.bot.chat(`*${source} used ${user_command_name.substring(1)}*`);
if (user_command_name === '!newAction') {
// all user initiated commands are ignored by the bot except for this one
// add the preceding message to the history to give context for newAction
this.history.add(source, message);
}
let execute_res = await executeCommand(this, message);
if (execute_res)
this.cleanChat(execute_res);
return true;
}
}
const checkInterrupt = () => this.self_prompter.shouldInterrupt(self_prompt) || this.shut_up;
let behavior_log = this.bot.modes.flushBehaviorLog();
if (behavior_log.trim().length > 0) {
const MAX_LOG = 500;
if (behavior_log.length > MAX_LOG) {
behavior_log = '...' + behavior_log.substring(behavior_log.length - MAX_LOG);
}
behavior_log = 'Recent behaviors log: \n' + behavior_log.substring(behavior_log.indexOf('\n'));
await this.history.add('system', behavior_log);
}
await this.history.add(source, message);
this.history.save();
if (!self_prompt && this.self_prompter.on) // message is from user during self-prompting
max_responses = 1; // force only respond to this message, then let self-prompting take over
for (let i=0; i<max_responses; i++) {
if (checkInterrupt()) break;
let history = this.history.getHistory();
let res = await this.prompter.promptConvo(history);
let command_name = containsCommand(res);
if (command_name) { // contains query or command
console.log(`Full response: ""${res}""`)
res = truncCommandMessage(res); // everything after the command is ignored
this.history.add(this.name, res);
if (!commandExists(command_name)) {
this.history.add('system', `Command ${command_name} does not exist.`);
console.warn('Agent hallucinated command:', command_name)
continue;
}
if (command_name === '!stopSelfPrompt' && self_prompt) {
this.history.add('system', `Cannot stopSelfPrompt unless requested by user.`);
continue;
}
if (checkInterrupt()) break;
this.self_prompter.handleUserPromptedCmd(self_prompt, isAction(command_name));
if (settings.verbose_commands) {
this.cleanChat(res, res.indexOf(command_name));
}
else { // only output command name
let pre_message = res.substring(0, res.indexOf(command_name)).trim();
let chat_message = `*used ${command_name.substring(1)}*`;
if (pre_message.length > 0)
chat_message = `${pre_message} ${chat_message}`;
this.cleanChat(res);
}
let execute_res = await executeCommand(this, res);
console.log('Agent executed:', command_name, 'and got:', execute_res);
used_command = true;
if (execute_res)
this.history.add('system', execute_res);
else
break;
}
else { // conversation response
this.history.add(this.name, res);
this.cleanChat(res);
console.log('Purely conversational response:', res);
break;
}
this.history.save();
}
this.bot.emit('finished_executing');
return used_command;
}
startEvents() {
// Custom events
this.bot.on('time', () => {
if (this.bot.time.timeOfDay == 0)
this.bot.emit('sunrise');
else if (this.bot.time.timeOfDay == 6000)
this.bot.emit('noon');
else if (this.bot.time.timeOfDay == 12000)
this.bot.emit('sunset');
else if (this.bot.time.timeOfDay == 18000)
this.bot.emit('midnight');
});
let prev_health = this.bot.health;
this.bot.lastDamageTime = 0;
this.bot.lastDamageTaken = 0;
this.bot.on('health', () => {
if (this.bot.health < prev_health) {
this.bot.lastDamageTime = Date.now();
this.bot.lastDamageTaken = prev_health - this.bot.health;
}
prev_health = this.bot.health;
});
// Logging callbacks
this.bot.on('error' , (err) => {
console.error('Error event!', err);
});
this.bot.on('end', (reason) => {
console.warn('Bot disconnected! Killing agent process.', reason)
this.cleanKill('Bot disconnected! Killing agent process.');
});
this.bot.on('death', () => {
this.coder.cancelResume();
this.coder.stop();
});
this.bot.on('kicked', (reason) => {
console.warn('Bot kicked!', reason);
this.cleanKill('Bot kicked! Killing agent process.');
});
this.bot.on('messagestr', async (message, _, jsonMsg) => {
if (jsonMsg.translate && jsonMsg.translate.startsWith('death') && message.startsWith(this.name)) {
console.log('Agent died: ', message);
let death_pos = this.bot.entity.position;
this.memory_bank.rememberPlace('last death position', death_pos);
let death_pos_text = null;
if (death_pos) {
death_pos_text = `x: ${death_pos.x.toFixed(2)}, y: ${death_pos.y.toFixed(2)}, z: ${death_pos.x.toFixed(2)}`;
}
let dimention = this.bot.game.dimension;
this.history.add('system', `You died at position ${death_pos_text || "unknown"} with the final message: '${message}'. Previous actions were stopped and you have since respawned.`);
this.handleMessage('system', `You died at position ${death_pos_text || "unknown"} in the dimention ${dimention} with the final message: '${message}'. Previous actions were stopped and you have respawned. Notify the user and perform any necessary actions. use !goToDeath to return to death position. (this will automaticly take you to your death position and pick up your items)`);
}
});
this.bot.on('idle', () => {
this.bot.clearControlStates();
this.bot.pathfinder.stop(); // clear any lingering pathfinder
this.bot.modes.unPauseAll();
this.coder.executeResume();
});
// Init NPC controller
this.npc.init();
// This update loop ensures that each update() is called one at a time, even if it takes longer than the interval
const INTERVAL = 300;
let last = Date.now();
setTimeout(async () => {
while (true) {
let start = Date.now();
await this.update(start - last);
let remaining = INTERVAL - (Date.now() - start);
if (remaining > 0) {
await new Promise((resolve) => setTimeout(resolve, remaining));
}
last = start;
}
}, INTERVAL);
this.bot.emit('idle');
}
async update(delta) {
await this.bot.modes.update();
await this.self_prompter.update(delta);
}
isIdle() {
return !this.coder.executing && !this.coder.generating;
}
cleanKill(msg='Killing agent process...') {
this.history.add('system', msg);
this.bot.chat('Goodbye world.')
this.history.save();
process.exit(1);
}
}