diff --git a/.gitignore b/.gitignore index f7aae85..633ddad 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ .vscode/ node_modules/ package-lock.json -temp.js scratch.js -agent_code/** -!agent_code/template.js \ No newline at end of file +!agent_code/template.js +bots/**/action-code/** +bots/**/save.json \ No newline at end of file diff --git a/bots/assist.json b/bots/andy/assist.json similarity index 100% rename from bots/assist.json rename to bots/andy/assist.json diff --git a/agent_code/template.js b/bots/template.js similarity index 55% rename from agent_code/template.js rename to bots/template.js index 52d0544..1e61d60 100644 --- a/agent_code/template.js +++ b/bots/template.js @@ -1,5 +1,5 @@ -import * as skills from '../utils/skills.js'; -import * as world from '../utils/world.js'; +import * as skills from '../../../src/agent/skills.js'; +import * as world from '../../../src/agent/world.js'; import Vec3 from 'vec3'; const log = skills.log; diff --git a/controller/init-agent.js b/controller/init-agent.js deleted file mode 100644 index 96d9381..0000000 --- a/controller/init-agent.js +++ /dev/null @@ -1,32 +0,0 @@ -import { Agent } from '../agent.js'; -import yargs from 'yargs'; - -const args = process.argv.slice(2); -if (args.length < 1) { - console.log('Usage: node init_agent.js [-c] [-a]'); - process.exit(1); -} - -const argv = yargs(args) - .option('profile', { - alias: 'p', - type: 'string', - description: 'profile to use for agent' - }) - .option('clear_memory', { - alias: 'c', - type: 'boolean', - description: 'restart memory from scratch' - }) - .option('autostart', { - alias: 'a', - type: 'boolean', - description: 'automatically prompt the agent on startup' - }).argv - -const name = argv._[0]; -const save_path = './bots/'+name+'.json'; -const load_path = !!argv.clear_memory ? './bots/'+argv.profile+'.json' : save_path; -const init_message = !!argv.autostart ? 'Agent process restarted. Notify the user and decide what to do.' : null; - -new Agent(name, save_path, load_path, init_message); diff --git a/main.js b/main.js index ea78372..6d6e133 100644 --- a/main.js +++ b/main.js @@ -1,3 +1,3 @@ -import { AgentProcess } from './controller/agent-process.js'; +import { AgentProcess } from './src/process/agent-process.js'; -new AgentProcess('andy').start(true, false); \ No newline at end of file +new AgentProcess('andy').start('assist'); \ No newline at end of file diff --git a/agent.js b/src/agent/agent.js similarity index 73% rename from agent.js rename to src/agent/agent.js index a6beffd..21309de 100644 --- a/agent.js +++ b/src/agent/agent.js @@ -1,37 +1,47 @@ -import { initBot } from './utils/mcdata.js'; -import { sendRequest } from './utils/gpt.js'; -import { History } from './utils/history.js'; -import { Coder } from './utils/coder.js'; -import { getQuery, containsQuery } from './utils/queries.js'; -import { containsCodeBlock } from './utils/skill-library.js'; -import { Events } from './utils/events.js'; +import { initBot } from '../utils/mcdata.js'; +import { sendRequest } from '../utils/gpt.js'; +import { History } from './history.js'; +import { Coder } from './coder.js'; +import { getQuery, containsQuery } from './queries.js'; +import { containsCodeBlock } from './skill-library.js'; +import { Events } from './events.js'; export class Agent { - constructor(name, save_path, load_path=null, init_message=null) { + constructor(name, profile=null, init_message=null) { this.name = name; this.bot = initBot(name); - this.history = new History(this, save_path); - this.history.loadExamples(); + this.history = new History(this); this.coder = new Coder(this); - if (load_path) { - this.history.load(load_path); - } + this.history.load(profile); this.events = new Events(this, this.history.events) - this.bot.on('login', () => { + this.bot.on('login', async () => { this.bot.chat('Hello world! I am ' + this.name); console.log(`${this.name} logged in.`); - + + const ignore_messages = [ + "Set own game mode to", + "Set the time to", + "Set the difficulty to", + "Teleported ", + "Set the weather to", + "Gamerule " + ]; this.bot.on('chat', (username, message) => { if (username === this.name) return; + + if (ignore_messages.some((m) => message.startsWith(m))) return; + console.log('received message from', username, ':', message); this.handleMessage(username, message); }); + await this.history.loadExamples(); + if (init_message) { this.handleMessage('system', init_message); } else { @@ -41,7 +51,8 @@ export class Agent { } async handleMessage(source, message) { - await this.history.add(source, message); + if (!!source && !!message) + await this.history.add(source, message); for (let i=0; i<5; i++) { let res = await sendRequest(this.history.getHistory(), this.history.getSystemMessage()); diff --git a/utils/coder.js b/src/agent/coder.js similarity index 93% rename from utils/coder.js rename to src/agent/coder.js index 0b6e405..23f3026 100644 --- a/utils/coder.js +++ b/src/agent/coder.js @@ -1,4 +1,4 @@ -import { writeFile, readFile, unlink } from 'fs'; +import { writeFile, readFile, unlink, mkdirSync } from 'fs'; export class Coder { constructor(agent) { @@ -6,17 +6,19 @@ export class Coder { this.queued_code = ''; this.current_code = ''; this.file_counter = 0; - this.fp = './agent_code/'; + this.fp = '/bots/'+agent.name+'/action-code/'; this.agent.bot.interrupt_code = false; this.executing = false; this.agent.bot.output = ''; this.code_template = ''; this.timedout = false; - readFile(this.fp+'template.js', 'utf8', (err, data) => { + readFile('./bots/template.js', 'utf8', (err, data) => { if (err) throw err; this.code_template = data; }); + + mkdirSync('.' + this.fp, { recursive: true }); } queueCode(code) { @@ -67,7 +69,7 @@ export class Coder { console.log("writing to file...", src) - let filename = this.fp + this.file_counter + '.js'; + let filename = this.file_counter + '.js'; // if (this.file_counter > 0) { // let prev_filename = this.fp + (this.file_counter-1) + '.js'; // unlink(prev_filename, (err) => { @@ -77,7 +79,7 @@ export class Coder { // } commented for now, useful to keep files for debugging this.file_counter++; - let write_result = await this.writeFilePromise(filename, src); + let write_result = await this.writeFilePromise('.' + this.fp + filename, src) if (write_result) { console.error('Error writing code execution file: ' + result); @@ -86,7 +88,7 @@ export class Coder { let TIMEOUT; try { console.log('executing code...\n'); - let execution_file = await import('.'+filename); + let execution_file = await import('../..' + this.fp + filename); await this.stop(); this.current_code = this.queued_code; diff --git a/utils/events.js b/src/agent/events.js similarity index 100% rename from utils/events.js rename to src/agent/events.js diff --git a/utils/history.js b/src/agent/history.js similarity index 89% rename from utils/history.js rename to src/agent/history.js index 93d46be..6f1cfd0 100644 --- a/utils/history.js +++ b/src/agent/history.js @@ -1,13 +1,13 @@ import { writeFileSync, readFileSync, mkdirSync } from 'fs'; import { getQueryDocs } from './queries.js'; import { getSkillDocs } from './skill-library.js'; -import { sendRequest, embed, cosineSimilarity } from './gpt.js'; +import { sendRequest, embed, cosineSimilarity } from '../utils/gpt.js'; export class History { - constructor(agent, save_path) { + constructor(agent) { this.name = agent.name; - this.save_path = save_path; + this.save_path = `./bots/${this.name}/save.json`; this.turns = []; // These define an agent's long term memory @@ -86,7 +86,7 @@ export class History { async loadExamples() { let examples = []; try { - const data = readFileSync('utils/examples.json', 'utf8'); + const data = readFileSync('./src/examples.json', 'utf8'); examples = JSON.parse(data); } catch (err) { console.log('No history examples found.'); @@ -148,12 +148,9 @@ export class History { await this.setExamples(); } - save(save_path=null) { - if (save_path == null) - save_path = this.save_path; - if (save_path === '' || save_path == null) return; + save() { // save history object to json file - mkdirSync('bots', { recursive: true }); + mkdirSync(`./bots/${this.name}`, { recursive: true }); let data = { 'name': this.name, 'bio': this.bio, @@ -162,7 +159,7 @@ export class History { 'turns': this.turns }; const json_data = JSON.stringify(data, null, 4); - writeFileSync(save_path, json_data, (err) => { + writeFileSync(this.save_path, json_data, (err) => { if (err) { throw err; } @@ -170,10 +167,8 @@ export class History { }); } - load(load_path=null) { - if (load_path == null) - load_path = this.save_path; - if (load_path === '' || load_path == null) return; + load(profile) { + const load_path = profile? `./bots/${this.name}/${profile}.json` : this.save_path; try { // load history object from json file const data = readFileSync(load_path, 'utf8'); @@ -183,8 +178,7 @@ export class History { this.events = obj.events; this.turns = obj.turns; } catch (err) { - console.log('No history file found for ' + this.name + '.'); - console.log(load_path); + console.error(`No file for profile '${load_path}' for agent ${this.name}.`); } } } \ No newline at end of file diff --git a/src/agent/queries.js b/src/agent/queries.js new file mode 100644 index 0000000..d1408b9 --- /dev/null +++ b/src/agent/queries.js @@ -0,0 +1,131 @@ +import { getNearestBlock, getNearbyMobTypes, getNearbyPlayerNames, getNearbyBlockTypes, getInventoryCounts } from './world.js'; +import { getAllItems } from '../utils/mcdata.js'; + + +const pad = (str) => { + return '\n' + str + '\n'; +} + +const queryList = [ + { + name: "!stats", + description: "Get your bot's stats", + perform: function (agent) { + let bot = agent.bot; + let res = 'STATS'; + res += `\n- position: x:${bot.entity.position.x}, y:${bot.entity.position.y}, z:${bot.entity.position.z}`; + res += `\n- health: ${bot.health} / 20`; + if (bot.time.timeOfDay < 6000) { + res += '\n- time: Morning'; + } else if (bot.time.timeOfDay < 12000) { + res += '\n- time: Afternoon'; + } else { + res += '\n- time: Night'; + } + return pad(res); + } + }, + { + name: "!inventory", + description: "Get your bot's inventory.", + perform: function (agent) { + let bot = agent.bot; + let inventory = getInventoryCounts(bot); + let res = 'INVENTORY'; + for (const item in inventory) { + if (inventory[item] && inventory[item] > 0) + res += `\n- ${item}: ${inventory[item]}`; + } + if (res == 'INVENTORY') { + res += ': none'; + } + return pad(res); + } + }, + { + name: "!blocks", + description: "Get the blocks near the bot.", + perform: function (agent) { + let bot = agent.bot; + let res = 'NEARBY_BLOCKS'; + let blocks = getNearbyBlockTypes(bot); + for (let i = 0; i < blocks.length; i++) { + res += `\n- ${blocks[i]}`; + } + if (blocks.length == 0) { + res += ': none'; + } + return pad(res); + } + }, + { + name: "!craftable", + description: "Get the craftable items with the bot's inventory.", + perform: function (agent) { + const bot = agent.bot; + const table = getNearestBlock(bot, 'crafting_table'); + let res = 'CRAFTABLE_ITEMS'; + for (const item of getAllItems()) { + let recipes = bot.recipesFor(item.id, null, 1, table); + if (recipes.length > 0) { + res += `\n- ${item.name}`; + } + } + if (res == 'CRAFTABLE_ITEMS') { + res += ': none'; + } + return pad(res); + } + }, + { + name: "!entities", + description: "Get the nearby players and entities.", + perform: function (agent) { + let bot = agent.bot; + let res = 'NEARBY_ENTITIES'; + for (const entity of getNearbyPlayerNames(bot)) { + res += `\n- player: ${entity}`; + } + for (const entity of getNearbyMobTypes(bot)) { + res += `\n- mob: ${entity}`; + } + if (res == 'NEARBY_ENTITIES') { + res += ': none'; + } + return pad(res); + } + }, + { + name: "!action", + description: "Get the currently executing code.", + perform: function (agent) { + return pad("Current code:\n`" + agent.coder.current_code +"`"); + } + } +]; + +const queryMap = {}; +for (let query of queryList) { + queryMap[query.name] = query; +} + +export function getQuery(name) { + return queryMap[name]; +} + +export function containsQuery(message) { + for (let query of queryList) { + if (message.includes(query.name)) { + return query.name; + } + } + return null; +} + +export function getQueryDocs() { + let docs = `\n*QUERY DOCS\n You can use the following commands to query for information about the world. Use the query name in your response and the next input will have the requested information.\n`; + for (let query of queryList) { + docs += query.name + ': ' + query.description + '\n'; + } + return docs + '*\n'; +} \ No newline at end of file diff --git a/utils/skill-library.js b/src/agent/skill-library.js similarity index 100% rename from utils/skill-library.js rename to src/agent/skill-library.js diff --git a/utils/skills.js b/src/agent/skills.js similarity index 99% rename from utils/skills.js rename to src/agent/skills.js index 2b119bc..1c9396e 100644 --- a/utils/skills.js +++ b/src/agent/skills.js @@ -1,4 +1,4 @@ -import { getItemId, getItemName } from "./mcdata.js"; +import { getItemId, getItemName } from "../utils/mcdata.js"; import { getNearestBlocks, getNearestBlock, getInventoryCounts, getInventoryStacks, getNearbyMobs, getNearbyBlocks } from "./world.js"; import pf from 'mineflayer-pathfinder'; import Vec3 from 'vec3'; diff --git a/utils/world.js b/src/agent/world.js similarity index 99% rename from utils/world.js rename to src/agent/world.js index 9cf6749..131e36d 100644 --- a/utils/world.js +++ b/src/agent/world.js @@ -1,4 +1,4 @@ -import { getAllBlockIds } from './mcdata.js'; +import { getAllBlockIds } from '../utils/mcdata.js'; export function getNearestBlocks(bot, block_types, distance=16, count=1) { diff --git a/utils/examples.json b/src/examples.json similarity index 100% rename from utils/examples.json rename to src/examples.json diff --git a/controller/agent-process.js b/src/process/agent-process.js similarity index 75% rename from controller/agent-process.js rename to src/process/agent-process.js index 929317e..c81de04 100644 --- a/controller/agent-process.js +++ b/src/process/agent-process.js @@ -4,13 +4,12 @@ export class AgentProcess { constructor(name) { this.name = name; } - start(clear_memory=false, autostart=false, profile='assist') { - let args = ['controller/init-agent.js', this.name]; - args.push('-p', profile); - if (clear_memory) - args.push('-c'); - if (autostart) - args.push('-a'); + start(profile='assist', init_message=null) { + let args = ['src/process/init-agent.js', this.name]; + if (profile) + args.push('-p', profile); + if (init_message) + args.push('-m', init_message); const agentProcess = spawn('node', args, { stdio: 'inherit', @@ -28,7 +27,7 @@ export class AgentProcess { process.exit(1); } console.log('Restarting agent...'); - this.start(false, true); + this.start('save', 'Agent process restarted. Notify the user and decide what to do.'); last_restart = Date.now(); } }); diff --git a/src/process/init-agent.js b/src/process/init-agent.js new file mode 100644 index 0000000..584f1bb --- /dev/null +++ b/src/process/init-agent.js @@ -0,0 +1,23 @@ +import { Agent } from '../agent/agent.js'; +import yargs from 'yargs'; + +const args = process.argv.slice(2); +if (args.length < 1) { + console.log('Usage: node init_agent.js [profile] [init_message]'); + process.exit(1); +} + +const argv = yargs(args) + .option('profile', { + alias: 'p', + type: 'string', + description: 'profile to use for agent' + }) + .option('init_message', { + alias: 'm', + type: 'string', + description: 'automatically prompt the agent on startup' + }).argv + +const name = args[0]; +new Agent(name, argv.profile, argv.init_message); diff --git a/utils/gpt.js b/src/utils/gpt.js similarity index 100% rename from utils/gpt.js rename to src/utils/gpt.js diff --git a/utils/mcdata.js b/src/utils/mcdata.js similarity index 100% rename from utils/mcdata.js rename to src/utils/mcdata.js diff --git a/utils/context.js b/utils/context.js deleted file mode 100644 index 3518b10..0000000 --- a/utils/context.js +++ /dev/null @@ -1,111 +0,0 @@ -import { readFileSync } from 'fs'; - -import { getNearestBlock, getNearbyMobTypes, getNearbyPlayerNames, getNearbyBlockTypes, getInventoryCounts } from './world.js'; -import { getAllItems } from './mcdata.js'; - - -export function getStats(bot) { - let res = 'STATS'; - res += `\n- position: x:${bot.entity.position.x}, y:${bot.entity.position.y}, z:${bot.entity.position.z}`; - res += `\n- health: ${bot.health} / 20`; - if (bot.time.timeOfDay < 6000) { - res += '\n- time: Morning'; - } else if (bot.time.timeOfDay < 12000) { - res += '\n- time: Afternoon'; - } else { - res += '\n- time: Night'; - } - return res; -} - - -export function getInventory(bot) { - let inventory = getInventoryCounts(bot); - let res = 'INVENTORY'; - for (const item in inventory) { - if (inventory[item] && inventory[item] > 0) - res += `\n- ${item}: ${inventory[item]}`; - } - if (res == 'INVENTORY') { - res += ': none'; - } - return res; -} - - -export function getBlocks(bot) { - let res = 'NEARBY_BLOCKS'; - let blocks = getNearbyBlockTypes(bot); - for (let i = 0; i < blocks.length; i++) { - res += `\n- ${blocks[i]}`; - } - if (blocks.length == 0) { - res += ': none'; - } - return res; -} - - -export function getNearbyEntities(bot) { - let res = 'NEARBY_ENTITIES'; - for (const entity of getNearbyPlayerNames(bot)) { - res += `\n- player: ${entity}`; - } - for (const entity of getNearbyMobTypes(bot)) { - res += `\n- mob: ${entity}`; - } - if (res == 'NEARBY_ENTITIES') { - res += ': none'; - } - return res; -} - - -export function getCraftable(bot) { - const table = getNearestBlock(bot, 'crafting_table'); - let res = 'CRAFTABLE_ITEMS'; - for (const item of getAllItems()) { - let recipes = bot.recipesFor(item.id, null, 1, table); - if (recipes.length > 0) { - res += `\n- ${item.name}`; - } - } - if (res == 'CRAFTABLE_ITEMS') { - res += ': none'; - } - return res; -} - - -export function getDetailedSkills() { - let res = 'namespace skills {'; - let contents = readFileSync("./utils/skills.js", "utf-8").split('\n'); - for (let i = 0; i < contents.length; i++) { - if (contents[i].slice(0, 3) == '/**') { - res += '\t' + contents[i]; - } else if (contents[i].slice(0, 2) == ' *') { - res += '\t' + contents[i]; - } else if (contents[i].slice(0, 4) == ' **/') { - res += '\t' + contents[i] + '\n\n'; - } - } - res = res.trim() + '\n}' - return res; -} - - -export function getWorldFunctions() { - let res = 'namespace world {'; - let contents = readFileSync("./utils/world.js", "utf-8").split('\n'); - for (let i = 0; i < contents.length; i++) { - if (contents[i].slice(0, 3) == '/**') { - res += '\t' + contents[i]; - } else if (contents[i].slice(0, 2) == ' *') { - res += '\t' + contents[i]; - } else if (contents[i].slice(0, 4) == ' **/') { - res += '\t' + contents[i] + '\n\n'; - } - } - res = res.trim() + '\n}' - return res; -} diff --git a/utils/queries.js b/utils/queries.js deleted file mode 100644 index 1820197..0000000 --- a/utils/queries.js +++ /dev/null @@ -1,76 +0,0 @@ -import { getStats, getInventory, getBlocks, getNearbyEntities, getCraftable } from './context.js'; - -const pad = (str) => { - return '\n' + str + '\n'; -} - -const queryList = [ - { - name: "!stats", - description: "Get your bot's stats", - perform: function (agent) { - return pad(getStats(agent.bot)); - } - }, - { - name: "!inventory", - description: "Get your bot's inventory.", - perform: function (agent) { - return pad(getInventory(agent.bot)); - } - }, - { - name: "!blocks", - description: "Get the blocks near the bot.", - perform: function (agent) { - return pad(getBlocks(agent.bot)); - } - }, - { - name: "!craftable", - description: "Get the craftable items with the bot's inventory.", - perform: function (agent) { - return pad(getCraftable(agent.bot)); - } - }, - { - name: "!entities", - description: "Get the nearby players and entities.", - perform: function (agent) { - return pad(getNearbyEntities(agent.bot)); - } - }, - { - name: "!action", - description: "Get the currently executing code.", - perform: function (agent) { - return pad("Current code:\n`" + agent.coder.current_code +"`"); - } - } -]; - -const queryMap = {}; -for (let query of queryList) { - queryMap[query.name] = query; -} - -export function getQuery(name) { - return queryMap[name]; -} - -export function containsQuery(message) { - for (let query of queryList) { - if (message.includes(query.name)) { - return query.name; - } - } - return null; -} - -export function getQueryDocs() { - let docs = `\n*QUERY DOCS\n You can use the following commands to query for information about the world. Use the query name in your response and the next input will have the requested information.\n`; - for (let query of queryList) { - docs += query.name + ': ' + query.description + '\n'; - } - return docs + '*\n'; -} \ No newline at end of file