diff --git a/example_tasks.json b/example_tasks.json new file mode 100644 index 0000000..b579233 --- /dev/null +++ b/example_tasks.json @@ -0,0 +1,53 @@ +{ + "debug_single_agent": { + "goal": "Just stand at a place and don't do anything", + "initial_inventory": {}, + "type": "debug" + }, + "debug_multi_agent": { + "goal": "Just stand at a place and don't do anything", + "agent_count": 2, + "initial_inventory": { + "0": { + "iron_ingot": 1 + }, + "1": { + "iron_ingot": 1 + } + }, + "type": "debug" + }, + "construction": { + "type": "construction", + "goal": "Build a house", + "initial_inventory": { + "oak_planks": 20 + } + }, + "techtree_1_shears_with_2_iron_ingot": { + "goal": "Build a shear.", + "initial_inventory": { + "iron_ingot": 1 + }, + "target": "shears", + "number_of_target": 1, + "type": "techtree", + "timeout": 60 + }, + "multiagent_techtree_1_stone_pickaxe": { + "conversation": "Let's collaborate to build a stone pickaxe", + "agent_count": 2, + "initial_inventory": { + "0": { + "wooden_pickaxe": 1 + }, + "1": { + "wooden_axe": 1 + } + }, + "target": "stone_pickaxe", + "number_of_target": 1, + "type": "techtree", + "timeout": 300 + } +} \ No newline at end of file diff --git a/main.js b/main.js index a344bc8..9efb4e6 100644 --- a/main.js +++ b/main.js @@ -12,6 +12,14 @@ function parseArguments() { type: 'array', describe: 'List of agent profile paths', }) + .option('task_path', { + type: 'string', + describe: 'Path to task file to execute' + }) + .option('task_id', { + type: 'string', + describe: 'Task ID to execute' + }) .help() .alias('help', 'h') .parse(); @@ -37,7 +45,7 @@ async function main() { const profile = readFileSync(profiles[i], 'utf8'); const agent_json = JSON.parse(profile); mainProxy.registerAgent(agent_json.name, agent_process); - agent_process.start(profiles[i], load_memory, init_message, i); + agent_process.start(profiles[i], load_memory, init_message, i, args.task_path, args.task_id); await new Promise(resolve => setTimeout(resolve, 1000)); } } diff --git a/src/agent/agent.js b/src/agent/agent.js index 7c514f0..7b14f3c 100644 --- a/src/agent/agent.js +++ b/src/agent/agent.js @@ -13,10 +13,12 @@ import { handleTranslation, handleEnglishTranslation } from '../utils/translator import { addViewer } from './viewer.js'; import settings from '../../settings.js'; import { serverProxy } from './agent_proxy.js'; +import { Task } from './tasks.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_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'); @@ -43,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); @@ -81,9 +86,12 @@ export class Agent { console.log(`${this.name} spawned.`); this.clearBotLogs(); - + this._setupEventHandlers(save_data, init_message); this.startEvents(); + + this.task.initBotTask(); + } catch (error) { console.error('Error in spawn event:', error); process.exit(0); @@ -429,20 +437,32 @@ export class Agent { }, INTERVAL); this.bot.emit('idle'); + + // Check for task completion + if (this.task.data) { + setInterval(() => { + let res = this.task.isDone(); + if (res) { + // TODO kill other bots + this.cleanKill(res.message, res.code); + } + }, 1000); + } } async update(delta) { await this.bot.modes.update(); - await this.self_prompter.update(delta); + this.self_prompter.update(delta); } 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(code > 1 ? 'Restarting.': 'Exiting.'); this.history.save(); - process.exit(1); + process.exit(code); } } diff --git a/src/agent/commands/index.js b/src/agent/commands/index.js index 76dd117..a8d09db 100644 --- a/src/agent/commands/index.js +++ b/src/agent/commands/index.js @@ -214,7 +214,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. @@ -228,6 +228,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 44d9d51..5c620f0 100644 --- a/src/agent/prompter.js +++ b/src/agent/prompter.js @@ -174,7 +174,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/agent/tasks.js b/src/agent/tasks.js new file mode 100644 index 0000000..881b933 --- /dev/null +++ b/src/agent/tasks.js @@ -0,0 +1,195 @@ +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`); + } + 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); + 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_count > 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 === 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 + + 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) { + // 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}")`); + } + + 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/process/agent_process.js b/src/process/agent_process.js index 83af427..7418d31 100644 --- a/src/process/agent_process.js +++ b/src/process/agent_process.js @@ -2,7 +2,7 @@ import { spawn } from 'child_process'; import { mainProxy } from './main_proxy.js'; export class AgentProcess { - start(profile, load_memory=false, init_message=null, count_id=0) { + start(profile, load_memory=false, init_message=null, count_id=0, task_path=null, task_id=null) { this.profile = profile; this.count_id = count_id; this.running = true; @@ -14,6 +14,10 @@ export class AgentProcess { args.push('-l', load_memory); if (init_message) args.push('-m', init_message); + if (task_path) + args.push('-t', task_path); + if (task_id) + args.push('-i', task_id); const agentProcess = spawn('node', args, { stdio: 'inherit', @@ -26,6 +30,11 @@ export class AgentProcess { this.running = false; mainProxy.logoutAgent(this.name); + if (code > 1) { + console.log(`Ending task`); + process.exit(code); + } + if (code !== 0 && signal !== 'SIGINT') { // agent must run for at least 10 seconds before restarting if (Date.now() - last_restart < 10000) { @@ -33,7 +42,7 @@ 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_path, task_id); last_restart = Date.now(); } }); diff --git a/src/process/init_agent.js b/src/process/init_agent.js index 829f437..88c99b9 100644 --- a/src/process/init_agent.js +++ b/src/process/init_agent.js @@ -33,6 +33,16 @@ const argv = yargs(args) type: 'string', description: 'automatically prompt the agent on startup' }) + .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', { alias: 'c', type: 'number', @@ -45,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); + 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',