mirror of
https://github.com/kolbytn/mindcraft.git
synced 2025-03-28 14:56:24 +01:00
commit
2c5dc2b43e
8 changed files with 309 additions and 11 deletions
53
example_tasks.json
Normal file
53
example_tasks.json
Normal file
|
@ -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
|
||||
}
|
||||
}
|
10
main.js
10
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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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)
|
||||
|
|
195
src/agent/tasks.js
Normal file
195
src/agent/tasks.js
Normal file
|
@ -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}")`);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Add table
Reference in a new issue