From feac9ab424392873a652c7bc1481aa44c5931982 Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Mon, 5 Feb 2024 19:08:08 -0600 Subject: [PATCH 1/4] various improvements --- src/agent/coder.js | 2 +- src/agent/commands/actions.js | 20 +++++++++++++------- src/agent/library/skills.js | 29 +++++++++++++++++------------ src/examples.json | 12 ++++++++---- 4 files changed, 39 insertions(+), 24 deletions(-) diff --git a/src/agent/coder.js b/src/agent/coder.js index aca2f5c..08c7121 100644 --- a/src/agent/coder.js +++ b/src/agent/coder.js @@ -99,7 +99,7 @@ export class Coder { let system_message = "You are a minecraft mineflayer bot that plays minecraft by writing javascript codeblocks. Given the conversation between you and the user, use the provided skills and world functions to write your code in a codeblock. Example response: ``` // your code here ``` You will then be given a response to your code. If you are satisfied with the response, respond without a codeblock in a conversational way. If something went wrong, write another codeblock and try to fix the problem."; system_message += getSkillDocs(); - system_message += "\n\nExamples:\nUser zZZn98: come here \nAssistant: I am going to navigate to zZZn98. ```\nawait skills.goToPlayer(bot, 'zZZn98');```\nSystem: Code execution finished successfully.\nAssistant: Done."; + system_message += "\n\nExamples:\nUser zZZn98: come here \nAssistant: I am going to navigate to zZZn98. ```\nawait skills.goToPlayer(bot, 'zZZn98', 3);```\nSystem: Code execution finished successfully.\nAssistant: Done."; let messages = await agent_history.getHistory(this.examples); diff --git a/src/agent/commands/actions.js b/src/agent/commands/actions.js index d9b88cf..d89b2a7 100644 --- a/src/agent/commands/actions.js +++ b/src/agent/commands/actions.js @@ -73,18 +73,24 @@ export const actionsList = [ }, { name: '!goToPlayer', - description: 'Go to the given player. Ex: !goToPlayer("steve")', - params: {'player_name': '(string) The name of the player to go to.'}, - perform: wrapExecution(async (agent, player_name) => { - return await skills.goToPlayer(agent.bot, player_name); + description: 'Go to the given player. Ex: !goToPlayer("steve", 3)', + params: { + 'player_name': '(string) The name of the player to go to.', + 'closeness': '(number) How close to get to the player.' + }, + perform: wrapExecution(async (agent, player_name, closeness) => { + return await skills.goToPlayer(agent.bot, player_name, closeness); }) }, { name: '!followPlayer', description: 'Endlessly follow the given player. Will defend that player if self_defense mode is on. Ex: !followPlayer("stevie")', - params: {'player_name': '(string) The name of the player to follow.'}, - perform: wrapExecution(async (agent, player_name) => { - await skills.followPlayer(agent.bot, player_name); + params: { + 'player_name': '(string) The name of the player to follow.', + 'follow_dist': '(number) The distance to follow from.' + }, + perform: wrapExecution(async (agent, player_name, follow_dist) => { + await skills.followPlayer(agent.bot, player_name, follow_dist); }, -1, 'followPlayer') }, { diff --git a/src/agent/library/skills.js b/src/agent/library/skills.js index d5abb0b..06892cf 100644 --- a/src/agent/library/skills.js +++ b/src/agent/library/skills.js @@ -27,11 +27,16 @@ async function autoLight(bot) { return false; } -function equipHighestAttack(bot) { - let weapons = bot.inventory.items().filter(item => item.name.includes('sword') || item.name.includes('axe') || item.name.includes('pickaxe') || item.name.includes('shovel')); - let weapon = weapons.sort((a, b) => b.attackDamage - a.attackDamage)[0]; +async function equipHighestAttack(bot) { + let weapons = bot.inventory.items().filter(item => item.name.includes('sword') || (item.name.includes('axe') && !item.name.includes('pickaxe'))); + if (weapons.length === 0) + weapons = bot.inventory.items().filter(item => item.name.includes('pickaxe') || item.name.includes('shovel')); + if (weapons.length === 0) + return; + weapons.sort((a, b) => a.attackDamage < b.attackDamage); + let weapon = weapons[0]; if (weapon) - bot.equip(weapon, 'hand'); + await bot.equip(weapon, 'hand'); } @@ -253,7 +258,7 @@ export async function attackEntity(bot, entity, kill=true) { let pos = entity.position; console.log(bot.entity.position.distanceTo(pos)) - equipHighestAttack(bot) + await equipHighestAttack(bot) if (!kill) { if (bot.entity.position.distanceTo(pos) > 5) { @@ -291,7 +296,7 @@ export async function defendSelf(bot, range=9) { let attacked = false; let enemy = world.getNearestEntityWhere(bot, entity => mc.isHostile(entity), range); while (enemy) { - equipHighestAttack(bot); + await equipHighestAttack(bot); if (bot.entity.position.distanceTo(enemy.position) > 4 && enemy.name !== 'creeper' && enemy.name !== 'phantom') { try { bot.pathfinder.setMovements(new pf.Movements(bot)); @@ -429,7 +434,7 @@ export async function breakBlockAt(bot, x, y, z) { let movements = new pf.Movements(bot); movements.canPlaceOn = false; movements.allow1by1towers = false; - bot.pathfinder.setMovements(); + bot.pathfinder.setMovements(movements); await bot.pathfinder.goto(new pf.goals.GoalNear(pos.x, pos.y, pos.z, 4)); } await bot.dig(block, true); @@ -639,11 +644,12 @@ export async function goToPosition(bot, x, y, z, min_distance=2) { } -export async function goToPlayer(bot, username) { +export async function goToPlayer(bot, username, distance=3) { /** * Navigate to the given player. * @param {MinecraftBot} bot, reference to the minecraft bot. * @param {string} username, the username of the player to navigate to. + * @param {number} distance, the goal distance to the player. * @returns {Promise} true if the player was found, false otherwise. * @example * await skills.goToPlayer(bot, "player"); @@ -656,13 +662,13 @@ export async function goToPlayer(bot, username) { } bot.pathfinder.setMovements(new pf.Movements(bot)); - await bot.pathfinder.goto(new pf.goals.GoalFollow(player, 3), true); + await bot.pathfinder.goto(new pf.goals.GoalFollow(player, distance), true); log(bot, `You have reached ${username}.`); } -export async function followPlayer(bot, username) { +export async function followPlayer(bot, username, distance=4) { /** * Follow the given player endlessly. Will not return until the code is manually stopped. * @param {MinecraftBot} bot, reference to the minecraft bot. @@ -675,9 +681,8 @@ export async function followPlayer(bot, username) { if (!player) return false; - const follow_distance = 4; bot.pathfinder.setMovements(new pf.Movements(bot)); - bot.pathfinder.setGoal(new pf.goals.GoalFollow(player, follow_distance), true); + bot.pathfinder.setGoal(new pf.goals.GoalFollow(player, distance), true); log(bot, `You are now actively following player ${username}.`); while (!bot.interrupt_code) { diff --git a/src/examples.json b/src/examples.json index 07c30be..80a4dbb 100644 --- a/src/examples.json +++ b/src/examples.json @@ -29,9 +29,11 @@ [ {"role": "user", "content": "zZZn98: come here"}, - {"role": "assistant", "content": "On my way! !goToPlayer('zZZn98')"}, - {"role": "system", "content": "Code execution finished successfully."}, - {"role": "assistant", "content": "Here!"} + {"role": "assistant", "content": "On my way! !goToPlayer('zZZn98', 3)"}, + {"role": "system", "content": "Arrived at player."}, + {"role": "assistant", "content": "Here!"}, + {"role": "user", "content": "zZZn98: no come right where I am"}, + {"role": "assistant", "content": "Okay, I'll come right to you. !goToPlayer('zZZn98', 0)"} ], [ @@ -43,7 +45,9 @@ [ {"role": "user", "content": "joe: Follow me!"}, - {"role": "assistant", "content": "Sure! !followPlayer('joe')"} + {"role": "assistant", "content": "Sure! !followPlayer('joe', 4)"}, + {"role": "user", "content": "joe: Not so close!"}, + {"role": "assistant", "content": "Okay, I'll keep my distance. !followPlayer('joe', 3)"} ], [ From 3e3e80c0ed59aada3b7aefe1ee2f0c8a20494bc1 Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Fri, 16 Feb 2024 11:57:48 -0600 Subject: [PATCH 2/4] added various fixes/improvements --- settings.json | 2 +- src/agent/coder.js | 15 ++++++++------- src/agent/commands/actions.js | 6 +++--- src/agent/commands/queries.js | 2 +- src/agent/history.js | 4 ++-- src/agent/library/skills.js | 18 ++++++++++++------ src/examples.json | 13 +++++++++---- src/utils/examples.js | 2 +- src/utils/gpt.js | 23 ++++++++++++++++++----- 9 files changed, 55 insertions(+), 30 deletions(-) diff --git a/settings.json b/settings.json index 8af8c75..bfd07a4 100644 --- a/settings.json +++ b/settings.json @@ -3,5 +3,5 @@ "host": "localhost", "port": 55916, "auth": "offline", - "allow_insecure_coding": true + "allow_insecure_coding": false } \ No newline at end of file diff --git a/src/agent/coder.js b/src/agent/coder.js index 08c7121..b4ca023 100644 --- a/src/agent/coder.js +++ b/src/agent/coder.js @@ -29,7 +29,7 @@ export class Coder { // write custom code to file and import it async stageCode(code) { - code = this.santitizeCode(code); + code = this.sanitizeCode(code); let src = ''; code = code.replaceAll('console.log(', 'log(bot,'); code = code.replaceAll('log("', 'log(bot,"'); @@ -62,7 +62,8 @@ export class Coder { return await import('../..' + this.fp + filename); } - santitizeCode(code) { + sanitizeCode(code) { + code = code.trim(); const remove_strs = ['Javascript', 'javascript', 'js'] for (let r of remove_strs) { if (code.startsWith(r)) { @@ -93,6 +94,7 @@ export class Coder { let res = await this.generateCodeLoop(agent_history); this.generating = false; if (!res.interrupted) this.agent.bot.emit('idle'); + return res.message; } async generateCodeLoop(agent_history) { @@ -107,7 +109,7 @@ export class Coder { let failures = 0; for (let i=0; i<5; i++) { if (this.agent.bot.interrupt_code) - return; + return {success: true, message: null, interrupted: true, timedout: false}; console.log(messages) let res = await sendRequest(messages, system_message); console.log('Code generation response:', res) @@ -120,8 +122,7 @@ export class Coder { return {success: true, message: null, interrupted: false, timedout: false}; } if (failures >= 1) { - agent_history.add('system', 'Action failed, agent would not write code.'); - return {success: false, message: null, interrupted: false, timedout: false}; + return {success: false, message: 'Action failed, agent would not write code.', interrupted: false, timedout: false}; } messages.push({ role: 'system', @@ -143,7 +144,7 @@ export class Coder { if (code_return.interrupted && !code_return.timedout) return {success: false, message: null, interrupted: true, timedout: false}; - console.log(code_return.message); + console.log("Code generation result:", code_return.success, code_return.message); messages.push({ role: 'assistant', @@ -219,7 +220,7 @@ export class Coder { formatOutput(bot) { if (bot.interrupt_code && !this.timedout) return ''; let output = bot.output; - const MAX_OUT = 1000; + const MAX_OUT = 500; if (output.length > MAX_OUT) { output = `Code output is very long (${output.length} chars) and has been shortened.\n First outputs:\n${output.substring(0, MAX_OUT/2)}\n...skipping many lines.\nFinal outputs:\n ${output.substring(output.length - MAX_OUT/2)}`; diff --git a/src/agent/commands/actions.js b/src/agent/commands/actions.js index d89b2a7..6db22d7 100644 --- a/src/agent/commands/actions.js +++ b/src/agent/commands/actions.js @@ -26,7 +26,7 @@ export const actionsList = [ perform: async function (agent) { if (!settings.allow_insecure_coding) return 'Agent is not allowed to write code.'; - await agent.coder.generateCode(agent.history); + return await agent.coder.generateCode(agent.history); } }, { @@ -47,7 +47,7 @@ export const actionsList = [ } }, { - name: '!clear', + name: '!clearChat', description: 'Clear the chat history.', perform: async function (agent) { agent.history.clear(); @@ -84,7 +84,7 @@ export const actionsList = [ }, { name: '!followPlayer', - description: 'Endlessly follow the given player. Will defend that player if self_defense mode is on. Ex: !followPlayer("stevie")', + description: 'Endlessly follow the given player. Will defend that player if self_defense mode is on. Ex: !followPlayer("stevie", 4)', params: { 'player_name': '(string) The name of the player to follow.', 'follow_dist': '(number) The distance to follow from.' diff --git a/src/agent/commands/queries.js b/src/agent/commands/queries.js index ec79474..cd73073 100644 --- a/src/agent/commands/queries.js +++ b/src/agent/commands/queries.js @@ -59,7 +59,7 @@ export const queryList = [ } }, { - name: "!blocks", + name: "!nearbyBlocks", description: "Get the blocks near the bot.", perform: function (agent) { let bot = agent.bot; diff --git a/src/agent/history.js b/src/agent/history.js index 1e020d6..6617812 100644 --- a/src/agent/history.js +++ b/src/agent/history.js @@ -29,8 +29,8 @@ export class History { } getSystemMessage() { - let system_message = `You are a playful Minecraft bot named '${this.name}' that can communicate with players, see, move, mine, build, and interact with the world by using commands. Act human-like as if you were a typical Minecraft player, rather than an AI. Be very brief in your responses, use commands often, and do not give instructions unless asked. - Don't pretend to act, use commands immediately when requested. Do NOT do this: "Sure, I'll follow you! *follows you*", instead do this: "Sure I'll follow you! !followPlayer('steve')". Have fun :) \n`; + let system_message = `You are a playful Minecraft bot named '${this.name}' that can communicate with players, see, move, mine, build, and interact with the world by using commands. Act human-like as if you were a typical Minecraft player, rather than an AI. Be very brief in your responses, don't apologize constantly, do not give instructions unless asked, and do not 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('bingo', 3)". Have fun :) \n`; system_message += getCommandDocs(); if (this.bio != '') system_message += '\n\nBio:\n' + this.bio; diff --git a/src/agent/library/skills.js b/src/agent/library/skills.js index 06892cf..1befd7d 100644 --- a/src/agent/library/skills.js +++ b/src/agent/library/skills.js @@ -421,14 +421,9 @@ export async function breakBlockAt(bot, x, y, z) { * let position = world.getPosition(bot); * await skills.breakBlockAt(bot, position.x, position.y - 1, position.x); **/ + if (x == null || y == null || z == null) throw new Error('Invalid position to break block at.'); let block = bot.blockAt(Vec3(x, y, z)); if (block.name !== 'air' && block.name !== 'water' && block.name !== 'lava') { - await bot.tool.equipForBlock(block); - const itemId = bot.heldItem ? bot.heldItem.type : null - if (!block.canHarvest(itemId)) { - log(bot, `Don't have right tools to break ${block.name}.`); - return false; - } if (bot.entity.position.distanceTo(block.position) > 4.5) { let pos = block.position; let movements = new pf.Movements(bot); @@ -437,7 +432,18 @@ export async function breakBlockAt(bot, x, y, z) { bot.pathfinder.setMovements(movements); await bot.pathfinder.goto(new pf.goals.GoalNear(pos.x, pos.y, pos.z, 4)); } + await bot.tool.equipForBlock(block); + const itemId = bot.heldItem ? bot.heldItem.type : null + if (!block.canHarvest(itemId)) { + log(bot, `Don't have right tools to break ${block.name}.`); + return false; + } await bot.dig(block, true); + log(bot, `Broke ${block.name} at x:${x.toFixed(1)}, y:${y.toFixed(1)}, z:${z.toFixed(1)}.`); + } + else { + log(bot, `Skipping block at x:${x.toFixed(1)}, y:${y.toFixed(1)}, z:${z.toFixed(1)} because it is ${block.name}.`); + return false; } return true; } diff --git a/src/examples.json b/src/examples.json index 80a4dbb..6ee0f91 100644 --- a/src/examples.json +++ b/src/examples.json @@ -6,14 +6,14 @@ [ {"role": "user", "content": "grombo_Xx: What do you see?"}, - {"role": "assistant", "content": "Let me see... !blocks"}, + {"role": "assistant", "content": "Let me see... !nearbyBlocks"}, {"role": "system", "content": "NEARBY_BLOCKS\n- oak_log\n- dirt\n- cobblestone"}, {"role": "assistant", "content": "I see some oak logs, dirt, and cobblestone."} ], [ {"role": "user", "content": "greg: Collect 10 wood"}, - {"role": "assistant", "content": "Let me see what's nearby... !blocks"}, + {"role": "assistant", "content": "Let me see what's nearby... !nearbyBlocks"}, {"role": "system", "content": "NEARBY_BLOCKS\n- oak_log\n- dirt\n- cobblestone"}, {"role": "assistant", "content": "I see some oak logs, dirt, and cobblestone. I'll collect oak logs. !collectBlocks('oak_log', 10)"} ], @@ -77,8 +77,13 @@ ], [ - {"role": "user", "content": "brung00: build a house"}, - {"role": "assistant", "content": "Sure, I'll try to build a house where I am. !newAction"} + {"role": "user", "content": "poppy: build a cobblestone house"}, + {"role": "assistant", "content": "Sure, I'll to build a small cobblestone house where I am. !newAction"} + ], + + [ + {"role": "user", "content": "zib: build a dirt tower"}, + {"role": "assistant", "content": "Sure, I'll build a dirt tower 5 tall where I am. !newAction"} ], [ diff --git a/src/utils/examples.js b/src/utils/examples.js index 721ff0b..3e6a52c 100644 --- a/src/utils/examples.js +++ b/src/utils/examples.js @@ -4,7 +4,7 @@ import { stringifyTurns } from './text.js'; export class Examples { - constructor(select_num=3) { + constructor(select_num=2) { this.examples = []; this.select_num = select_num; } diff --git a/src/utils/gpt.js b/src/utils/gpt.js index b5281c7..5d18988 100644 --- a/src/utils/gpt.js +++ b/src/utils/gpt.js @@ -20,14 +20,28 @@ else { const openai = new OpenAIApi(openAiConfig); +let counter = 0; +let request_queue = []; +export async function sendRequest(turns, systemMessage) { + // this wrapper function ensures that new requests await the completion of previous requests in order + let id = counter++; + request_queue.push(id); + if (request_queue.length > 1) + console.log('awaiting previous requests to complete, queueing request', id); + while (request_queue[0] !== id) { + await new Promise(r => setTimeout(r, 100)); + } + let res = await queryGPT(turns, systemMessage); + request_queue.shift(); + return res; +} -export async function sendRequest(turns, systemMessage, stop_seq='***') { - +async function queryGPT(turns, systemMessage, stop_seq='***') { let messages = [{'role': 'system', 'content': systemMessage}].concat(turns); let res = null; try { - console.log('Awaiting openai api response...') + console.log('Awaiting openai api response...'); let completion = await openai.chat.completions.create({ model: 'gpt-3.5-turbo', messages: messages, @@ -35,13 +49,12 @@ export async function sendRequest(turns, systemMessage, stop_seq='***') { }); if (completion.choices[0].finish_reason == 'length') throw new Error('Context length exceeded'); - console.log('Received.') res = completion.choices[0].message.content; } catch (err) { if ((err.message == 'Context length exceeded' || err.code == 'context_length_exceeded') && turns.length > 1) { console.log('Context length exceeded, trying again with shorter context.'); - return await sendRequest(turns.slice(1), systemMessage, stop_seq); + return await queryGPT(turns.slice(1), systemMessage, stop_seq); } else { console.log(err); res = 'My brain disconnected, try again.'; From b3b7dd6a175893166a9ffad2dd56483566217676 Mon Sep 17 00:00:00 2001 From: Max Robinson Date: Fri, 16 Feb 2024 12:03:07 -0600 Subject: [PATCH 3/4] Create LICENSE --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1d5880c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Kolby Nottingham + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From 34d91dff790ef287143e0899bb738493dba69a0f Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Fri, 16 Feb 2024 17:04:11 -0600 Subject: [PATCH 4/4] removed request queuing --- src/utils/gpt.js | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/src/utils/gpt.js b/src/utils/gpt.js index 5d18988..b5281c7 100644 --- a/src/utils/gpt.js +++ b/src/utils/gpt.js @@ -20,28 +20,14 @@ else { const openai = new OpenAIApi(openAiConfig); -let counter = 0; -let request_queue = []; -export async function sendRequest(turns, systemMessage) { - // this wrapper function ensures that new requests await the completion of previous requests in order - let id = counter++; - request_queue.push(id); - if (request_queue.length > 1) - console.log('awaiting previous requests to complete, queueing request', id); - while (request_queue[0] !== id) { - await new Promise(r => setTimeout(r, 100)); - } - let res = await queryGPT(turns, systemMessage); - request_queue.shift(); - return res; -} -async function queryGPT(turns, systemMessage, stop_seq='***') { +export async function sendRequest(turns, systemMessage, stop_seq='***') { + let messages = [{'role': 'system', 'content': systemMessage}].concat(turns); let res = null; try { - console.log('Awaiting openai api response...'); + console.log('Awaiting openai api response...') let completion = await openai.chat.completions.create({ model: 'gpt-3.5-turbo', messages: messages, @@ -49,12 +35,13 @@ async function queryGPT(turns, systemMessage, stop_seq='***') { }); if (completion.choices[0].finish_reason == 'length') throw new Error('Context length exceeded'); + console.log('Received.') res = completion.choices[0].message.content; } catch (err) { if ((err.message == 'Context length exceeded' || err.code == 'context_length_exceeded') && turns.length > 1) { console.log('Context length exceeded, trying again with shorter context.'); - return await queryGPT(turns.slice(1), systemMessage, stop_seq); + return await sendRequest(turns.slice(1), systemMessage, stop_seq); } else { console.log(err); res = 'My brain disconnected, try again.';