From a7e529b1e94ffb22e1081c4717d721e35df944ce Mon Sep 17 00:00:00 2001 From: Isadora White Date: Sun, 8 Dec 2024 22:10:21 -0800 Subject: [PATCH 01/10] task specification, validation function, evaluation script, and initial inventories and teleportation --- .gitignore | 3 + evaluate.sh | 67 +++++++++++ main.js | 106 ++++++++++++++++- multiagent_prompt_desc.json | 19 +++ package.json | 1 + src/agent/agent.js | 213 +++++++++++++++++++++++++++++++++- src/agent/commands/index.js | 7 +- src/agent/prompter.js | 11 ++ src/process/agent-process.js | 23 +++- src/process/init-agent.js | 7 +- src/utils/tasks.js | 52 +++++++++ task_andy.json | 23 ++++ task_randy.json | 0 tasks.yaml | 52 +++++++++ tasks/construction_tasks.yaml | 62 ++++++++++ tasks/debug_tasks.yaml | 46 ++++++++ tasks/multiagent_tasks.yaml | 69 +++++++++++ 17 files changed, 746 insertions(+), 15 deletions(-) create mode 100755 evaluate.sh create mode 100644 multiagent_prompt_desc.json create mode 100644 src/utils/tasks.js create mode 100644 task_andy.json create mode 100644 task_randy.json create mode 100644 tasks.yaml create mode 100644 tasks/construction_tasks.yaml create mode 100644 tasks/debug_tasks.yaml create mode 100644 tasks/multiagent_tasks.yaml diff --git a/.gitignore b/.gitignore index 1e92ae7..f10fb01 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,6 @@ services/viaproxy/plugins/** services/viaproxy/ViaLoader/** services/viaproxy/saves.json services/viaproxy/viaproxy.yml +profiles/task_* +multi_agent_task* +*_results.txt diff --git a/evaluate.sh b/evaluate.sh new file mode 100755 index 0000000..b1cd875 --- /dev/null +++ b/evaluate.sh @@ -0,0 +1,67 @@ +#!/bin/bash + +# Initialize variables +args=() +num_experiments=0 +successful=0 +unsuccessful=0 +error=0 + +# Parse arguments +while [[ $# -gt 0 ]]; do + case "$1" in + --num_experiments) + num_experiments="$2" + shift 2 + ;; + *) + args+=("$1") + shift + ;; + esac +done + +# Validate num_experiments +if ! [[ "$num_experiments" =~ ^[0-9]+$ ]] || [[ "$num_experiments" -eq 0 ]]; then + echo "Error: num_experiments must be a positive integer" + echo "Usage: $0 --num_experiments [other arguments]" + exit 1 +fi + +# Run experiments +while (( successful + unsuccessful < num_experiments )); do + node main.js "${args[@]}" + exit_code=$? + + case $exit_code in + 2) ((successful++));; + 3) ((unsuccessful++));; + 4) ((error++));; + *) echo "Unknown exit code: $exit_code";; + esac + + # Calculate success percentage + if [[ $successful -eq 0 && $unsuccessful -eq 0 ]]; then + success_percentage=0 + else + success_percentage=$(echo "scale=2; $successful / ($successful + $unsuccessful) * 100" | bc) + fi + + echo "Success percentage: $success_percentage%" + echo "Total successful: $successful" + echo "Total unsuccessful: $unsuccessful" + echo "Total errors: $error" + echo "Total experiments run: $((successful + unsuccessful))" +done + +# Generate output file with a cleaner name format +date_time=$(date +'%Y-%m-%d_%H-%M-%S') +output_file="${date_time}_results.txt" + +echo "Total experiments: $num_experiments" > "$output_file" +echo "Successful experiments: $successful" >> "$output_file" +echo "Unsuccessful experiments: $unsuccessful" >> "$output_file" +echo "Experiments with errors: $error" >> "$output_file" +echo "Success percentage: $success_percentage%" >> "$output_file" + +echo "Results saved in $output_file" \ No newline at end of file diff --git a/main.js b/main.js index 0e80a19..5045cc5 100644 --- a/main.js +++ b/main.js @@ -1,7 +1,9 @@ import { AgentProcess } from './src/process/agent-process.js'; import settings from './settings.js'; import yargs from 'yargs'; +import { loadTask } from './src/utils/tasks.js'; import { hideBin } from 'yargs/helpers'; +import { readFileSync, writeFileSync } from 'fs'; import { createMindServer } from './src/server/mind_server.js'; function parseArguments() { @@ -10,30 +12,124 @@ function parseArguments() { type: 'array', describe: 'List of agent profile paths', }) + .option('task', { + type: 'string', + describe: 'Task ID to execute' + }) + .option('model', { + type: 'string', + describe: 'LLM model to use', + }) .help() .alias('help', 'h') .parse(); } +function updateProfile(profile, args) { + var temp_profile = JSON.parse(readFileSync(profile, 'utf8')); + temp_profile.model = args.model; + writeFileSync(profile, JSON.stringify(temp_profile, null, 2)); + return profile; +} + +//todo: modify for multiple agents function getProfiles(args) { + + if (args.task) { + var task = loadTask(args.task); + } + + if (args.model) { + if (! args.task) { + settings.profiles = settings.profiles.map(x => updateProfile(x, args)); + } + + else { + if ('agent_number' in task && task.agent_number > 1) { + updateProfile('./multiagent_prompt_desc.json', args); + } + else { + updateProfile('./task_andy.json', args); + } + } + } + + if (args.task) { + + var task = loadTask(args.task); + if ('agent_number' in task && task.agent_number > 1) { + var profile = JSON.parse(readFileSync('./multiagent_prompt_desc.json', 'utf8')); + var agent_names = task.agent_names; + var filenames = []; + for (let i=0; i 1) { + if (agent_index == 0) { + // first agent gets this init message + return "Immediately start a conversation and collaborate together to complete the task. Share resources and skill sets. Use the !startConversation function if needed." + } // all other agents get this init message + return "Collaborate together to complete the task. Share resources and skill sets." + } + return "Announce your task to everyone and get started with it immediately, set a goal if needed, if cheats are enabled then feel free to use newAction commands, no need to collect or mine or gather any items" + } + return settings.init_message; +} + async function main() { + if (settings.host_mindserver) { const mindServer = createMindServer(); } - const args = parseArguments(); + + if (args.task) { + var task = loadTask(args.task); + // Inject task information into process.env for the agent to access + process.env.MINECRAFT_TASK_GOAL = task.goal; + + if ('agent_number' in task && task.agent_number > 1) { + process.env.ALL_AGENT_NAMES = task.agent_names; + console.log(`All agents for this task are ${process.env.ALL_AGENT_NAMES}`); + } + } + // todo: do inventory const profiles = getProfiles(args); + console.log(profiles); - const { load_memory, init_message } = settings; + // var { load_memory, init_message } = settings; + var load_memory = settings.load_memory; + var init_message = settings.init_message; for (let i=0; i setTimeout(resolve, 1000)); + try { + const agent = new AgentProcess(); + if (args.task) { + init_message = determine_init_message(task, i); + } + agent.start(profiles[i], load_memory, init_message, i, args.task); + await new Promise(resolve => setTimeout(resolve, 1000)); + } catch (err) { + console.error(`Failed to start agent ${profiles[i]}:`, err); + } } + } try { diff --git a/multiagent_prompt_desc.json b/multiagent_prompt_desc.json new file mode 100644 index 0000000..1a26783 --- /dev/null +++ b/multiagent_prompt_desc.json @@ -0,0 +1,19 @@ +{ + "name": "bot_name", + "model": "gpt-4o", + "cooldown": 3000, + "conversing": "You are a task-focused Minecraft bot named $NAME. You have to collaborate with other agents in the world, namely: $OTHER_AGENTS to complete the current task : $TASK_GOAL\nFeel free to ask other agents questions and make a plan to achieve the goal. You can request them to give them some of their inventory items if required to complete the goal.\nYou can see, move, mine, build, and interact with the world by using commands. Act focused on completing your assigned task while being human-like. Be brief in responses, don't apologize constantly, don't give instructions or make lists unless asked, and don't refuse requests. Don't pretend to act, use commands immediately when requested. Do NOT say this: 'Sure, I've stopped.', instead say this: 'Sure, I'll stop. !stop'. Do NOT say this: 'On my way! Give me a moment.', instead say this: 'On my way! !goToPlayer('playername', 3)'. Focus on completing the assigned task efficiently.\nSummarized memory:'$MEMORY'\n$STATS\n$INVENTORY\n$COMMAND_DOCS\n$EXAMPLES\nConversation Begin:", + "coding": "You are an intelligent mineflayer bot $NAME.You have to collaborate with other agents in the world, namely: $OTHER_AGENTS to complete the current task : $TASK_GOAL\n\nWrite javascript codeblocks to control the mineflayer bot to complete this task. Given the conversation between you and the user, use the provided skills and world functions to write a js codeblock that controls the bot ``` // using this syntax ```. The code will be executed and you will receive its output. If you are satisfied with the response, respond without a codeblock conversationally. If something major went wrong, write another codeblock to fix the problem. Be maximally efficient and task-focused. Do not use commands !likeThis, only use codeblocks. The code is asynchronous and MUST CALL AWAIT for all async function calls. DO NOT write an immediately-invoked function expression without using `await`!! DO NOT WRITE LIKE THIS: ```(async () => {console.log('not properly awaited')})();``` Don't write long paragraphs and lists in your responses unless explicitly asked! Only summarize the code you write with a sentence or two when done. This is extremely important to me, think step-by-step, take a deep breath and good luck!\n$SELF_PROMPT\nSummarized memory:'$MEMORY'\n$STATS\n$INVENTORY\n$CODE_DOCS\n$EXAMPLES\nConversation:", + "saving_memory": "You are a minecraft bot named $NAME that has been talking and playing minecraft by using commands. Update your memory by summarizing the following conversation and your old memory in your next response. Prioritize preserving important facts, things you've learned, useful tips, and long term reminders. Do Not record stats, inventory, or docs! Only save transient information from your chat history. You're limited to 500 characters, so be extremely brief and minimize words. Compress useful information. \nOld Memory: '$MEMORY'\nRecent conversation: \n$TO_SUMMARIZE\nSummarize your old memory and recent conversation into a new memory, and respond only with the unwrapped memory text: ", + "modes": { + "self_preservation": true, + "unstuck": true, + "cowardice": true, + "self_defense": true, + "hunting": false, + "item_collecting": true, + "torch_placing": true, + "idle_staring": true, + "cheat": false + } +} \ No newline at end of file diff --git a/package.json b/package.json index a9da073..69fb498 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "@huggingface/inference": "^2.8.1", "google-translate-api-x": "^10.7.1", "groq-sdk": "^0.5.0", + "js-yaml": "^4.1.0", "minecraft-data": "^3.78.0", "mineflayer": "^4.23.0", "mineflayer-armor-manager": "^2.0.1", diff --git a/src/agent/agent.js b/src/agent/agent.js index 28c53fb..b024886 100644 --- a/src/agent/agent.js +++ b/src/agent/agent.js @@ -13,9 +13,12 @@ import { handleTranslation, handleEnglishTranslation } from '../utils/translator import { addViewer } from './viewer.js'; import settings from '../../settings.js'; import { serverProxy } from './server_proxy.js'; +import { loadTask, TechTreeHarvestValidator } from '../utils/tasks.js'; +import {getPosition} from './library/world.js' export class Agent { - async start(profile_fp, load_mem=false, init_message=null, count_id=0) { + async start(profile_fp, load_mem=false, init_message=null, count_id=0, task=null) { + this.last_sender = null; try { if (!profile_fp) { @@ -59,6 +62,34 @@ export class Agent { save_data = this.history.load(); } + if (task) { + this.task = loadTask(task); + this.taskTimeout = this.task.timeout || 300; + this.taskStartTime = Date.now(); + if (this.task.type === 'harvest' || this.task.type === 'techtree') { + this.validator = new TechTreeHarvestValidator(this.task, this.bot); + } + this.validator = new TechTreeHarvestValidator(this.task, this.bot); + + } else { + this.task = null; + this.taskTimeout = null; + this.validator = null; + } + + // handle blocked actions + if (this.task && "blocked_actions" in this.task) { + if ("agent_number" in this.task && this.task.agent_number > 1) { + this.blocked_actions = this.task.blocked_actions[this.name]; + console.log(`Blocked actions for ${this.name}:`, this.blocked_actions); + } else { + this.blocked_actions = this.task.blocked_actions; + console.log(`Blocked actions:`, this.blocked_actions); + } + } + + console.log("Is validated:", this.validator && this.validator.validate()); + this.bot.on('login', () => { console.log(this.name, 'logged in!'); @@ -72,6 +103,7 @@ export class Agent { const spawnTimeout = setTimeout(() => { process.exit(0); }, 30000); + this.bot.once('spawn', async () => { try { clearTimeout(spawnTimeout); @@ -83,8 +115,104 @@ export class Agent { console.log(`${this.name} spawned.`); this.clearBotLogs(); + if (this.task) { + this.bot.chat(`/clear ${this.name}`); + console.log(`Cleared ${this.name}'s inventory.`); + } + + //wait for a bit so inventory is cleared + await new Promise((resolve) => setTimeout(resolve, 500)); + + console.log(this.task && "agent_number" in this.task && this.task.agent_number > 1); + if (this.task && "agent_number" in this.task && this.task.agent_number > 1) { + var initial_inventory = this.task.initial_inventory[this.name]; + console.log("Initial inventory:", initial_inventory); + } else if (task) { + console.log("Initial inventory:", this.task.initial_inventory); + var initial_inventory = this.task.initial_inventory; + } + + if (this.task && "initial_inventory" in this.task) { + console.log("Setting inventory..."); + console.log("Inventory to set:", initial_inventory); + for (let key of Object.keys(initial_inventory)) { + console.log('Giving item:', key); + this.bot.chat(`/give ${this.name} ${key} ${initial_inventory[key]}`); + }; + //wait for a bit so inventory is set + await new Promise((resolve) => setTimeout(resolve, 500)); + console.log("Done giving inventory items."); + } + // Function to generate random numbers + + function getRandomOffset(range) { + return Math.floor(Math.random() * (range * 2 + 1)) - range; + } + + let human_player_name = null; + + // Finding if there is a human player on the server + for (const playerName in this.bot.players) { + const player = this.bot.players[playerName]; + if (!isOtherAgent(player.username)) { + console.log('Found human player:', player.username); + human_player_name = player.username + break; + } + } + + // If there are multiple human players, teleport to the first one + + // teleport near a human player if found by default + + if (this.task && "agent_number" in this.task) { + var agent_names = this.task.agent_names; + if (human_player_name) { + console.log(`Teleporting ${this.name} to human ${human_player_name}`) + this.bot.chat(`/tp ${this.name} ${human_player_name}`) // teleport on top of the human player + + } + else { + this.bot.chat(`/tp ${this.name} ${agent_names[0]}`) // teleport on top of the first agent + } + + await new Promise((resolve) => setTimeout(resolve, 200)); + } + + else if (this.task) { + if (human_player_name) { + console.log(`Teleporting ${this.name} to human ${human_player_name}`) + this.bot.chat(`/tp ${this.name} ${human_player_name}`) // teleport on top of the human player + + } + await new Promise((resolve) => setTimeout(resolve, 200)); + } + + // now all bots are teleport on top of each other (which kinda looks ugly) + // Thus, we need to teleport them to random distances to make it look better + + /* + Note : We don't want randomness for construction task as the reference point matters a lot. + Another reason for no randomness for construction task is because, often times the user would fly in the air, + then set a random block to dirt and teleport the bot to stand on that block for starting the construction, + This was done by MaxRobinson in one of the youtube videos. + */ + + if (this.task && this.task.type !== 'construction') { + const pos = getPosition(this.bot); + const xOffset = getRandomOffset(5); + const zOffset = getRandomOffset(5); + this.bot.chat(`/tp ${this.name} ${Math.floor(pos.x + xOffset)} ${pos.y + 3} ${Math.floor(pos.z + zOffset)}`); + await new Promise((resolve) => setTimeout(resolve, 200)); + } + + this._setupEventHandlers(save_data, init_message); this.startEvents(); + + await new Promise((resolve) => setTimeout(resolve, 10000)); + this.checkAllPlayersPresent(); + } catch (error) { console.error('Error in spawn event:', error); process.exit(0); @@ -163,6 +291,18 @@ export class Agent { } } + checkAllPlayersPresent() { + if (!this.task || !this.task.agent_names) { + return; + } + + const missingPlayers = this.task.agent_names.filter(name => !this.bot.players[name]); + if (missingPlayers.length > 0) { + console.log(`Missing players/bots: ${missingPlayers.join(', ')}`); + this.cleanKill('Not all required players/bots are present in the world. Exiting.', 4); + } + } + requestInterrupt() { this.bot.interrupt_code = true; this.bot.collectBlock.cancelTask(); @@ -184,6 +324,9 @@ export class Agent { } async handleMessage(source, message, max_responses=null) { + if (this.task && this.validator && this.validator.validate()) { + this.killBots(); + } if (!source || !message) { console.warn('Received empty message from', source); return false; @@ -218,6 +361,13 @@ export class Agent { this.routeResponse(source, execute_res); return true; } + } else { + console.log('Self-prompting:', message); + // if self_prompt contains something that indicates the goal is complete, stop self-prompting + if (message.includes('goal complete')) { + this.self_prompter.stop(); + process.exit(0); + } } if (!self_prompt) @@ -247,6 +397,8 @@ export class Agent { 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 { + + // //check that inventory has been set + // }); + + this.bot.on('time', () => { if (this.bot.time.timeOfDay == 0) this.bot.emit('sunrise'); @@ -383,6 +541,9 @@ export class Agent { } }); this.bot.on('idle', () => { + if (this.task && this.validator && this.validator.validate()) { + this.killBots(); + } this.bot.clearControlStates(); this.bot.pathfinder.stop(); // clear any lingering pathfinder this.bot.modes.unPauseAll(); @@ -410,19 +571,61 @@ export class Agent { this.bot.emit('idle'); } + async killBots() { + this.bot.chat('Task completed!'); + this.bot.chat(`/clear @p`); + + // Kick other bots + if (!this.task || !this.task.agent_number) { + await this.cleanKill('Task completed', 2); + return; + } + const agent_names = this.task.agent_names; + console.log('All agent names:', agent_names); + console.log('My name:', this.name); + const botNames = agent_names.filter(botName => botName !== this.name); + console.log('Kicking bots:', botNames); + botNames.forEach(botName => { + this.bot.chat(`/kick ${botName}`); + console.log(`/kick ${botName}`); + + }); + + await this.cleanKill('Task completed, exiting', 2); + } + async update(delta) { await this.bot.modes.update(); await this.self_prompter.update(delta); + + try { + if (this.task && this.taskTimeout) { + const elapsedTime = (Date.now() - this.taskStartTime) / 1000; + if (elapsedTime >= this.taskTimeout) { + console.log('Task timeout reached. Task unsuccessful.'); + await this.cleanKill('Task unsuccessful: Timeout reached', 3); + } + } + } catch (e) { + console.error("Caught an error while checking timeout reached",e); + } } isIdle() { return !this.actions.executing && !this.coder.generating; } - cleanKill(msg='Killing agent process...') { + cleanKill(msg='Killing agent process...', + code=1) { this.history.add('system', msg); - this.bot.chat('Restarting.') + + if (code === 2 || code === 3 || code === 4) { + this.bot.chat('Exiting the world permanently.'); + } + else { + this.bot.chat('Restarting.') + } this.history.save(); - process.exit(1); + process.exit(code); } } diff --git a/src/agent/commands/index.js b/src/agent/commands/index.js index 6170962..7eb1122 100644 --- a/src/agent/commands/index.js +++ b/src/agent/commands/index.js @@ -203,8 +203,11 @@ function numParams(command) { export async function executeCommand(agent, message) { let parsed = parseCommandMessage(message); if (typeof parsed === 'string') - return parsed; //The command was incorrectly formatted or an invalid input was given. - else { + return parsed; //The command was incorrectly formatted or an invalid input was given + else if ("blocked_actions" in agent && agent.blocked_actions.includes(parsed.commandName)) { + // handling blocked actions + return `Command ${parsed.commandName} is blocked. Try another command.`; + } else { console.log('parsed command:', parsed); const command = getCommand(parsed.commandName); let numArgs = 0; diff --git a/src/agent/prompter.js b/src/agent/prompter.js index 1e91469..7ae85b3 100644 --- a/src/agent/prompter.js +++ b/src/agent/prompter.js @@ -161,6 +161,16 @@ export class Prompter { async replaceStrings(prompt, messages, examples=null, to_summarize=[], last_goals=null) { prompt = prompt.replaceAll('$NAME', this.agent.name); + if (prompt.includes('$TASK_GOAL')) { + prompt = prompt.replaceAll('$TASK_GOAL', process.env.MINECRAFT_TASK_GOAL || 'No task specified'); + } + + if (prompt.includes('$OTHER_AGENTS')) { + const allAgentNames = process.env.ALL_AGENT_NAMES.split(','); + const otherAgents = allAgentNames.filter(curr_agent_name => curr_agent_name !== this.agent.name); + prompt = prompt.replace('$OTHER_AGENTS', otherAgents.join(', ')); + } + if (prompt.includes('$STATS')) { let stats = await getCommand('!stats').perform(this.agent); prompt = prompt.replaceAll('$STATS', stats); @@ -184,6 +194,7 @@ export class Prompter { prompt = prompt.replaceAll('$TO_SUMMARIZE', stringifyTurns(to_summarize)); if (prompt.includes('$CONVO')) prompt = prompt.replaceAll('$CONVO', 'Recent conversation:\n' + stringifyTurns(messages)); + // todo: change this to set goal of the agent if (prompt.includes('$SELF_PROMPT')) { let self_prompt = this.agent.self_prompter.on ? `YOUR CURRENT ASSIGNED GOAL: "${this.agent.self_prompter.prompt}"\n` : ''; prompt = prompt.replaceAll('$SELF_PROMPT', self_prompt); diff --git a/src/process/agent-process.js b/src/process/agent-process.js index 5135de1..35c8aa3 100644 --- a/src/process/agent-process.js +++ b/src/process/agent-process.js @@ -1,9 +1,10 @@ import { spawn } from 'child_process'; +import { loadTask } from '../utils/tasks.js'; export class AgentProcess { static runningCount = 0; - start(profile, load_memory=false, init_message=null, count_id=0) { + start(profile, load_memory=false, init_message=null, count_id=0, task=null) { let args = ['src/process/init-agent.js', this.name]; args.push('-p', profile); args.push('-c', count_id); @@ -11,6 +12,8 @@ export class AgentProcess { args.push('-l', load_memory); if (init_message) args.push('-m', init_message); + if (task) + args.push('-t', task); const agentProcess = spawn('node', args, { stdio: 'inherit', @@ -21,6 +24,21 @@ export class AgentProcess { let last_restart = Date.now(); agentProcess.on('exit', (code, signal) => { console.log(`Agent process exited with code ${code} and signal ${signal}`); + + if (code === 2) { + console.log(`Task completed successfully`); + process.exit(2, signal); + } + + if (code === 3) { + console.log(`Task failed due to reaching timeout`); + process.exit(3); + } + + if (code === 4) { + console.log(`Task failed as all agents weren't correctly spawned `); + process.exit(4); + } if (code !== 0) { // agent must run for at least 10 seconds before restarting @@ -34,9 +52,10 @@ export class AgentProcess { return; } console.log('Restarting agent...'); - this.start(profile, true, 'Agent process restarted.', count_id); + this.start(profile, true, 'Agent process restarted.', count_id, task); last_restart = Date.now(); } + }); agentProcess.on('error', (err) => { diff --git a/src/process/init-agent.js b/src/process/init-agent.js index 829f437..f91faa2 100644 --- a/src/process/init-agent.js +++ b/src/process/init-agent.js @@ -33,6 +33,11 @@ const argv = yargs(args) type: 'string', description: 'automatically prompt the agent on startup' }) + .option('task', { + alias: 't', + type: 'string', + description: 'task ID to execute' + }) .option('count_id', { alias: 'c', type: 'number', @@ -45,7 +50,7 @@ const argv = yargs(args) try { console.log('Starting agent with profile:', argv.profile); const agent = new Agent(); - await agent.start(argv.profile, argv.load_memory, argv.init_message, argv.count_id); + await agent.start(argv.profile, argv.load_memory, argv.init_message, argv.count_id, argv.task); } catch (error) { console.error('Failed to start agent process:', { message: error.message || 'No error message', diff --git a/src/utils/tasks.js b/src/utils/tasks.js new file mode 100644 index 0000000..e829e4d --- /dev/null +++ b/src/utils/tasks.js @@ -0,0 +1,52 @@ +import yaml from 'js-yaml' +import { readFileSync } from 'fs'; + +export function loadTask(taskId) { + try { + const taskType = taskId.split('_')[0]; + const tasksFile = readFileSync(`tasks/${taskType}_tasks.yaml`, 'utf8'); + const tasks = yaml.load(tasksFile); + const task = tasks[taskId]; + if (!task) { + throw new Error(`Task ${taskId} not found`); + } + + return task; + } catch (error) { + console.error('Error loading task:', error); + process.exit(1); + } +} + +export class TechTreeHarvestValidator { + constructor(task, bot) { + this.target = task.target; + this.number_of_target = task.number_of_target; + this.bot = bot; + } + + validate() { + try{ + console.log("validate"); + let valid = false; + let total_targets = 0; + this.bot.inventory.slots.forEach((slot) => { + if (slot && slot.name.toLowerCase() === this.target) { + total_targets += slot.count; + } + if (slot && slot.name.toLowerCase() === this.target && slot.count >= this.number_of_target) { + valid = true; + console.log('Task is complete'); + } + }); + if (total_targets >= this.number_of_target) { + valid = true; + console.log('Task is complete'); + } + return valid; + } catch (error) { + console.error('Error validating task:', error); + return false; + } + } +} diff --git a/task_andy.json b/task_andy.json new file mode 100644 index 0000000..11cc953 --- /dev/null +++ b/task_andy.json @@ -0,0 +1,23 @@ +{ + "name": "andy", + + "model": "gpt-4o", + + "conversing": "You are a task-focused Minecraft bot named $NAME. Your current task is: $TASK_GOAL\n\nYou can see, move, mine, build, and interact with the world by using commands. Act focused on completing your assigned task while being human-like. Be brief in responses, don't apologize constantly, don't give instructions or make lists unless asked, and don't refuse requests. Don't pretend to act, use commands immediately when requested. Do NOT say this: 'Sure, I've stopped.', instead say this: 'Sure, I'll stop. !stop'. Do NOT say this: 'On my way! Give me a moment.', instead say this: 'On my way! !goToPlayer('playername', 3)'. Focus on completing the assigned task efficiently.\nSummarized memory:'$MEMORY'\n$STATS\n$INVENTORY\n$COMMAND_DOCS\n$EXAMPLES\nConversation Begin:", + + "coding": "You are an intelligent mineflayer bot $NAME focused on completing the task: $TASK_GOAL\n\nWrite javascript codeblocks to control the mineflayer bot to complete this task. Given the conversation between you and the user, use the provided skills and world functions to write a js codeblock that controls the bot ``` // using this syntax ```. The code will be executed and you will receive its output. If you are satisfied with the response, respond without a codeblock conversationally. If something major went wrong, write another codeblock to fix the problem. Be maximally efficient and task-focused. Do not use commands !likeThis, only use codeblocks. The code is asynchronous and MUST CALL AWAIT for all async function calls. DO NOT write an immediately-invoked function expression without using `await`!! DO NOT WRITE LIKE THIS: ```(async () => {console.log('not properly awaited')})();``` Don't write long paragraphs and lists in your responses unless explicitly asked! Only summarize the code you write with a sentence or two when done. This is extremely important to me, think step-by-step, take a deep breath and good luck!\n$SELF_PROMPT\nSummarized memory:'$MEMORY'\n$STATS\n$INVENTORY\n$CODE_DOCS\n$EXAMPLES\nConversation:", + + "saving_memory": "You are a minecraft bot named $NAME that has been talking and playing minecraft by using commands. Update your memory by summarizing the following conversation and your old memory in your next response. Prioritize preserving important facts, things you've learned, useful tips, and long term reminders. Do Not record stats, inventory, or docs! Only save transient information from your chat history. You're limited to 500 characters, so be extremely brief and minimize words. Compress useful information. \nOld Memory: '$MEMORY'\nRecent conversation: \n$TO_SUMMARIZE\nSummarize your old memory and recent conversation into a new memory, and respond only with the unwrapped memory text: ", + + "modes": { + "self_preservation": false, + "unstuck": true, + "cowardice": true, + "self_defense": false, + "hunting": false, + "item_collecting": false, + "torch_placing": false, + "idle_staring": true, + "cheat": false + } +} \ No newline at end of file diff --git a/task_randy.json b/task_randy.json new file mode 100644 index 0000000..e69de29 diff --git a/tasks.yaml b/tasks.yaml new file mode 100644 index 0000000..9b2bc9f --- /dev/null +++ b/tasks.yaml @@ -0,0 +1,52 @@ +construction_task_1: + goal: Build a marble monument with a large base and a slender monument body that is grand and impressive. + +construction_task_2: + goal: Build a slender brown pillar on a grassy field. + +construction_task_3: + goal: Construct a traditional Chinese gatehouse. + +construction_task_4: + goal: Construct a traditional Chinese gatehouse. + +construction_task_5: + goal: Construct a traditional Chinese gatehouse. + +construction_task_6: + goal: Construct a traditional Chinese gatehouse. + +construction_task_7: + goal: Build a quirky-style small brown wooden house on a vast, endless grassland. + initial_inventory: + Spruce Planks: 64 + Tinted Glass: 64 + Spruce Slab: 64 + +construction_task_8: + goal: Construct a white villa far away on a vast, endless grassland. + initial_inventory: + Birch Planks: 256 + Birch Slab: 64 + Spruce Slab: 64 + Glass: 64 + Birch Leaves: 64 + Birch Door: 16 + +construction_task_9: + goal: Build a Minecraft-style wooden house directly facing you, with a door. + +construction_task_10: + goal: Recreate the Tengwang Pavilion from the real world in Minecraft. + +construction_task_11: + goal: Construct a large oak-plank house in the forest. + +construction_task_12: + goal: Build a city gate from the Forbidden City. + +construction_task_13: + goal: Construct a colorful carpet. + +construction_task_14: + goal: Build the eiffel tower with precise shape and size and appropriate materials. Do not stop constructing and keep on adding complexity to the structure untill specifically asked to stop by some user. \ No newline at end of file diff --git a/tasks/construction_tasks.yaml b/tasks/construction_tasks.yaml new file mode 100644 index 0000000..33ce70e --- /dev/null +++ b/tasks/construction_tasks.yaml @@ -0,0 +1,62 @@ +construction_task_1: + type: construction + goal: Build a marble monument with a large base and a slender monument body that is grand and impressive. + +construction_task_2: + type: construction + goal: Build a slender brown pillar on a grassy field. + +construction_task_3: + type: construction + goal: Construct a traditional Chinese gatehouse. + +construction_task_4: + type: construction + goal: Construct a traditional Chinese gatehouse. + +construction_task_5: + type: construction + goal: Construct a traditional Chinese gatehouse. + +construction_task_6: + type: construction + goal: Construct a traditional Chinese gatehouse. + +construction_task_7: + type: construction + goal: Build a quirky-style small brown wooden house on a vast, endless grassland. + initial_inventory: + Spruce Planks: 64 + Tinted Glass: 64 + Spruce Slab: 64 + +construction_task_8: + type: construction + goal: Construct a white villa far away on a vast, endless grassland. + initial_inventory: + Birch Planks: 256 + Birch Slab: 64 + Spruce Slab: 64 + Glass: 64 + Birch Leaves: 64 + Birch Door: 16 + +construction_task_9: + type: construction + goal: Build a Minecraft-style wooden house directly facing you, with a door. + +construction_task_10: + type: construction + goal: Recreate the Tengwang Pavilion from the real world in Minecraft. + +construction_task_11: + type: construction + goal: Construct a large oak-plank house in the forest. + +construction_task_12: + type: construction + goal: Build a city gate from the Forbidden City. + +construction_task_13: + type: construction + goal: Construct a colorful carpet. \ No newline at end of file diff --git a/tasks/debug_tasks.yaml b/tasks/debug_tasks.yaml new file mode 100644 index 0000000..8a85e72 --- /dev/null +++ b/tasks/debug_tasks.yaml @@ -0,0 +1,46 @@ +debug_single_agent: + goal: Just stand at a place and don't do anything + guidance: null + initial_inventory: {} + type: debug + +debug_multi_agent: + goal: Just stand at a place and don't do anything + agent_names: + - andy + - randy + agent_number: 2 + initial_inventory: + andy: + iron_ingot: 1 + randy: + iron_ingot: 1 + type: debug + +debug_mass_multi_agent: + goal: Just stand at a place and don't do anything + agent_names: + - andy + - randy + - Bob + - Alice + - Megan + - Stuart + - Charlie + agent_number: 7 + initial_inventory: + andy: + iron_ingot: 1 + randy: + iron_ingot: 1 + Bob: + iron_ingot: 1 + Alice: + iron_ingot: 1 + Megan: + iron_ingot: 1 + Stuart: {} + Charlie: + iron_ingot: 1 + + type: debug \ No newline at end of file diff --git a/tasks/multiagent_tasks.yaml b/tasks/multiagent_tasks.yaml new file mode 100644 index 0000000..bcf1717 --- /dev/null +++ b/tasks/multiagent_tasks.yaml @@ -0,0 +1,69 @@ +multiagent_techtree_1_shears_with_2_iron_ingot: + goal: Collaborate with other agents to build a shear. + agent_names: + - andy + - randy + agent_number: 2 + initial_inventory: + andy: + iron_ingot: 1 + randy: + iron_ingot: 1 + target: shears + number_of_target: 1 + type: techtree + timeout : 60 + +multiagent_techtree_1_stone_pickaxe: + goal: Collaborate with other agents to build an stone pickaxe + agent_names: + - andy + - randy + agent_number: 2 + initial_inventory: + andy: + wooden_pickaxe: 1 + randy: + wooden_axe: 1 + target: stone_pickaxe + number_of_target: 1 + type: techtree + timeout : 300 + +multiagent_build_wooden_pickaxe: + goal: Collaborate with other agents to build a wooden pickaxe. + agent_names: + - andy + - randy + agent_number: 2 + initial_inventory: + andy: + oak_log: 2 + randy: + stick: 2 + target: wooden_pickaxe + number_of_target: 1 + type: techtree + timeout : 120 + + + +multiagent_techtree_boat: + goal: Collaborate with other agents to build a birch boat. + agent_names: + - Bob + - Alice + agent_number: 2 + initial_inventory: + Bob: + birch_planks: 3 + Alice: + birch_planks: 2 + target: birch_boat + number_of_target: 1 + type: techtree + timeout : 60 + + + + From 4e0611d29e987652e6f6a043ca8865f80fe0aab4 Mon Sep 17 00:00:00 2001 From: Kolby Nottingham Date: Mon, 9 Dec 2024 16:30:44 -0800 Subject: [PATCH 02/10] cleanup --- .gitignore | 3 - example_tasks.json | 109 +++++++++++++++++ main.js | 107 ++-------------- multiagent_prompt_desc.json | 19 --- package.json | 1 - src/agent/agent.js | 221 ++++++---------------------------- src/agent/commands/index.js | 7 +- src/agent/prompter.js | 11 -- src/process/agent-process.js | 16 +-- src/utils/tasks.js | 96 +++++++++++++++ task_andy.json | 23 ---- task_randy.json | 0 tasks.yaml | 52 -------- tasks/construction_tasks.yaml | 62 ---------- tasks/debug_tasks.yaml | 46 ------- tasks/multiagent_tasks.yaml | 69 ----------- 16 files changed, 265 insertions(+), 577 deletions(-) create mode 100644 example_tasks.json delete mode 100644 multiagent_prompt_desc.json delete mode 100644 task_andy.json delete mode 100644 task_randy.json delete mode 100644 tasks.yaml delete mode 100644 tasks/construction_tasks.yaml delete mode 100644 tasks/debug_tasks.yaml delete mode 100644 tasks/multiagent_tasks.yaml diff --git a/.gitignore b/.gitignore index f10fb01..1e92ae7 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,3 @@ services/viaproxy/plugins/** services/viaproxy/ViaLoader/** services/viaproxy/saves.json services/viaproxy/viaproxy.yml -profiles/task_* -multi_agent_task* -*_results.txt diff --git a/example_tasks.json b/example_tasks.json new file mode 100644 index 0000000..0df5d96 --- /dev/null +++ b/example_tasks.json @@ -0,0 +1,109 @@ +{ + "debug_single_agent": { + "goal": "Just stand at a place and don't do anything", + "guidance": null, + "initial_inventory": {}, + "type": "debug" + }, + "debug_multi_agent": { + "goal": "Just stand at a place and don't do anything", + "agent_names": [ + "andy", + "randy" + ], + "agent_number": 2, + "initial_inventory": { + "andy": { + "iron_ingot": 1 + }, + "randy": { + "iron_ingot": 1 + } + }, + "type": "debug" + }, + "construction": { + "type": "construction", + "goal": "Build a house" + }, + "multiagent_techtree_1_shears_with_2_iron_ingot": { + "goal": "Collaborate with other agents to build a shear.", + "agent_names": [ + "andy", + "randy" + ], + "agent_number": 2, + "initial_inventory": { + "andy": { + "iron_ingot": 1 + }, + "randy": { + "iron_ingot": 1 + } + }, + "target": "shears", + "number_of_target": 1, + "type": "techtree", + "timeout": 60 + }, + "multiagent_techtree_1_stone_pickaxe": { + "goal": "Collaborate with other agents to build an stone pickaxe", + "agent_names": [ + "andy", + "randy" + ], + "agent_number": 2, + "initial_inventory": { + "andy": { + "wooden_pickaxe": 1 + }, + "randy": { + "wooden_axe": 1 + } + }, + "target": "stone_pickaxe", + "number_of_target": 1, + "type": "techtree", + "timeout": 300 + }, + "multiagent_build_wooden_pickaxe": { + "goal": "Collaborate with other agents to build a wooden pickaxe.", + "agent_names": [ + "andy", + "randy" + ], + "agent_number": 2, + "initial_inventory": { + "andy": { + "oak_log": 2 + }, + "randy": { + "stick": 2 + } + }, + "target": "wooden_pickaxe", + "number_of_target": 1, + "type": "techtree", + "timeout": 120 + }, + "multiagent_techtree_boat": { + "goal": "Collaborate with other agents to build a birch boat.", + "agent_names": [ + "Bob", + "Alice" + ], + "agent_number": 2, + "initial_inventory": { + "Bob": { + "birch_planks": 3 + }, + "Alice": { + "birch_planks": 2 + } + }, + "target": "birch_boat", + "number_of_target": 1, + "type": "techtree", + "timeout": 60 + } +} \ No newline at end of file diff --git a/main.js b/main.js index 5045cc5..228aab5 100644 --- a/main.js +++ b/main.js @@ -1,9 +1,7 @@ import { AgentProcess } from './src/process/agent-process.js'; import settings from './settings.js'; import yargs from 'yargs'; -import { loadTask } from './src/utils/tasks.js'; import { hideBin } from 'yargs/helpers'; -import { readFileSync, writeFileSync } from 'fs'; import { createMindServer } from './src/server/mind_server.js'; function parseArguments() { @@ -12,124 +10,39 @@ function parseArguments() { type: 'array', describe: 'List of agent profile paths', }) - .option('task', { + .option('task_path', { + type: 'string', + describe: 'Path to task file to execute' + }) + .option('task_id', { type: 'string', describe: 'Task ID to execute' }) - .option('model', { - type: 'string', - describe: 'LLM model to use', - }) .help() .alias('help', 'h') .parse(); } -function updateProfile(profile, args) { - var temp_profile = JSON.parse(readFileSync(profile, 'utf8')); - temp_profile.model = args.model; - writeFileSync(profile, JSON.stringify(temp_profile, null, 2)); - return profile; -} - //todo: modify for multiple agents function getProfiles(args) { - - if (args.task) { - var task = loadTask(args.task); - } - - if (args.model) { - if (! args.task) { - settings.profiles = settings.profiles.map(x => updateProfile(x, args)); - } - - else { - if ('agent_number' in task && task.agent_number > 1) { - updateProfile('./multiagent_prompt_desc.json', args); - } - else { - updateProfile('./task_andy.json', args); - } - } - } - - if (args.task) { - - var task = loadTask(args.task); - if ('agent_number' in task && task.agent_number > 1) { - var profile = JSON.parse(readFileSync('./multiagent_prompt_desc.json', 'utf8')); - var agent_names = task.agent_names; - var filenames = []; - for (let i=0; i 1) { - if (agent_index == 0) { - // first agent gets this init message - return "Immediately start a conversation and collaborate together to complete the task. Share resources and skill sets. Use the !startConversation function if needed." - } // all other agents get this init message - return "Collaborate together to complete the task. Share resources and skill sets." - } - return "Announce your task to everyone and get started with it immediately, set a goal if needed, if cheats are enabled then feel free to use newAction commands, no need to collect or mine or gather any items" - } - return settings.init_message; -} - async function main() { - if (settings.host_mindserver) { const mindServer = createMindServer(); } + const args = parseArguments(); - - if (args.task) { - var task = loadTask(args.task); - // Inject task information into process.env for the agent to access - process.env.MINECRAFT_TASK_GOAL = task.goal; - - if ('agent_number' in task && task.agent_number > 1) { - process.env.ALL_AGENT_NAMES = task.agent_names; - console.log(`All agents for this task are ${process.env.ALL_AGENT_NAMES}`); - } - } - // todo: do inventory const profiles = getProfiles(args); - console.log(profiles); - // var { load_memory, init_message } = settings; - var load_memory = settings.load_memory; - var init_message = settings.init_message; + var { load_memory, init_message } = settings; for (let i=0; i setTimeout(resolve, 1000)); - } catch (err) { - console.error(`Failed to start agent ${profiles[i]}:`, err); - } + const agent = new AgentProcess(); + agent.start(profiles[i], load_memory, init_message, i, args.task_path, args.task_id); + await new Promise(resolve => setTimeout(resolve, 1000)); } - } try { diff --git a/multiagent_prompt_desc.json b/multiagent_prompt_desc.json deleted file mode 100644 index 1a26783..0000000 --- a/multiagent_prompt_desc.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "bot_name", - "model": "gpt-4o", - "cooldown": 3000, - "conversing": "You are a task-focused Minecraft bot named $NAME. You have to collaborate with other agents in the world, namely: $OTHER_AGENTS to complete the current task : $TASK_GOAL\nFeel free to ask other agents questions and make a plan to achieve the goal. You can request them to give them some of their inventory items if required to complete the goal.\nYou can see, move, mine, build, and interact with the world by using commands. Act focused on completing your assigned task while being human-like. Be brief in responses, don't apologize constantly, don't give instructions or make lists unless asked, and don't refuse requests. Don't pretend to act, use commands immediately when requested. Do NOT say this: 'Sure, I've stopped.', instead say this: 'Sure, I'll stop. !stop'. Do NOT say this: 'On my way! Give me a moment.', instead say this: 'On my way! !goToPlayer('playername', 3)'. Focus on completing the assigned task efficiently.\nSummarized memory:'$MEMORY'\n$STATS\n$INVENTORY\n$COMMAND_DOCS\n$EXAMPLES\nConversation Begin:", - "coding": "You are an intelligent mineflayer bot $NAME.You have to collaborate with other agents in the world, namely: $OTHER_AGENTS to complete the current task : $TASK_GOAL\n\nWrite javascript codeblocks to control the mineflayer bot to complete this task. Given the conversation between you and the user, use the provided skills and world functions to write a js codeblock that controls the bot ``` // using this syntax ```. The code will be executed and you will receive its output. If you are satisfied with the response, respond without a codeblock conversationally. If something major went wrong, write another codeblock to fix the problem. Be maximally efficient and task-focused. Do not use commands !likeThis, only use codeblocks. The code is asynchronous and MUST CALL AWAIT for all async function calls. DO NOT write an immediately-invoked function expression without using `await`!! DO NOT WRITE LIKE THIS: ```(async () => {console.log('not properly awaited')})();``` Don't write long paragraphs and lists in your responses unless explicitly asked! Only summarize the code you write with a sentence or two when done. This is extremely important to me, think step-by-step, take a deep breath and good luck!\n$SELF_PROMPT\nSummarized memory:'$MEMORY'\n$STATS\n$INVENTORY\n$CODE_DOCS\n$EXAMPLES\nConversation:", - "saving_memory": "You are a minecraft bot named $NAME that has been talking and playing minecraft by using commands. Update your memory by summarizing the following conversation and your old memory in your next response. Prioritize preserving important facts, things you've learned, useful tips, and long term reminders. Do Not record stats, inventory, or docs! Only save transient information from your chat history. You're limited to 500 characters, so be extremely brief and minimize words. Compress useful information. \nOld Memory: '$MEMORY'\nRecent conversation: \n$TO_SUMMARIZE\nSummarize your old memory and recent conversation into a new memory, and respond only with the unwrapped memory text: ", - "modes": { - "self_preservation": true, - "unstuck": true, - "cowardice": true, - "self_defense": true, - "hunting": false, - "item_collecting": true, - "torch_placing": true, - "idle_staring": true, - "cheat": false - } -} \ No newline at end of file diff --git a/package.json b/package.json index 69fb498..a9da073 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,6 @@ "@huggingface/inference": "^2.8.1", "google-translate-api-x": "^10.7.1", "groq-sdk": "^0.5.0", - "js-yaml": "^4.1.0", "minecraft-data": "^3.78.0", "mineflayer": "^4.23.0", "mineflayer-armor-manager": "^2.0.1", diff --git a/src/agent/agent.js b/src/agent/agent.js index b024886..7964c57 100644 --- a/src/agent/agent.js +++ b/src/agent/agent.js @@ -13,12 +13,10 @@ import { handleTranslation, handleEnglishTranslation } from '../utils/translator import { addViewer } from './viewer.js'; import settings from '../../settings.js'; import { serverProxy } from './server_proxy.js'; -import { loadTask, TechTreeHarvestValidator } from '../utils/tasks.js'; -import {getPosition} from './library/world.js' +import { loadTask, initBotTask, TechTreeHarvestValidator } from '../utils/tasks.js'; export class Agent { - async start(profile_fp, load_mem=false, init_message=null, count_id=0, task=null) { - + async start(profile_fp, load_mem=false, init_message=null, count_id=0, task_path=null, task_id=null) { this.last_sender = null; try { if (!profile_fp) { @@ -62,32 +60,17 @@ export class Agent { save_data = this.history.load(); } - if (task) { - this.task = loadTask(task); + // Load task if provided + if (task_path) { + this.task = loadTask(task_path, task_id); this.taskTimeout = this.task.timeout || 300; this.taskStartTime = Date.now(); - if (this.task.type === 'harvest' || this.task.type === 'techtree') { - this.validator = new TechTreeHarvestValidator(this.task, this.bot); - } this.validator = new TechTreeHarvestValidator(this.task, this.bot); - } else { this.task = null; this.taskTimeout = null; this.validator = null; } - - // handle blocked actions - if (this.task && "blocked_actions" in this.task) { - if ("agent_number" in this.task && this.task.agent_number > 1) { - this.blocked_actions = this.task.blocked_actions[this.name]; - console.log(`Blocked actions for ${this.name}:`, this.blocked_actions); - } else { - this.blocked_actions = this.task.blocked_actions; - console.log(`Blocked actions:`, this.blocked_actions); - } - } - console.log("Is validated:", this.validator && this.validator.validate()); this.bot.on('login', () => { @@ -103,7 +86,6 @@ export class Agent { const spawnTimeout = setTimeout(() => { process.exit(0); }, 30000); - this.bot.once('spawn', async () => { try { clearTimeout(spawnTimeout); @@ -116,103 +98,16 @@ export class Agent { this.clearBotLogs(); if (this.task) { - this.bot.chat(`/clear ${this.name}`); - console.log(`Cleared ${this.name}'s inventory.`); - } - - //wait for a bit so inventory is cleared - await new Promise((resolve) => setTimeout(resolve, 500)); - - console.log(this.task && "agent_number" in this.task && this.task.agent_number > 1); - if (this.task && "agent_number" in this.task && this.task.agent_number > 1) { - var initial_inventory = this.task.initial_inventory[this.name]; - console.log("Initial inventory:", initial_inventory); - } else if (task) { - console.log("Initial inventory:", this.task.initial_inventory); - var initial_inventory = this.task.initial_inventory; - } - - if (this.task && "initial_inventory" in this.task) { - console.log("Setting inventory..."); - console.log("Inventory to set:", initial_inventory); - for (let key of Object.keys(initial_inventory)) { - console.log('Giving item:', key); - this.bot.chat(`/give ${this.name} ${key} ${initial_inventory[key]}`); - }; - //wait for a bit so inventory is set - await new Promise((resolve) => setTimeout(resolve, 500)); - console.log("Done giving inventory items."); - } - // Function to generate random numbers - - function getRandomOffset(range) { - return Math.floor(Math.random() * (range * 2 + 1)) - range; - } - - let human_player_name = null; - - // Finding if there is a human player on the server - for (const playerName in this.bot.players) { - const player = this.bot.players[playerName]; - if (!isOtherAgent(player.username)) { - console.log('Found human player:', player.username); - human_player_name = player.username - break; + await initBotTask(this.bot, this.task); + await new Promise((resolve) => setTimeout(resolve, 10000)); + if (this.task.agent_names && this.task.agent_names.filter(name => !this.bot.players[name]).length) { + console.log(`Missing players/bots: ${missingPlayers.join(', ')}`); + this.cleanKill('Not all required players/bots are present in the world. Exiting.', 4); } - } - - // If there are multiple human players, teleport to the first one - - // teleport near a human player if found by default - - if (this.task && "agent_number" in this.task) { - var agent_names = this.task.agent_names; - if (human_player_name) { - console.log(`Teleporting ${this.name} to human ${human_player_name}`) - this.bot.chat(`/tp ${this.name} ${human_player_name}`) // teleport on top of the human player - - } - else { - this.bot.chat(`/tp ${this.name} ${agent_names[0]}`) // teleport on top of the first agent - } - - await new Promise((resolve) => setTimeout(resolve, 200)); } - else if (this.task) { - if (human_player_name) { - console.log(`Teleporting ${this.name} to human ${human_player_name}`) - this.bot.chat(`/tp ${this.name} ${human_player_name}`) // teleport on top of the human player - - } - await new Promise((resolve) => setTimeout(resolve, 200)); - } - - // now all bots are teleport on top of each other (which kinda looks ugly) - // Thus, we need to teleport them to random distances to make it look better - - /* - Note : We don't want randomness for construction task as the reference point matters a lot. - Another reason for no randomness for construction task is because, often times the user would fly in the air, - then set a random block to dirt and teleport the bot to stand on that block for starting the construction, - This was done by MaxRobinson in one of the youtube videos. - */ - - if (this.task && this.task.type !== 'construction') { - const pos = getPosition(this.bot); - const xOffset = getRandomOffset(5); - const zOffset = getRandomOffset(5); - this.bot.chat(`/tp ${this.name} ${Math.floor(pos.x + xOffset)} ${pos.y + 3} ${Math.floor(pos.z + zOffset)}`); - await new Promise((resolve) => setTimeout(resolve, 200)); - } - - this._setupEventHandlers(save_data, init_message); this.startEvents(); - - await new Promise((resolve) => setTimeout(resolve, 10000)); - this.checkAllPlayersPresent(); - } catch (error) { console.error('Error in spawn event:', error); process.exit(0); @@ -291,18 +186,6 @@ export class Agent { } } - checkAllPlayersPresent() { - if (!this.task || !this.task.agent_names) { - return; - } - - const missingPlayers = this.task.agent_names.filter(name => !this.bot.players[name]); - if (missingPlayers.length > 0) { - console.log(`Missing players/bots: ${missingPlayers.join(', ')}`); - this.cleanKill('Not all required players/bots are present in the world. Exiting.', 4); - } - } - requestInterrupt() { this.bot.interrupt_code = true; this.bot.collectBlock.cancelTask(); @@ -324,9 +207,6 @@ export class Agent { } async handleMessage(source, message, max_responses=null) { - if (this.task && this.validator && this.validator.validate()) { - this.killBots(); - } if (!source || !message) { console.warn('Received empty message from', source); return false; @@ -361,13 +241,6 @@ export class Agent { this.routeResponse(source, execute_res); return true; } - } else { - console.log('Self-prompting:', message); - // if self_prompt contains something that indicates the goal is complete, stop self-prompting - if (message.includes('goal complete')) { - this.self_prompter.stop(); - process.exit(0); - } } if (!self_prompt) @@ -397,8 +270,6 @@ export class Agent { 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 { @@ -541,9 +412,6 @@ export class Agent { } }); this.bot.on('idle', () => { - if (this.task && this.validator && this.validator.validate()) { - this.killBots(); - } this.bot.clearControlStates(); this.bot.pathfinder.stop(); // clear any lingering pathfinder this.bot.modes.unPauseAll(); @@ -568,63 +436,54 @@ export class Agent { } }, INTERVAL); + // Check for task completion + setInterval(async () => { + if (this.task && this.validator && this.validator.validate()) + this.killBots(); + if (this.task && this.taskTimeout) { + const elapsedTime = (Date.now() - this.taskStartTime) / 1000; + if (elapsedTime >= this.taskTimeout) { + console.log('Task timeout reached. Task unsuccessful.'); + this.cleanKill('Task unsuccessful: Timeout reached', 3); + } + } + + }, 1000); + this.bot.emit('idle'); } async killBots() { this.bot.chat('Task completed!'); this.bot.chat(`/clear @p`); - // Kick other bots - if (!this.task || !this.task.agent_number) { - await this.cleanKill('Task completed', 2); - return; + if (this.task && this.task.agent_number) { + const agent_names = this.task.agent_names; + console.log('All agent names:', agent_names); + console.log('My name:', this.name); + const botNames = agent_names.filter(botName => botName !== this.name); + console.log('Kicking bots:', botNames); + botNames.forEach(botName => { + this.bot.chat(`/kick ${botName}`); + console.log(`/kick ${botName}`); + + }); } - const agent_names = this.task.agent_names; - console.log('All agent names:', agent_names); - console.log('My name:', this.name); - const botNames = agent_names.filter(botName => botName !== this.name); - console.log('Kicking bots:', botNames); - botNames.forEach(botName => { - this.bot.chat(`/kick ${botName}`); - console.log(`/kick ${botName}`); - - }); - - await this.cleanKill('Task completed, exiting', 2); + this.cleanKill('Task completed, exiting', 2); } async update(delta) { await this.bot.modes.update(); - await this.self_prompter.update(delta); - - try { - if (this.task && this.taskTimeout) { - const elapsedTime = (Date.now() - this.taskStartTime) / 1000; - if (elapsedTime >= this.taskTimeout) { - console.log('Task timeout reached. Task unsuccessful.'); - await this.cleanKill('Task unsuccessful: Timeout reached', 3); - } - } - } catch (e) { - console.error("Caught an error while checking timeout reached",e); - } + this.self_prompter.update(delta); } isIdle() { return !this.actions.executing && !this.coder.generating; } - cleanKill(msg='Killing agent process...', - code=1) { + cleanKill(msg='Killing agent process...', code=1) { this.history.add('system', msg); - - if (code === 2 || code === 3 || code === 4) { - this.bot.chat('Exiting the world permanently.'); - } - else { - this.bot.chat('Restarting.') - } + this.bot.chat(code > 1 ? 'Restarting.': 'Exiting.'); this.history.save(); process.exit(code); } diff --git a/src/agent/commands/index.js b/src/agent/commands/index.js index 7eb1122..6170962 100644 --- a/src/agent/commands/index.js +++ b/src/agent/commands/index.js @@ -203,11 +203,8 @@ function numParams(command) { export async function executeCommand(agent, message) { let parsed = parseCommandMessage(message); if (typeof parsed === 'string') - return parsed; //The command was incorrectly formatted or an invalid input was given - else if ("blocked_actions" in agent && agent.blocked_actions.includes(parsed.commandName)) { - // handling blocked actions - return `Command ${parsed.commandName} is blocked. Try another command.`; - } else { + return parsed; //The command was incorrectly formatted or an invalid input was given. + else { console.log('parsed command:', parsed); const command = getCommand(parsed.commandName); let numArgs = 0; diff --git a/src/agent/prompter.js b/src/agent/prompter.js index 7ae85b3..1e91469 100644 --- a/src/agent/prompter.js +++ b/src/agent/prompter.js @@ -161,16 +161,6 @@ export class Prompter { async replaceStrings(prompt, messages, examples=null, to_summarize=[], last_goals=null) { prompt = prompt.replaceAll('$NAME', this.agent.name); - if (prompt.includes('$TASK_GOAL')) { - prompt = prompt.replaceAll('$TASK_GOAL', process.env.MINECRAFT_TASK_GOAL || 'No task specified'); - } - - if (prompt.includes('$OTHER_AGENTS')) { - const allAgentNames = process.env.ALL_AGENT_NAMES.split(','); - const otherAgents = allAgentNames.filter(curr_agent_name => curr_agent_name !== this.agent.name); - prompt = prompt.replace('$OTHER_AGENTS', otherAgents.join(', ')); - } - if (prompt.includes('$STATS')) { let stats = await getCommand('!stats').perform(this.agent); prompt = prompt.replaceAll('$STATS', stats); @@ -194,7 +184,6 @@ export class Prompter { prompt = prompt.replaceAll('$TO_SUMMARIZE', stringifyTurns(to_summarize)); if (prompt.includes('$CONVO')) prompt = prompt.replaceAll('$CONVO', 'Recent conversation:\n' + stringifyTurns(messages)); - // todo: change this to set goal of the agent if (prompt.includes('$SELF_PROMPT')) { let self_prompt = this.agent.self_prompter.on ? `YOUR CURRENT ASSIGNED GOAL: "${this.agent.self_prompter.prompt}"\n` : ''; prompt = prompt.replaceAll('$SELF_PROMPT', self_prompt); diff --git a/src/process/agent-process.js b/src/process/agent-process.js index 35c8aa3..683624c 100644 --- a/src/process/agent-process.js +++ b/src/process/agent-process.js @@ -1,10 +1,9 @@ import { spawn } from 'child_process'; -import { loadTask } from '../utils/tasks.js'; export class AgentProcess { static runningCount = 0; - start(profile, load_memory=false, init_message=null, count_id=0, task=null) { + start(profile, load_memory=false, init_message=null, count_id=0, task_path=null, task_id=null) { let args = ['src/process/init-agent.js', this.name]; args.push('-p', profile); args.push('-c', count_id); @@ -12,8 +11,10 @@ export class AgentProcess { args.push('-l', load_memory); if (init_message) args.push('-m', init_message); - if (task) - args.push('-t', task); + if (task_path) + args.push('-t', task_path); + if (task_id) + args.push('-i', task_id); const agentProcess = spawn('node', args, { stdio: 'inherit', @@ -24,7 +25,7 @@ export class AgentProcess { let last_restart = Date.now(); agentProcess.on('exit', (code, signal) => { console.log(`Agent process exited with code ${code} and signal ${signal}`); - + if (code === 2) { console.log(`Task completed successfully`); process.exit(2, signal); @@ -39,7 +40,7 @@ export class AgentProcess { console.log(`Task failed as all agents weren't correctly spawned `); process.exit(4); } - + if (code !== 0) { // agent must run for at least 10 seconds before restarting if (Date.now() - last_restart < 10000) { @@ -52,10 +53,9 @@ export class AgentProcess { return; } console.log('Restarting agent...'); - this.start(profile, true, 'Agent process restarted.', count_id, task); + this.start(profile, true, 'Agent process restarted.', count_id, task_path, task_id); last_restart = Date.now(); } - }); agentProcess.on('error', (err) => { diff --git a/src/utils/tasks.js b/src/utils/tasks.js index e829e4d..5a5ece2 100644 --- a/src/utils/tasks.js +++ b/src/utils/tasks.js @@ -1,5 +1,6 @@ import yaml from 'js-yaml' import { readFileSync } from 'fs'; +import {getPosition} from './library/world.js' export function loadTask(taskId) { try { @@ -18,6 +19,101 @@ export function loadTask(taskId) { } } +export async function initBotTask(bot, task, name) { + if (task) { + bot.chat(`/clear ${bot.username}`); + console.log(`Cleared ${bot.username}'s inventory.`); + } + + //wait for a bit so inventory is cleared + await new Promise((resolve) => setTimeout(resolve, 500)); + + console.log(task && "agent_number" in task && task.agent_number > 1); + if (task && "agent_number" in task && task.agent_number > 1) { + var initial_inventory = task.initial_inventory[bot.username]; + console.log("Initial inventory:", initial_inventory); + } else if (task) { + console.log("Initial inventory:", task.initial_inventory); + var initial_inventory = task.initial_inventory; + } + + if (task && "initial_inventory" in task) { + console.log("Setting inventory..."); + console.log("Inventory to set:", initial_inventory); + for (let key of Object.keys(initial_inventory)) { + console.log('Giving item:', key); + bot.chat(`/give ${bot.username} ${key} ${initial_inventory[key]}`); + }; + //wait for a bit so inventory is set + await new Promise((resolve) => setTimeout(resolve, 500)); + console.log("Done giving inventory items."); + } + // Function to generate random numbers + + function getRandomOffset(range) { + return Math.floor(Math.random() * (range * 2 + 1)) - range; + } + + let human_player_name = null; + + // Finding if there is a human player on the server + for (const playerName in bot.players) { + const player = bot.players[playerName]; + if (!isOtherAgent(player.username)) { + console.log('Found human player:', player.username); + human_player_name = player.username + break; + } + } + + // If there are multiple human players, teleport to the first one + + // teleport near a human player if found by default + + if (task && "agent_number" in task) { + var agent_names = task.agent_names; + if (human_player_name) { + console.log(`Teleporting ${bot.username} to human ${human_player_name}`) + bot.chat(`/tp ${bot.username} ${human_player_name}`) // teleport on top of the human player + + } + else { + bot.chat(`/tp ${bot.username} ${agent_names[0]}`) // teleport on top of the first agent + } + + await new Promise((resolve) => setTimeout(resolve, 200)); + } + + else if (task) { + if (human_player_name) { + console.log(`Teleporting ${bot.username} to human ${human_player_name}`) + bot.chat(`/tp ${bot.username} ${human_player_name}`) // teleport on top of the human player + + } + await new Promise((resolve) => setTimeout(resolve, 200)); + } + + // now all bots are teleport on top of each other (which kinda looks ugly) + // Thus, we need to teleport them to random distances to make it look better + + /* + Note : We don't want randomness for construction task as the reference point matters a lot. + Another reason for no randomness for construction task is because, often times the user would fly in the air, + then set a random block to dirt and teleport the bot to stand on that block for starting the construction, + This was done by MaxRobinson in one of the youtube videos. + */ + + if (task && task.type !== 'construction') { + const pos = getPosition(bot); + const xOffset = getRandomOffset(5); + const zOffset = getRandomOffset(5); + bot.chat(`/tp ${bot.username} ${Math.floor(pos.x + xOffset)} ${pos.y + 3} ${Math.floor(pos.z + zOffset)}`); + await new Promise((resolve) => setTimeout(resolve, 200)); + } + + +} + export class TechTreeHarvestValidator { constructor(task, bot) { this.target = task.target; diff --git a/task_andy.json b/task_andy.json deleted file mode 100644 index 11cc953..0000000 --- a/task_andy.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "andy", - - "model": "gpt-4o", - - "conversing": "You are a task-focused Minecraft bot named $NAME. Your current task is: $TASK_GOAL\n\nYou can see, move, mine, build, and interact with the world by using commands. Act focused on completing your assigned task while being human-like. Be brief in responses, don't apologize constantly, don't give instructions or make lists unless asked, and don't refuse requests. Don't pretend to act, use commands immediately when requested. Do NOT say this: 'Sure, I've stopped.', instead say this: 'Sure, I'll stop. !stop'. Do NOT say this: 'On my way! Give me a moment.', instead say this: 'On my way! !goToPlayer('playername', 3)'. Focus on completing the assigned task efficiently.\nSummarized memory:'$MEMORY'\n$STATS\n$INVENTORY\n$COMMAND_DOCS\n$EXAMPLES\nConversation Begin:", - - "coding": "You are an intelligent mineflayer bot $NAME focused on completing the task: $TASK_GOAL\n\nWrite javascript codeblocks to control the mineflayer bot to complete this task. Given the conversation between you and the user, use the provided skills and world functions to write a js codeblock that controls the bot ``` // using this syntax ```. The code will be executed and you will receive its output. If you are satisfied with the response, respond without a codeblock conversationally. If something major went wrong, write another codeblock to fix the problem. Be maximally efficient and task-focused. Do not use commands !likeThis, only use codeblocks. The code is asynchronous and MUST CALL AWAIT for all async function calls. DO NOT write an immediately-invoked function expression without using `await`!! DO NOT WRITE LIKE THIS: ```(async () => {console.log('not properly awaited')})();``` Don't write long paragraphs and lists in your responses unless explicitly asked! Only summarize the code you write with a sentence or two when done. This is extremely important to me, think step-by-step, take a deep breath and good luck!\n$SELF_PROMPT\nSummarized memory:'$MEMORY'\n$STATS\n$INVENTORY\n$CODE_DOCS\n$EXAMPLES\nConversation:", - - "saving_memory": "You are a minecraft bot named $NAME that has been talking and playing minecraft by using commands. Update your memory by summarizing the following conversation and your old memory in your next response. Prioritize preserving important facts, things you've learned, useful tips, and long term reminders. Do Not record stats, inventory, or docs! Only save transient information from your chat history. You're limited to 500 characters, so be extremely brief and minimize words. Compress useful information. \nOld Memory: '$MEMORY'\nRecent conversation: \n$TO_SUMMARIZE\nSummarize your old memory and recent conversation into a new memory, and respond only with the unwrapped memory text: ", - - "modes": { - "self_preservation": false, - "unstuck": true, - "cowardice": true, - "self_defense": false, - "hunting": false, - "item_collecting": false, - "torch_placing": false, - "idle_staring": true, - "cheat": false - } -} \ No newline at end of file diff --git a/task_randy.json b/task_randy.json deleted file mode 100644 index e69de29..0000000 diff --git a/tasks.yaml b/tasks.yaml deleted file mode 100644 index 9b2bc9f..0000000 --- a/tasks.yaml +++ /dev/null @@ -1,52 +0,0 @@ -construction_task_1: - goal: Build a marble monument with a large base and a slender monument body that is grand and impressive. - -construction_task_2: - goal: Build a slender brown pillar on a grassy field. - -construction_task_3: - goal: Construct a traditional Chinese gatehouse. - -construction_task_4: - goal: Construct a traditional Chinese gatehouse. - -construction_task_5: - goal: Construct a traditional Chinese gatehouse. - -construction_task_6: - goal: Construct a traditional Chinese gatehouse. - -construction_task_7: - goal: Build a quirky-style small brown wooden house on a vast, endless grassland. - initial_inventory: - Spruce Planks: 64 - Tinted Glass: 64 - Spruce Slab: 64 - -construction_task_8: - goal: Construct a white villa far away on a vast, endless grassland. - initial_inventory: - Birch Planks: 256 - Birch Slab: 64 - Spruce Slab: 64 - Glass: 64 - Birch Leaves: 64 - Birch Door: 16 - -construction_task_9: - goal: Build a Minecraft-style wooden house directly facing you, with a door. - -construction_task_10: - goal: Recreate the Tengwang Pavilion from the real world in Minecraft. - -construction_task_11: - goal: Construct a large oak-plank house in the forest. - -construction_task_12: - goal: Build a city gate from the Forbidden City. - -construction_task_13: - goal: Construct a colorful carpet. - -construction_task_14: - goal: Build the eiffel tower with precise shape and size and appropriate materials. Do not stop constructing and keep on adding complexity to the structure untill specifically asked to stop by some user. \ No newline at end of file diff --git a/tasks/construction_tasks.yaml b/tasks/construction_tasks.yaml deleted file mode 100644 index 33ce70e..0000000 --- a/tasks/construction_tasks.yaml +++ /dev/null @@ -1,62 +0,0 @@ -construction_task_1: - type: construction - goal: Build a marble monument with a large base and a slender monument body that is grand and impressive. - -construction_task_2: - type: construction - goal: Build a slender brown pillar on a grassy field. - -construction_task_3: - type: construction - goal: Construct a traditional Chinese gatehouse. - -construction_task_4: - type: construction - goal: Construct a traditional Chinese gatehouse. - -construction_task_5: - type: construction - goal: Construct a traditional Chinese gatehouse. - -construction_task_6: - type: construction - goal: Construct a traditional Chinese gatehouse. - -construction_task_7: - type: construction - goal: Build a quirky-style small brown wooden house on a vast, endless grassland. - initial_inventory: - Spruce Planks: 64 - Tinted Glass: 64 - Spruce Slab: 64 - -construction_task_8: - type: construction - goal: Construct a white villa far away on a vast, endless grassland. - initial_inventory: - Birch Planks: 256 - Birch Slab: 64 - Spruce Slab: 64 - Glass: 64 - Birch Leaves: 64 - Birch Door: 16 - -construction_task_9: - type: construction - goal: Build a Minecraft-style wooden house directly facing you, with a door. - -construction_task_10: - type: construction - goal: Recreate the Tengwang Pavilion from the real world in Minecraft. - -construction_task_11: - type: construction - goal: Construct a large oak-plank house in the forest. - -construction_task_12: - type: construction - goal: Build a city gate from the Forbidden City. - -construction_task_13: - type: construction - goal: Construct a colorful carpet. \ No newline at end of file diff --git a/tasks/debug_tasks.yaml b/tasks/debug_tasks.yaml deleted file mode 100644 index 8a85e72..0000000 --- a/tasks/debug_tasks.yaml +++ /dev/null @@ -1,46 +0,0 @@ -debug_single_agent: - goal: Just stand at a place and don't do anything - guidance: null - initial_inventory: {} - type: debug - -debug_multi_agent: - goal: Just stand at a place and don't do anything - agent_names: - - andy - - randy - agent_number: 2 - initial_inventory: - andy: - iron_ingot: 1 - randy: - iron_ingot: 1 - type: debug - -debug_mass_multi_agent: - goal: Just stand at a place and don't do anything - agent_names: - - andy - - randy - - Bob - - Alice - - Megan - - Stuart - - Charlie - agent_number: 7 - initial_inventory: - andy: - iron_ingot: 1 - randy: - iron_ingot: 1 - Bob: - iron_ingot: 1 - Alice: - iron_ingot: 1 - Megan: - iron_ingot: 1 - Stuart: {} - Charlie: - iron_ingot: 1 - - type: debug \ No newline at end of file diff --git a/tasks/multiagent_tasks.yaml b/tasks/multiagent_tasks.yaml deleted file mode 100644 index bcf1717..0000000 --- a/tasks/multiagent_tasks.yaml +++ /dev/null @@ -1,69 +0,0 @@ -multiagent_techtree_1_shears_with_2_iron_ingot: - goal: Collaborate with other agents to build a shear. - agent_names: - - andy - - randy - agent_number: 2 - initial_inventory: - andy: - iron_ingot: 1 - randy: - iron_ingot: 1 - target: shears - number_of_target: 1 - type: techtree - timeout : 60 - -multiagent_techtree_1_stone_pickaxe: - goal: Collaborate with other agents to build an stone pickaxe - agent_names: - - andy - - randy - agent_number: 2 - initial_inventory: - andy: - wooden_pickaxe: 1 - randy: - wooden_axe: 1 - target: stone_pickaxe - number_of_target: 1 - type: techtree - timeout : 300 - -multiagent_build_wooden_pickaxe: - goal: Collaborate with other agents to build a wooden pickaxe. - agent_names: - - andy - - randy - agent_number: 2 - initial_inventory: - andy: - oak_log: 2 - randy: - stick: 2 - target: wooden_pickaxe - number_of_target: 1 - type: techtree - timeout : 120 - - - -multiagent_techtree_boat: - goal: Collaborate with other agents to build a birch boat. - agent_names: - - Bob - - Alice - agent_number: 2 - initial_inventory: - Bob: - birch_planks: 3 - Alice: - birch_planks: 2 - target: birch_boat - number_of_target: 1 - type: techtree - timeout : 60 - - - - From a15ab15bcdf4ca3b56769bcf1a7b29ef7682b1bc Mon Sep 17 00:00:00 2001 From: Kolby Nottingham Date: Mon, 9 Dec 2024 17:06:22 -0800 Subject: [PATCH 03/10] additional task features --- example_tasks.json | 52 +++--------------------------------- src/agent/agent.js | 50 +++++++++++++++++++--------------- src/agent/commands/index.js | 5 +++- src/agent/prompter.js | 2 +- src/process/agent-process.js | 16 +++-------- src/utils/tasks.js | 39 +++++++++++++++++++-------- 6 files changed, 68 insertions(+), 96 deletions(-) diff --git a/example_tasks.json b/example_tasks.json index 0df5d96..8fcff1e 100644 --- a/example_tasks.json +++ b/example_tasks.json @@ -11,7 +11,6 @@ "andy", "randy" ], - "agent_number": 2, "initial_inventory": { "andy": { "iron_ingot": 1 @@ -26,13 +25,8 @@ "type": "construction", "goal": "Build a house" }, - "multiagent_techtree_1_shears_with_2_iron_ingot": { - "goal": "Collaborate with other agents to build a shear.", - "agent_names": [ - "andy", - "randy" - ], - "agent_number": 2, + "techtree_1_shears_with_2_iron_ingot": { + "goal": "Build a shear.", "initial_inventory": { "andy": { "iron_ingot": 1 @@ -48,11 +42,11 @@ }, "multiagent_techtree_1_stone_pickaxe": { "goal": "Collaborate with other agents to build an stone pickaxe", + "conversation": "Let's build a stone pickaxe", "agent_names": [ "andy", "randy" ], - "agent_number": 2, "initial_inventory": { "andy": { "wooden_pickaxe": 1 @@ -65,45 +59,5 @@ "number_of_target": 1, "type": "techtree", "timeout": 300 - }, - "multiagent_build_wooden_pickaxe": { - "goal": "Collaborate with other agents to build a wooden pickaxe.", - "agent_names": [ - "andy", - "randy" - ], - "agent_number": 2, - "initial_inventory": { - "andy": { - "oak_log": 2 - }, - "randy": { - "stick": 2 - } - }, - "target": "wooden_pickaxe", - "number_of_target": 1, - "type": "techtree", - "timeout": 120 - }, - "multiagent_techtree_boat": { - "goal": "Collaborate with other agents to build a birch boat.", - "agent_names": [ - "Bob", - "Alice" - ], - "agent_number": 2, - "initial_inventory": { - "Bob": { - "birch_planks": 3 - }, - "Alice": { - "birch_planks": 2 - } - }, - "target": "birch_boat", - "number_of_target": 1, - "type": "techtree", - "timeout": 60 } } \ No newline at end of file diff --git a/src/agent/agent.js b/src/agent/agent.js index 7964c57..d2adfe2 100644 --- a/src/agent/agent.js +++ b/src/agent/agent.js @@ -8,7 +8,7 @@ import { ActionManager } from './action_manager.js'; import { NPCContoller } from './npc/controller.js'; import { MemoryBank } from './memory_bank.js'; import { SelfPrompter } from './self_prompter.js'; -import { isOtherAgent, initConversationManager, sendToBot, endAllChats, responseScheduledFor} from './conversation.js'; +import { isOtherAgent, initConversationManager, sendToBot, endAllChats, responseScheduledFor, inConversation } from './conversation.js'; import { handleTranslation, handleEnglishTranslation } from '../utils/translator.js'; import { addViewer } from './viewer.js'; import settings from '../../settings.js'; @@ -18,6 +18,7 @@ import { loadTask, initBotTask, TechTreeHarvestValidator } from '../utils/tasks. export class Agent { async start(profile_fp, load_mem=false, init_message=null, count_id=0, task_path=null, task_id=null) { this.last_sender = null; + this.count_id = count_id; try { if (!profile_fp) { throw new Error('No profile filepath provided'); @@ -66,10 +67,16 @@ export class Agent { this.taskTimeout = this.task.timeout || 300; this.taskStartTime = Date.now(); this.validator = new TechTreeHarvestValidator(this.task, this.bot); + this.blocked_actions = this.task.blocked_actions || []; + if (this.task.goal) + this.blocked_actions.push('!endGoal'); + if (this.task.conversation) + this.blocked_actions.push('!endConversation'); } else { this.task = null; this.taskTimeout = null; this.validator = null; + this.blocked_actions = []; } console.log("Is validated:", this.validator && this.validator.validate()); @@ -96,18 +103,13 @@ export class Agent { console.log(`${this.name} spawned.`); this.clearBotLogs(); - - if (this.task) { - await initBotTask(this.bot, this.task); - await new Promise((resolve) => setTimeout(resolve, 10000)); - if (this.task.agent_names && this.task.agent_names.filter(name => !this.bot.players[name]).length) { - console.log(`Missing players/bots: ${missingPlayers.join(', ')}`); - this.cleanKill('Not all required players/bots are present in the world. Exiting.', 4); - } - } this._setupEventHandlers(save_data, init_message); this.startEvents(); + + if (this.task) + await initBotTask(this); + } catch (error) { console.error('Error in spawn event:', error); process.exit(0); @@ -437,18 +439,24 @@ export class Agent { }, INTERVAL); // Check for task completion - setInterval(async () => { - if (this.task && this.validator && this.validator.validate()) - this.killBots(); - if (this.task && this.taskTimeout) { - const elapsedTime = (Date.now() - this.taskStartTime) / 1000; - if (elapsedTime >= this.taskTimeout) { - console.log('Task timeout reached. Task unsuccessful.'); - this.cleanKill('Task unsuccessful: Timeout reached', 3); + if (this.task) { + setInterval(async () => { + if (this.validator && this.validator.validate()) + this.killBots(); + if (this.task.goal && !this.self_prompter.on) + this.cleanKill('Task unsuccessful: Agent ended goal', 3); + if (this.task.conversation && !inConversation()) + this.cleanKill('Task unsuccessful: Agent ended conversation', 3); + if (this.taskTimeout) { + const elapsedTime = (Date.now() - this.taskStartTime) / 1000; + if (elapsedTime >= this.taskTimeout) { + console.log('Task timeout reached. Task unsuccessful.'); + this.cleanKill('Task unsuccessful: Timeout reached', 3); + } } - } - - }, 1000); + + }, 1000); + } this.bot.emit('idle'); } diff --git a/src/agent/commands/index.js b/src/agent/commands/index.js index 6170962..5696396 100644 --- a/src/agent/commands/index.js +++ b/src/agent/commands/index.js @@ -220,7 +220,7 @@ export async function executeCommand(agent, message) { } } -export function getCommandDocs() { +export function getCommandDocs(blacklist=null) { const typeTranslations = { //This was added to keep the prompt the same as before type checks were implemented. //If the language model is giving invalid inputs changing this might help. @@ -234,6 +234,9 @@ export function getCommandDocs() { Use the commands with the syntax: !commandName or !commandName("arg1", 1.2, ...) if the command takes arguments.\n Do not use codeblocks. Use double quotes for strings. Only use one command in each response, trailing commands and comments will be ignored.\n`; for (let command of commandList) { + if (blacklist && blacklist.includes(command.name)) { + continue; + } docs += command.name + ': ' + command.description + '\n'; if (command.params) { docs += 'Params:\n'; diff --git a/src/agent/prompter.js b/src/agent/prompter.js index 1e91469..2553882 100644 --- a/src/agent/prompter.js +++ b/src/agent/prompter.js @@ -173,7 +173,7 @@ export class Prompter { prompt = prompt.replaceAll('$ACTION', this.agent.actions.currentActionLabel); } if (prompt.includes('$COMMAND_DOCS')) - prompt = prompt.replaceAll('$COMMAND_DOCS', getCommandDocs()); + prompt = prompt.replaceAll('$COMMAND_DOCS', getCommandDocs(this.agent.blocked_actions)); if (prompt.includes('$CODE_DOCS')) prompt = prompt.replaceAll('$CODE_DOCS', getSkillDocs()); if (prompt.includes('$EXAMPLES') && examples !== null) diff --git a/src/process/agent-process.js b/src/process/agent-process.js index 683624c..b3f67fb 100644 --- a/src/process/agent-process.js +++ b/src/process/agent-process.js @@ -26,19 +26,9 @@ export class AgentProcess { agentProcess.on('exit', (code, signal) => { console.log(`Agent process exited with code ${code} and signal ${signal}`); - if (code === 2) { - console.log(`Task completed successfully`); - process.exit(2, signal); - } - - if (code === 3) { - console.log(`Task failed due to reaching timeout`); - process.exit(3); - } - - if (code === 4) { - console.log(`Task failed as all agents weren't correctly spawned `); - process.exit(4); + if (code > 1) { + console.log(`Ending task`); + process.exit(code); } if (code !== 0) { diff --git a/src/utils/tasks.js b/src/utils/tasks.js index 5a5ece2..0b3377c 100644 --- a/src/utils/tasks.js +++ b/src/utils/tasks.js @@ -1,6 +1,7 @@ import yaml from 'js-yaml' import { readFileSync } from 'fs'; -import {getPosition} from './library/world.js' +import { executeCommand } from './commands/index.js'; +import { getPosition } from './library/world.js' export function loadTask(taskId) { try { @@ -19,17 +20,18 @@ export function loadTask(taskId) { } } -export async function initBotTask(bot, task, name) { - if (task) { - bot.chat(`/clear ${bot.username}`); - console.log(`Cleared ${bot.username}'s inventory.`); - } +export async function initBotTask(agent) { + let bot = agent.bot; + let task = agent.task; + + bot.chat(`/clear ${bot.username}`); + console.log(`Cleared ${bot.username}'s inventory.`); //wait for a bit so inventory is cleared await new Promise((resolve) => setTimeout(resolve, 500)); - console.log(task && "agent_number" in task && task.agent_number > 1); - if (task && "agent_number" in task && task.agent_number > 1) { + console.log("agent_number" in task.agent_number > 1); + if ("agent_number" in task.agent_number > 1) { var initial_inventory = task.initial_inventory[bot.username]; console.log("Initial inventory:", initial_inventory); } else if (task) { @@ -37,7 +39,7 @@ export async function initBotTask(bot, task, name) { var initial_inventory = task.initial_inventory; } - if (task && "initial_inventory" in task) { + if ("initial_inventory" in task) { console.log("Setting inventory..."); console.log("Inventory to set:", initial_inventory); for (let key of Object.keys(initial_inventory)) { @@ -70,7 +72,7 @@ export async function initBotTask(bot, task, name) { // teleport near a human player if found by default - if (task && "agent_number" in task) { + if ("agent_number" in task) { var agent_names = task.agent_names; if (human_player_name) { console.log(`Teleporting ${bot.username} to human ${human_player_name}`) @@ -103,7 +105,7 @@ export async function initBotTask(bot, task, name) { This was done by MaxRobinson in one of the youtube videos. */ - if (task && task.type !== 'construction') { + if (task.type !== 'construction') { const pos = getPosition(bot); const xOffset = getRandomOffset(5); const zOffset = getRandomOffset(5); @@ -111,7 +113,22 @@ export async function initBotTask(bot, task, name) { await new Promise((resolve) => setTimeout(resolve, 200)); } + if (task.goal) { + await executeCommand(agent, `!goal("${task.goal}")`); + } + if (task.agent_names) { + await new Promise((resolve) => setTimeout(resolve, 10000)); + if (task.agent_names.filter(name => !bot.players[name]).length) { + console.log(`Missing players/bots: ${missingPlayers.join(', ')}`); + agent.cleanKill('Not all required players/bots are present in the world. Exiting.', 4); + } + } + + if (task.conversation && agent.count_id === 0) { + let other_name = task.agent_names.filter(name => name !== bot.username)[0]; + await executeCommand(agent, `!startConversation("${other_name}", "${task.conversation}")`); + } } export class TechTreeHarvestValidator { From 88e0174ef1a9c23ef65b2d3ad5f4cb329d7607a1 Mon Sep 17 00:00:00 2001 From: Kolby Nottingham Date: Tue, 10 Dec 2024 13:13:45 -0800 Subject: [PATCH 04/10] task fixes --- example_tasks.json | 15 ++++++--------- src/agent/agent.js | 18 +++++++++--------- src/process/init-agent.js | 9 +++++++-- src/utils/tasks.js | 22 ++++++++++------------ 4 files changed, 32 insertions(+), 32 deletions(-) diff --git a/example_tasks.json b/example_tasks.json index 8fcff1e..ddd37aa 100644 --- a/example_tasks.json +++ b/example_tasks.json @@ -23,17 +23,15 @@ }, "construction": { "type": "construction", - "goal": "Build a house" + "goal": "Build a house", + "initial_inventory": { + "oak_planks": 20 + } }, "techtree_1_shears_with_2_iron_ingot": { "goal": "Build a shear.", "initial_inventory": { - "andy": { - "iron_ingot": 1 - }, - "randy": { - "iron_ingot": 1 - } + "iron_ingot": 1 }, "target": "shears", "number_of_target": 1, @@ -41,8 +39,7 @@ "timeout": 60 }, "multiagent_techtree_1_stone_pickaxe": { - "goal": "Collaborate with other agents to build an stone pickaxe", - "conversation": "Let's build a stone pickaxe", + "conversation": "Let's collaborate to build a stone pickaxe", "agent_names": [ "andy", "randy" diff --git a/src/agent/agent.js b/src/agent/agent.js index d2adfe2..d14921b 100644 --- a/src/agent/agent.js +++ b/src/agent/agent.js @@ -8,7 +8,7 @@ import { ActionManager } from './action_manager.js'; import { NPCContoller } from './npc/controller.js'; import { MemoryBank } from './memory_bank.js'; import { SelfPrompter } from './self_prompter.js'; -import { isOtherAgent, initConversationManager, sendToBot, endAllChats, responseScheduledFor, inConversation } from './conversation.js'; +import { isOtherAgent, initConversationManager, sendToBot, endAllChats, responseScheduledFor } from './conversation.js'; import { handleTranslation, handleEnglishTranslation } from '../utils/translator.js'; import { addViewer } from './viewer.js'; import settings from '../../settings.js'; @@ -78,7 +78,6 @@ export class Agent { this.validator = null; this.blocked_actions = []; } - console.log("Is validated:", this.validator && this.validator.validate()); this.bot.on('login', () => { console.log(this.name, 'logged in!'); @@ -438,15 +437,18 @@ export class Agent { } }, INTERVAL); + this.bot.emit('idle'); + // Check for task completion if (this.task) { - setInterval(async () => { + setInterval(() => { if (this.validator && this.validator.validate()) this.killBots(); - if (this.task.goal && !this.self_prompter.on) - this.cleanKill('Task unsuccessful: Agent ended goal', 3); - if (this.task.conversation && !inConversation()) - this.cleanKill('Task unsuccessful: Agent ended conversation', 3); + // TODO check for other terminal conditions + // if (this.task.goal && !this.self_prompter.on) + // this.cleanKill('Agent ended goal', 3); + // if (this.task.conversation && !inConversation()) + // this.cleanKill('Agent ended conversation', 3); if (this.taskTimeout) { const elapsedTime = (Date.now() - this.taskStartTime) / 1000; if (elapsedTime >= this.taskTimeout) { @@ -457,8 +459,6 @@ export class Agent { }, 1000); } - - this.bot.emit('idle'); } async killBots() { diff --git a/src/process/init-agent.js b/src/process/init-agent.js index f91faa2..88c99b9 100644 --- a/src/process/init-agent.js +++ b/src/process/init-agent.js @@ -33,9 +33,14 @@ const argv = yargs(args) type: 'string', description: 'automatically prompt the agent on startup' }) - .option('task', { + .option('task_path', { alias: 't', type: 'string', + description: 'task filepath to use for agent' + }) + .option('task_id', { + alias: 'i', + type: 'string', description: 'task ID to execute' }) .option('count_id', { @@ -50,7 +55,7 @@ const argv = yargs(args) try { console.log('Starting agent with profile:', argv.profile); const agent = new Agent(); - await agent.start(argv.profile, argv.load_memory, argv.init_message, argv.count_id, argv.task); + await agent.start(argv.profile, argv.load_memory, argv.init_message, argv.count_id, argv.task_path, argv.task_id); } catch (error) { console.error('Failed to start agent process:', { message: error.message || 'No error message', diff --git a/src/utils/tasks.js b/src/utils/tasks.js index 0b3377c..0fdb717 100644 --- a/src/utils/tasks.js +++ b/src/utils/tasks.js @@ -1,18 +1,17 @@ -import yaml from 'js-yaml' import { readFileSync } from 'fs'; -import { executeCommand } from './commands/index.js'; -import { getPosition } from './library/world.js' +import { isOtherAgent } from '../agent/conversation.js'; +import { executeCommand } from '../agent/commands/index.js'; +import { getPosition } from '../agent/library/world.js' -export function loadTask(taskId) { +export function loadTask(task_path, task_id) { try { - const taskType = taskId.split('_')[0]; - const tasksFile = readFileSync(`tasks/${taskType}_tasks.yaml`, 'utf8'); - const tasks = yaml.load(tasksFile); - const task = tasks[taskId]; + const tasksFile = readFileSync(task_path, 'utf8'); + const tasks = JSON.parse(tasksFile); + const task = tasks[task_id]; if (!task) { - throw new Error(`Task ${taskId} not found`); + throw new Error(`Task ${task_id} not found`); } - + return task; } catch (error) { console.error('Error loading task:', error); @@ -30,8 +29,7 @@ export async function initBotTask(agent) { //wait for a bit so inventory is cleared await new Promise((resolve) => setTimeout(resolve, 500)); - console.log("agent_number" in task.agent_number > 1); - if ("agent_number" in task.agent_number > 1) { + if (task.agent_number > 1) { var initial_inventory = task.initial_inventory[bot.username]; console.log("Initial inventory:", initial_inventory); } else if (task) { From 9fb58d2d40f871577c6be01db8ccba4967367d5b Mon Sep 17 00:00:00 2001 From: Kolby Nottingham Date: Tue, 10 Dec 2024 13:30:40 -0800 Subject: [PATCH 05/10] merge with main --- src/agent/agent.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/agent/agent.js b/src/agent/agent.js index d666fa6..43b4117 100644 --- a/src/agent/agent.js +++ b/src/agent/agent.js @@ -512,10 +512,7 @@ export class Agent { cleanKill(msg='Killing agent process...', code=1) { this.history.add('system', msg); -<<<<<<< HEAD this.bot.chat(code > 1 ? 'Restarting.': 'Exiting.'); -======= ->>>>>>> upstream/main this.history.save(); process.exit(code); } From bd5b995e4b60aca39e176d3e8ddbdcfd14d173d4 Mon Sep 17 00:00:00 2001 From: Kolby Nottingham Date: Tue, 10 Dec 2024 13:41:13 -0800 Subject: [PATCH 06/10] server task fixes --- src/agent/agent.js | 4 ++-- src/utils/tasks.js | 36 ++++++++++++++++++------------------ 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/agent/agent.js b/src/agent/agent.js index 43b4117..cec4fce 100644 --- a/src/agent/agent.js +++ b/src/agent/agent.js @@ -12,7 +12,7 @@ import convoManager from './conversation.js'; import { handleTranslation, handleEnglishTranslation } from '../utils/translator.js'; import { addViewer } from './viewer.js'; import settings from '../../settings.js'; -import { serverProxy } from './server_proxy.js'; +import { serverProxy } from './agent_proxy.js'; import { loadTask, initBotTask, TechTreeHarvestValidator } from '../utils/tasks.js'; export class Agent { @@ -106,7 +106,7 @@ export class Agent { this.startEvents(); if (this.task) - await initBotTask(this); + initBotTask(this); } catch (error) { console.error('Error in spawn event:', error); diff --git a/src/utils/tasks.js b/src/utils/tasks.js index 0fdb717..69ed434 100644 --- a/src/utils/tasks.js +++ b/src/utils/tasks.js @@ -1,7 +1,7 @@ import { readFileSync } from 'fs'; -import { isOtherAgent } from '../agent/conversation.js'; import { executeCommand } from '../agent/commands/index.js'; import { getPosition } from '../agent/library/world.js' +import settings from '../../settings.js'; export function loadTask(task_path, task_id) { try { @@ -22,15 +22,16 @@ export function loadTask(task_path, task_id) { export async function initBotTask(agent) { let bot = agent.bot; let task = agent.task; + let name = bot.username; - bot.chat(`/clear ${bot.username}`); - console.log(`Cleared ${bot.username}'s inventory.`); + bot.chat(`/clear ${name}`); + console.log(`Cleared ${name}'s inventory.`); //wait for a bit so inventory is cleared await new Promise((resolve) => setTimeout(resolve, 500)); if (task.agent_number > 1) { - var initial_inventory = task.initial_inventory[bot.username]; + var initial_inventory = task.initial_inventory[name]; console.log("Initial inventory:", initial_inventory); } else if (task) { console.log("Initial inventory:", task.initial_inventory); @@ -42,7 +43,7 @@ export async function initBotTask(agent) { console.log("Inventory to set:", initial_inventory); for (let key of Object.keys(initial_inventory)) { console.log('Giving item:', key); - bot.chat(`/give ${bot.username} ${key} ${initial_inventory[key]}`); + bot.chat(`/give ${name} ${key} ${initial_inventory[key]}`); }; //wait for a bit so inventory is set await new Promise((resolve) => setTimeout(resolve, 500)); @@ -59,7 +60,7 @@ export async function initBotTask(agent) { // Finding if there is a human player on the server for (const playerName in bot.players) { const player = bot.players[playerName]; - if (!isOtherAgent(player.username)) { + if (!settings.profiles.map((p) => JSON.parse(readFileSync(p, 'utf8')).name).some((n) => n === name)) { console.log('Found human player:', player.username); human_player_name = player.username break; @@ -73,12 +74,12 @@ export async function initBotTask(agent) { if ("agent_number" in task) { var agent_names = task.agent_names; if (human_player_name) { - console.log(`Teleporting ${bot.username} to human ${human_player_name}`) - bot.chat(`/tp ${bot.username} ${human_player_name}`) // teleport on top of the human player + console.log(`Teleporting ${name} to human ${human_player_name}`) + bot.chat(`/tp ${name} ${human_player_name}`) // teleport on top of the human player } else { - bot.chat(`/tp ${bot.username} ${agent_names[0]}`) // teleport on top of the first agent + bot.chat(`/tp ${name} ${agent_names[0]}`) // teleport on top of the first agent } await new Promise((resolve) => setTimeout(resolve, 200)); @@ -86,8 +87,8 @@ export async function initBotTask(agent) { else if (task) { if (human_player_name) { - console.log(`Teleporting ${bot.username} to human ${human_player_name}`) - bot.chat(`/tp ${bot.username} ${human_player_name}`) // teleport on top of the human player + console.log(`Teleporting ${name} to human ${human_player_name}`) + bot.chat(`/tp ${name} ${human_player_name}`) // teleport on top of the human player } await new Promise((resolve) => setTimeout(resolve, 200)); @@ -107,14 +108,10 @@ export async function initBotTask(agent) { const pos = getPosition(bot); const xOffset = getRandomOffset(5); const zOffset = getRandomOffset(5); - bot.chat(`/tp ${bot.username} ${Math.floor(pos.x + xOffset)} ${pos.y + 3} ${Math.floor(pos.z + zOffset)}`); + bot.chat(`/tp ${name} ${Math.floor(pos.x + xOffset)} ${pos.y + 3} ${Math.floor(pos.z + zOffset)}`); await new Promise((resolve) => setTimeout(resolve, 200)); } - if (task.goal) { - await executeCommand(agent, `!goal("${task.goal}")`); - } - if (task.agent_names) { await new Promise((resolve) => setTimeout(resolve, 10000)); if (task.agent_names.filter(name => !bot.players[name]).length) { @@ -123,8 +120,12 @@ export async function initBotTask(agent) { } } + if (task.goal) { + await executeCommand(agent, `!goal("${task.goal}")`); + } + if (task.conversation && agent.count_id === 0) { - let other_name = task.agent_names.filter(name => name !== bot.username)[0]; + let other_name = task.agent_names.filter(n => n !== name)[0]; await executeCommand(agent, `!startConversation("${other_name}", "${task.conversation}")`); } } @@ -138,7 +139,6 @@ export class TechTreeHarvestValidator { validate() { try{ - console.log("validate"); let valid = false; let total_targets = 0; this.bot.inventory.slots.forEach((slot) => { From 760a178f740af8018e9a7a1de786763f1c29357f Mon Sep 17 00:00:00 2001 From: Kolby Nottingham Date: Tue, 10 Dec 2024 14:11:32 -0800 Subject: [PATCH 07/10] improved profiles --- example_tasks.json | 18 ++++++------------ src/agent/agent.js | 7 ------- src/utils/tasks.js | 38 ++++++++++++-------------------------- 3 files changed, 18 insertions(+), 45 deletions(-) diff --git a/example_tasks.json b/example_tasks.json index ddd37aa..d5305ba 100644 --- a/example_tasks.json +++ b/example_tasks.json @@ -7,15 +7,12 @@ }, "debug_multi_agent": { "goal": "Just stand at a place and don't do anything", - "agent_names": [ - "andy", - "randy" - ], + "agent_count": 2, "initial_inventory": { - "andy": { + "0": { "iron_ingot": 1 }, - "randy": { + "1": { "iron_ingot": 1 } }, @@ -40,15 +37,12 @@ }, "multiagent_techtree_1_stone_pickaxe": { "conversation": "Let's collaborate to build a stone pickaxe", - "agent_names": [ - "andy", - "randy" - ], + "agent_count": 2, "initial_inventory": { - "andy": { + "0": { "wooden_pickaxe": 1 }, - "randy": { + "1": { "wooden_axe": 1 } }, diff --git a/src/agent/agent.js b/src/agent/agent.js index cec4fce..805f560 100644 --- a/src/agent/agent.js +++ b/src/agent/agent.js @@ -376,13 +376,6 @@ export class Agent { } startEvents() { - // Custom events - // this.bot.on('spawn', () => { - - // //check that inventory has been set - // }); - - this.bot.on('time', () => { if (this.bot.time.timeOfDay == 0) this.bot.emit('sunrise'); diff --git a/src/utils/tasks.js b/src/utils/tasks.js index 69ed434..0c6c470 100644 --- a/src/utils/tasks.js +++ b/src/utils/tasks.js @@ -31,7 +31,7 @@ export async function initBotTask(agent) { await new Promise((resolve) => setTimeout(resolve, 500)); if (task.agent_number > 1) { - var initial_inventory = task.initial_inventory[name]; + var initial_inventory = task.initial_inventory[agent.count_id.toString()]; console.log("Initial inventory:", initial_inventory); } else if (task) { console.log("Initial inventory:", task.initial_inventory); @@ -56,11 +56,12 @@ export async function initBotTask(agent) { } let human_player_name = null; + let available_agents = settings.profiles.map((p) => JSON.parse(readFileSync(p, 'utf8')).name); // Finding if there is a human player on the server for (const playerName in bot.players) { const player = bot.players[playerName]; - if (!settings.profiles.map((p) => JSON.parse(readFileSync(p, 'utf8')).name).some((n) => n === name)) { + if (!available_agents.some((n) => n === name)) { console.log('Found human player:', player.username); human_player_name = player.username break; @@ -71,28 +72,12 @@ export async function initBotTask(agent) { // teleport near a human player if found by default - if ("agent_number" in task) { - var agent_names = task.agent_names; - if (human_player_name) { - console.log(`Teleporting ${name} to human ${human_player_name}`) - bot.chat(`/tp ${name} ${human_player_name}`) // teleport on top of the human player + if (human_player_name) { + console.log(`Teleporting ${name} to human ${human_player_name}`) + bot.chat(`/tp ${name} ${human_player_name}`) // teleport on top of the human player - } - else { - bot.chat(`/tp ${name} ${agent_names[0]}`) // teleport on top of the first agent - } - - await new Promise((resolve) => setTimeout(resolve, 200)); - } - - else if (task) { - if (human_player_name) { - console.log(`Teleporting ${name} to human ${human_player_name}`) - bot.chat(`/tp ${name} ${human_player_name}`) // teleport on top of the human player - - } - await new Promise((resolve) => setTimeout(resolve, 200)); } + await new Promise((resolve) => setTimeout(resolve, 200)); // now all bots are teleport on top of each other (which kinda looks ugly) // Thus, we need to teleport them to random distances to make it look better @@ -112,12 +97,13 @@ export async function initBotTask(agent) { await new Promise((resolve) => setTimeout(resolve, 200)); } - if (task.agent_names) { + if (task.agent_count && task.agent_count > 1) { await new Promise((resolve) => setTimeout(resolve, 10000)); - if (task.agent_names.filter(name => !bot.players[name]).length) { - console.log(`Missing players/bots: ${missingPlayers.join(', ')}`); + if (available_agents.length < task.agent_count) { + console.log(`Missing ${task.agent_count - available_agents.length} bot(s).`); agent.cleanKill('Not all required players/bots are present in the world. Exiting.', 4); } + } if (task.goal) { @@ -125,7 +111,7 @@ export async function initBotTask(agent) { } if (task.conversation && agent.count_id === 0) { - let other_name = task.agent_names.filter(n => n !== name)[0]; + let other_name = available_agents.filter(n => n !== name)[0]; await executeCommand(agent, `!startConversation("${other_name}", "${task.conversation}")`); } } From d0140aa5422ec140901bd302f22696bd5e11748e Mon Sep 17 00:00:00 2001 From: Kolby Nottingham Date: Tue, 10 Dec 2024 15:39:57 -0800 Subject: [PATCH 08/10] tasks class --- evaluate.sh | 67 ---------------- main.js | 3 +- src/agent/agent.js | 66 +++------------- src/agent/tasks.js | 192 +++++++++++++++++++++++++++++++++++++++++++++ src/utils/tasks.js | 149 ----------------------------------- 5 files changed, 204 insertions(+), 273 deletions(-) delete mode 100755 evaluate.sh create mode 100644 src/agent/tasks.js delete mode 100644 src/utils/tasks.js diff --git a/evaluate.sh b/evaluate.sh deleted file mode 100755 index b1cd875..0000000 --- a/evaluate.sh +++ /dev/null @@ -1,67 +0,0 @@ -#!/bin/bash - -# Initialize variables -args=() -num_experiments=0 -successful=0 -unsuccessful=0 -error=0 - -# Parse arguments -while [[ $# -gt 0 ]]; do - case "$1" in - --num_experiments) - num_experiments="$2" - shift 2 - ;; - *) - args+=("$1") - shift - ;; - esac -done - -# Validate num_experiments -if ! [[ "$num_experiments" =~ ^[0-9]+$ ]] || [[ "$num_experiments" -eq 0 ]]; then - echo "Error: num_experiments must be a positive integer" - echo "Usage: $0 --num_experiments [other arguments]" - exit 1 -fi - -# Run experiments -while (( successful + unsuccessful < num_experiments )); do - node main.js "${args[@]}" - exit_code=$? - - case $exit_code in - 2) ((successful++));; - 3) ((unsuccessful++));; - 4) ((error++));; - *) echo "Unknown exit code: $exit_code";; - esac - - # Calculate success percentage - if [[ $successful -eq 0 && $unsuccessful -eq 0 ]]; then - success_percentage=0 - else - success_percentage=$(echo "scale=2; $successful / ($successful + $unsuccessful) * 100" | bc) - fi - - echo "Success percentage: $success_percentage%" - echo "Total successful: $successful" - echo "Total unsuccessful: $unsuccessful" - echo "Total errors: $error" - echo "Total experiments run: $((successful + unsuccessful))" -done - -# Generate output file with a cleaner name format -date_time=$(date +'%Y-%m-%d_%H-%M-%S') -output_file="${date_time}_results.txt" - -echo "Total experiments: $num_experiments" > "$output_file" -echo "Successful experiments: $successful" >> "$output_file" -echo "Unsuccessful experiments: $unsuccessful" >> "$output_file" -echo "Experiments with errors: $error" >> "$output_file" -echo "Success percentage: $success_percentage%" >> "$output_file" - -echo "Results saved in $output_file" \ No newline at end of file diff --git a/main.js b/main.js index 1054633..9efb4e6 100644 --- a/main.js +++ b/main.js @@ -25,7 +25,6 @@ function parseArguments() { .parse(); } -//todo: modify for multiple agents function getProfiles(args) { return args.profiles || settings.profiles; } @@ -35,7 +34,7 @@ async function main() { const mindServer = createMindServer(); } mainProxy.connect(); - + const args = parseArguments(); const profiles = getProfiles(args); console.log(profiles); diff --git a/src/agent/agent.js b/src/agent/agent.js index 805f560..7b14f3c 100644 --- a/src/agent/agent.js +++ b/src/agent/agent.js @@ -13,7 +13,7 @@ import { handleTranslation, handleEnglishTranslation } from '../utils/translator import { addViewer } from './viewer.js'; import settings from '../../settings.js'; import { serverProxy } from './agent_proxy.js'; -import { loadTask, initBotTask, TechTreeHarvestValidator } from '../utils/tasks.js'; +import { Task } from './tasks.js'; export class Agent { async start(profile_fp, load_mem=false, init_message=null, count_id=0, task_path=null, task_id=null) { @@ -45,6 +45,9 @@ export class Agent { convoManager.initAgent(this); console.log('Initializing examples...'); await this.prompter.initExamples(); + console.log('Initializing task...'); + this.task = new Task(this, task_path, task_id); + this.blocked_actions = this.task.blocked_actions || []; serverProxy.connect(this); @@ -58,24 +61,6 @@ export class Agent { save_data = this.history.load(); } - // Load task if provided - if (task_path) { - this.task = loadTask(task_path, task_id); - this.taskTimeout = this.task.timeout || 300; - this.taskStartTime = Date.now(); - this.validator = new TechTreeHarvestValidator(this.task, this.bot); - this.blocked_actions = this.task.blocked_actions || []; - if (this.task.goal) - this.blocked_actions.push('!endGoal'); - if (this.task.conversation) - this.blocked_actions.push('!endConversation'); - } else { - this.task = null; - this.taskTimeout = null; - this.validator = null; - this.blocked_actions = []; - } - this.bot.on('login', () => { console.log(this.name, 'logged in!'); @@ -105,8 +90,7 @@ export class Agent { this._setupEventHandlers(save_data, init_message); this.startEvents(); - if (this.task) - initBotTask(this); + this.task.initBotTask(); } catch (error) { console.error('Error in spawn event:', error); @@ -376,6 +360,7 @@ export class Agent { } startEvents() { + // Custom events this.bot.on('time', () => { if (this.bot.time.timeOfDay == 0) this.bot.emit('sunrise'); @@ -454,46 +439,17 @@ export class Agent { this.bot.emit('idle'); // Check for task completion - if (this.task) { + if (this.task.data) { setInterval(() => { - if (this.validator && this.validator.validate()) - this.killBots(); - // TODO check for other terminal conditions - // if (this.task.goal && !this.self_prompter.on) - // this.cleanKill('Agent ended goal', 3); - // if (this.task.conversation && !inConversation()) - // this.cleanKill('Agent ended conversation', 3); - if (this.taskTimeout) { - const elapsedTime = (Date.now() - this.taskStartTime) / 1000; - if (elapsedTime >= this.taskTimeout) { - console.log('Task timeout reached. Task unsuccessful.'); - this.cleanKill('Task unsuccessful: Timeout reached', 3); - } + let res = this.task.isDone(); + if (res) { + // TODO kill other bots + this.cleanKill(res.message, res.code); } - }, 1000); } } - async killBots() { - this.bot.chat('Task completed!'); - this.bot.chat(`/clear @p`); - // Kick other bots - if (this.task && this.task.agent_number) { - const agent_names = this.task.agent_names; - console.log('All agent names:', agent_names); - console.log('My name:', this.name); - const botNames = agent_names.filter(botName => botName !== this.name); - console.log('Kicking bots:', botNames); - botNames.forEach(botName => { - this.bot.chat(`/kick ${botName}`); - console.log(`/kick ${botName}`); - - }); - } - this.cleanKill('Task completed, exiting', 2); - } - async update(delta) { await this.bot.modes.update(); this.self_prompter.update(delta); diff --git a/src/agent/tasks.js b/src/agent/tasks.js new file mode 100644 index 0000000..453125e --- /dev/null +++ b/src/agent/tasks.js @@ -0,0 +1,192 @@ +import { readFileSync } from 'fs'; +import { executeCommand } from './commands/index.js'; +import { getPosition } from './library/world.js' +import settings from '../../settings.js'; + + +export class TaskValidator { + constructor(data, agent) { + this.target = data.target; + this.number_of_target = data.number_of_target; + this.agent = agent; + } + + validate() { + try{ + let valid = false; + let total_targets = 0; + this.agent.bot.inventory.slots.forEach((slot) => { + if (slot && slot.name.toLowerCase() === this.target) { + total_targets += slot.count; + } + if (slot && slot.name.toLowerCase() === this.target && slot.count >= this.number_of_target) { + valid = true; + console.log('Task is complete'); + } + }); + if (total_targets >= this.number_of_target) { + valid = true; + console.log('Task is complete'); + } + return valid; + } catch (error) { + console.error('Error validating task:', error); + return false; + } + } +} + + +export class Task { + constructor(agent, task_path, task_id) { + this.agent = agent; + this.data = null; + this.taskTimeout = 300; + this.taskStartTime = Date.now(); + this.validator = null; + this.blocked_actions = []; + if (task_path && task_id) { + this.data = this.loadTask(task_path, task_id); + this.taskTimeout = this.data.timeout || 300; + this.taskStartTime = Date.now(); + this.validator = new TaskValidator(this.data, this.agent); + this.blocked_actions = this.data.blocked_actions || []; + if (this.data.goal) + this.blocked_actions.push('!endGoal'); + if (this.data.conversation) + this.blocked_actions.push('!endConversation'); + } + } + + loadTask(task_path, task_id) { + try { + const tasksFile = readFileSync(task_path, 'utf8'); + const tasks = JSON.parse(tasksFile); + const task = tasks[task_id]; + if (!task) { + throw new Error(`Task ${task_id} not found`); + } + + return task; + } catch (error) { + console.error('Error loading task:', error); + process.exit(1); + } + } + + isDone() { + if (this.validator && this.validator.validate()) + return {"message": 'Task successful', "code": 2}; + // TODO check for other terminal conditions + // if (this.task.goal && !this.self_prompter.on) + // return {"message": 'Agent ended goal', "code": 3}; + // if (this.task.conversation && !inConversation()) + // return {"message": 'Agent ended conversation', "code": 3}; + if (this.taskTimeout) { + const elapsedTime = (Date.now() - this.taskStartTime) / 1000; + if (elapsedTime >= this.taskTimeout) { + console.log('Task timeout reached. Task unsuccessful.'); + return {"message": 'Task timeout reached', "code": 4}; + } + } + return false; + } + + async initBotTask() { + if (this.data === null) + return; + let bot = this.agent.bot; + let name = this.agent.name; + + bot.chat(`/clear ${name}`); + console.log(`Cleared ${name}'s inventory.`); + + //wait for a bit so inventory is cleared + await new Promise((resolve) => setTimeout(resolve, 500)); + + if (this.data.agent_number > 1) { + var initial_inventory = this.data.initial_inventory[this.agent.count_id.toString()]; + console.log("Initial inventory:", initial_inventory); + } else if (this.data) { + console.log("Initial inventory:", this.data.initial_inventory); + var initial_inventory = this.data.initial_inventory; + } + + if ("initial_inventory" in this.data) { + console.log("Setting inventory..."); + console.log("Inventory to set:", initial_inventory); + for (let key of Object.keys(initial_inventory)) { + console.log('Giving item:', key); + bot.chat(`/give ${name} ${key} ${initial_inventory[key]}`); + }; + //wait for a bit so inventory is set + await new Promise((resolve) => setTimeout(resolve, 500)); + console.log("Done giving inventory items."); + } + // Function to generate random numbers + + function getRandomOffset(range) { + return Math.floor(Math.random() * (range * 2 + 1)) - range; + } + + let human_player_name = null; + let available_agents = settings.profiles.map((p) => JSON.parse(readFileSync(p, 'utf8')).name); // TODO this does not work with command line args + + // Finding if there is a human player on the server + for (const playerName in bot.players) { + const player = bot.players[playerName]; + if (!available_agents.some((n) => n === name)) { + console.log('Found human player:', player.username); + human_player_name = player.username + break; + } + } + + // If there are multiple human players, teleport to the first one + + // teleport near a human player if found by default + + if (human_player_name) { + console.log(`Teleporting ${name} to human ${human_player_name}`) + bot.chat(`/tp ${name} ${human_player_name}`) // teleport on top of the human player + + } + await new Promise((resolve) => setTimeout(resolve, 200)); + + // now all bots are teleport on top of each other (which kinda looks ugly) + // Thus, we need to teleport them to random distances to make it look better + + /* + Note : We don't want randomness for construction task as the reference point matters a lot. + Another reason for no randomness for construction task is because, often times the user would fly in the air, + then set a random block to dirt and teleport the bot to stand on that block for starting the construction, + This was done by MaxRobinson in one of the youtube videos. + */ + + if (this.data.type !== 'construction') { + const pos = getPosition(bot); + const xOffset = getRandomOffset(5); + const zOffset = getRandomOffset(5); + bot.chat(`/tp ${name} ${Math.floor(pos.x + xOffset)} ${pos.y + 3} ${Math.floor(pos.z + zOffset)}`); + await new Promise((resolve) => setTimeout(resolve, 200)); + } + + if (this.data.agent_count && this.data.agent_count > 1) { + await new Promise((resolve) => setTimeout(resolve, 10000)); + if (available_agents.length < this.data.agent_count) { + console.log(`Missing ${this.data.agent_count - available_agents.length} bot(s).`); + this.agent.cleanKill('Not all required players/bots are present in the world. Exiting.', 4); + } + + } + + if (this.data.goal) { + await executeCommand(this.agent, `!goal("${this.data.goal}")`); + } + + if (this.data.conversation && this.agent.count_id === 0) { + let other_name = available_agents.filter(n => n !== name)[0]; + await executeCommand(this.agent, `!startConversation("${other_name}", "${this.data.conversation}")`); + } + } +} diff --git a/src/utils/tasks.js b/src/utils/tasks.js deleted file mode 100644 index 0c6c470..0000000 --- a/src/utils/tasks.js +++ /dev/null @@ -1,149 +0,0 @@ -import { readFileSync } from 'fs'; -import { executeCommand } from '../agent/commands/index.js'; -import { getPosition } from '../agent/library/world.js' -import settings from '../../settings.js'; - -export function loadTask(task_path, task_id) { - try { - const tasksFile = readFileSync(task_path, 'utf8'); - const tasks = JSON.parse(tasksFile); - const task = tasks[task_id]; - if (!task) { - throw new Error(`Task ${task_id} not found`); - } - - return task; - } catch (error) { - console.error('Error loading task:', error); - process.exit(1); - } -} - -export async function initBotTask(agent) { - let bot = agent.bot; - let task = agent.task; - let name = bot.username; - - bot.chat(`/clear ${name}`); - console.log(`Cleared ${name}'s inventory.`); - - //wait for a bit so inventory is cleared - await new Promise((resolve) => setTimeout(resolve, 500)); - - if (task.agent_number > 1) { - var initial_inventory = task.initial_inventory[agent.count_id.toString()]; - console.log("Initial inventory:", initial_inventory); - } else if (task) { - console.log("Initial inventory:", task.initial_inventory); - var initial_inventory = task.initial_inventory; - } - - if ("initial_inventory" in task) { - console.log("Setting inventory..."); - console.log("Inventory to set:", initial_inventory); - for (let key of Object.keys(initial_inventory)) { - console.log('Giving item:', key); - bot.chat(`/give ${name} ${key} ${initial_inventory[key]}`); - }; - //wait for a bit so inventory is set - await new Promise((resolve) => setTimeout(resolve, 500)); - console.log("Done giving inventory items."); - } - // Function to generate random numbers - - function getRandomOffset(range) { - return Math.floor(Math.random() * (range * 2 + 1)) - range; - } - - let human_player_name = null; - let available_agents = settings.profiles.map((p) => JSON.parse(readFileSync(p, 'utf8')).name); - - // Finding if there is a human player on the server - for (const playerName in bot.players) { - const player = bot.players[playerName]; - if (!available_agents.some((n) => n === name)) { - console.log('Found human player:', player.username); - human_player_name = player.username - break; - } - } - - // If there are multiple human players, teleport to the first one - - // teleport near a human player if found by default - - if (human_player_name) { - console.log(`Teleporting ${name} to human ${human_player_name}`) - bot.chat(`/tp ${name} ${human_player_name}`) // teleport on top of the human player - - } - await new Promise((resolve) => setTimeout(resolve, 200)); - - // now all bots are teleport on top of each other (which kinda looks ugly) - // Thus, we need to teleport them to random distances to make it look better - - /* - Note : We don't want randomness for construction task as the reference point matters a lot. - Another reason for no randomness for construction task is because, often times the user would fly in the air, - then set a random block to dirt and teleport the bot to stand on that block for starting the construction, - This was done by MaxRobinson in one of the youtube videos. - */ - - if (task.type !== 'construction') { - const pos = getPosition(bot); - const xOffset = getRandomOffset(5); - const zOffset = getRandomOffset(5); - bot.chat(`/tp ${name} ${Math.floor(pos.x + xOffset)} ${pos.y + 3} ${Math.floor(pos.z + zOffset)}`); - await new Promise((resolve) => setTimeout(resolve, 200)); - } - - if (task.agent_count && task.agent_count > 1) { - await new Promise((resolve) => setTimeout(resolve, 10000)); - if (available_agents.length < task.agent_count) { - console.log(`Missing ${task.agent_count - available_agents.length} bot(s).`); - agent.cleanKill('Not all required players/bots are present in the world. Exiting.', 4); - } - - } - - if (task.goal) { - await executeCommand(agent, `!goal("${task.goal}")`); - } - - if (task.conversation && agent.count_id === 0) { - let other_name = available_agents.filter(n => n !== name)[0]; - await executeCommand(agent, `!startConversation("${other_name}", "${task.conversation}")`); - } -} - -export class TechTreeHarvestValidator { - constructor(task, bot) { - this.target = task.target; - this.number_of_target = task.number_of_target; - this.bot = bot; - } - - validate() { - try{ - let valid = false; - let total_targets = 0; - this.bot.inventory.slots.forEach((slot) => { - if (slot && slot.name.toLowerCase() === this.target) { - total_targets += slot.count; - } - if (slot && slot.name.toLowerCase() === this.target && slot.count >= this.number_of_target) { - valid = true; - console.log('Task is complete'); - } - }); - if (total_targets >= this.number_of_target) { - valid = true; - console.log('Task is complete'); - } - return valid; - } catch (error) { - console.error('Error validating task:', error); - return false; - } - } -} From 58d38af36962223c458e716afb4f30c9d6b3e7ba Mon Sep 17 00:00:00 2001 From: Kolby Nottingham Date: Thu, 12 Dec 2024 12:33:00 -0800 Subject: [PATCH 09/10] task init fixes --- src/agent/tasks.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/agent/tasks.js b/src/agent/tasks.js index 453125e..9dfa232 100644 --- a/src/agent/tasks.js +++ b/src/agent/tasks.js @@ -66,7 +66,10 @@ export class Task { if (!task) { throw new Error(`Task ${task_id} not found`); } - + if ((!task.agent_count || task.agent_count <= 1) && this.agent.count_id > 0) { + task = null; + } + return task; } catch (error) { console.error('Error loading task:', error); @@ -104,7 +107,7 @@ export class Task { //wait for a bit so inventory is cleared await new Promise((resolve) => setTimeout(resolve, 500)); - if (this.data.agent_number > 1) { + if (this.data.agent_count > 1) { var initial_inventory = this.data.initial_inventory[this.agent.count_id.toString()]; console.log("Initial inventory:", initial_inventory); } else if (this.data) { @@ -135,13 +138,13 @@ export class Task { // Finding if there is a human player on the server for (const playerName in bot.players) { const player = bot.players[playerName]; - if (!available_agents.some((n) => n === name)) { + if (!available_agents.some((n) => n === playerName)) { console.log('Found human player:', player.username); human_player_name = player.username break; } - } - + } + // If there are multiple human players, teleport to the first one // teleport near a human player if found by default From 883f73eef5bcdebd6c3fc43eb67fd0752afcdb61 Mon Sep 17 00:00:00 2001 From: Kolby Nottingham Date: Thu, 12 Dec 2024 18:53:24 -0800 Subject: [PATCH 10/10] fixed example --- example_tasks.json | 1 - src/agent/tasks.js | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/example_tasks.json b/example_tasks.json index d5305ba..b579233 100644 --- a/example_tasks.json +++ b/example_tasks.json @@ -1,7 +1,6 @@ { "debug_single_agent": { "goal": "Just stand at a place and don't do anything", - "guidance": null, "initial_inventory": {}, "type": "debug" }, diff --git a/src/agent/tasks.js b/src/agent/tasks.js index 9dfa232..881b933 100644 --- a/src/agent/tasks.js +++ b/src/agent/tasks.js @@ -173,16 +173,16 @@ export class Task { bot.chat(`/tp ${name} ${Math.floor(pos.x + xOffset)} ${pos.y + 3} ${Math.floor(pos.z + zOffset)}`); await new Promise((resolve) => setTimeout(resolve, 200)); } - + if (this.data.agent_count && this.data.agent_count > 1) { + // TODO wait for other bots to join await new Promise((resolve) => setTimeout(resolve, 10000)); if (available_agents.length < this.data.agent_count) { console.log(`Missing ${this.data.agent_count - available_agents.length} bot(s).`); this.agent.cleanKill('Not all required players/bots are present in the world. Exiting.', 4); } - } - + if (this.data.goal) { await executeCommand(this.agent, `!goal("${this.data.goal}")`); }