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. 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 aca2f5c..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,13 +94,14 @@ 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) { 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); @@ -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 d9b88cf..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(); @@ -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); + 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.' + }, + perform: wrapExecution(async (agent, player_name, follow_dist) => { + await skills.followPlayer(agent.bot, player_name, follow_dist); }, -1, 'followPlayer') }, { 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 d5abb0b..1befd7d 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)); @@ -416,23 +421,29 @@ 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') { + if (bot.entity.position.distanceTo(block.position) > 4.5) { + let pos = block.position; + let movements = new pf.Movements(bot); + movements.canPlaceOn = false; + movements.allow1by1towers = false; + 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; } - if (bot.entity.position.distanceTo(block.position) > 4.5) { - let pos = block.position; - let movements = new pf.Movements(bot); - movements.canPlaceOn = false; - movements.allow1by1towers = false; - bot.pathfinder.setMovements(); - await bot.pathfinder.goto(new pf.goals.GoalNear(pos.x, pos.y, pos.z, 4)); - } 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; } @@ -639,11 +650,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 +668,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 +687,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..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)"} ], @@ -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)"} ], [ @@ -73,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; }