From b3fc11cfeca1b241ed904eba4c16f48d04c954c4 Mon Sep 17 00:00:00 2001 From: Kolby Nottingham Date: Wed, 3 Jan 2024 22:16:50 -0800 Subject: [PATCH 01/16] init commands --- src/agent/agent.js | 49 +++++++---------- src/agent/coder.js | 93 ++++++++++++++++++++++++++++++- src/agent/commands.js | 119 ++++++++++++++++++++++++++++++++++++++++ src/agent/history.js | 22 ++------ src/agent/skills.js | 2 +- src/examples.json | 20 +++---- src/examples_coder.json | 52 ++++++++++++++++++ src/utils/text.js | 14 +++++ 8 files changed, 312 insertions(+), 59 deletions(-) create mode 100644 src/agent/commands.js create mode 100644 src/examples_coder.json create mode 100644 src/utils/text.js diff --git a/src/agent/agent.js b/src/agent/agent.js index 21309de..4dc2992 100644 --- a/src/agent/agent.js +++ b/src/agent/agent.js @@ -3,7 +3,7 @@ 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 { getCommand, containsCommand } from './commands.js'; import { Events } from './events.js'; @@ -57,36 +57,27 @@ export class Agent { for (let i=0; i<5; i++) { let res = await sendRequest(this.history.getHistory(), this.history.getSystemMessage()); this.history.add(this.name, res); - let query_cmd = containsQuery(res); - if (query_cmd) { // contains query - let message = res.substring(0, res.indexOf(query_cmd)).trim(); - if (message) - this.bot.chat(message); - let query = getQuery(query_cmd); - let query_res = query.perform(this); - console.log('Agent used query:', query_cmd, 'and got:', query_res) - this.history.add('system', query_res); - } - else if (containsCodeBlock(res)) { // contains code block - console.log('Agent is executing code:', res) - let message = res.substring(0, res.indexOf('```')).trim(); - if (message) - this.bot.chat(message); - let code = res.substring(res.indexOf('```')+3, res.lastIndexOf('```')); + let query_name = containsQuery(res); + let command_name = containsCommand(res); - if (code) { - this.coder.queueCode(code); - let code_return = await this.coder.execute(); - let message = code_return.message; - if (code_return.interrupted && !code_return.timedout) - break; - if (!code_return.success) { - message += "\nWrite code to fix the problem and try again."; - } - console.log('code return:', message); - this.history.add('system', message); - } + if (query_name || command_name) { // contains query or command + console.log('Query/Command response:', res); + + let execute_name = query_name ? query_name : command_name; + let message = res.substring(0, res.indexOf(execute_name)).trim(); + if (message) + this.bot.chat(message); + + let execute_func = query_name ? getQuery(query_name) : getCommand(command_name); + let execute_res = await execute_func.perform(this); + + console.log('Agent executed:', execute_name, 'and got:', execute_res); + + if (execute_res) + this.history.add('system', execute_res); + else + break; } else { // conversation response this.bot.chat(res); diff --git a/src/agent/coder.js b/src/agent/coder.js index 23f3026..2446f08 100644 --- a/src/agent/coder.js +++ b/src/agent/coder.js @@ -1,4 +1,7 @@ -import { writeFile, readFile, unlink, mkdirSync } from 'fs'; +import { writeFile, readFile, mkdirSync } from 'fs'; +import { sendRequest, embed, cosineSimilarity } from '../utils/gpt.js'; +import { stringifyTurns } from '../utils/text.js'; + export class Coder { constructor(agent) { @@ -12,6 +15,8 @@ export class Coder { this.agent.bot.output = ''; this.code_template = ''; this.timedout = false; + this.fewshot = 3; + this.examples = []; readFile('./bots/template.js', 'utf8', (err, data) => { if (err) throw err; @@ -49,6 +54,92 @@ export class Coder { }); } + async loadExamples() { + let examples = []; + try { + const data = readFileSync('./src/examples.json', 'utf8'); + examples = JSON.parse(data); + } catch (err) { + console.log('No history examples found.'); + } + + this.examples = []; + for (let example of examples) { + let context = ''; + for (let turn of example.conversation) { + context += turn.content + '\n'; + } + context = context.trim(); + const embedding = await embed(context); + this.examples.push({'embedding': embedding, 'turns': example}); + } + + await this.setExamples(); + } + + async sortExamples(messages) { + let context = ''; + for (let turn of messages) { + context += turn.content + '\n'; + } + context = context.trim(); + const embedding = await embed(context); + this.examples.sort((a, b) => { + return cosineSimilarity(a.embedding, embedding) - cosineSimilarity(b.embedding, embedding); + }); + } + + async generateCode(agent_history) { + let system_message = "You are a minecraft bot that plays minecraft by writing javascript. Given the conversation between you and the user, use the provided skills and world queries to write your code. You will then be given a response to your code. If you are satisfied with the response, return output without writing any additional code. If you want to try again, output the code you want to try."; + system_message += getSkillDocs(); + + let messages = []; + this.sortExamples(agent_history.turns); + for (let example of this.examples.slice(-this.fewshot)) { + messages.push({ + role: 'user', + content: stringifyTurns(example.conversation) + }); + for (let i = 0; i < example.coder.length; i++) { + messages.push({ + role: i % 2 == 0 ? 'assistant' : 'user', + content: example.coder[i] + }); + } + } + messages.push({ + role: 'user', + content: stringifyTurns(agent_history.turns), + }); + + let final_message = 'No code generated.'; + for (let i=0; i<5; i++) { + + let res = await sendRequest(messages, system_message); + let code = res.substring(res.indexOf('```')+3, res.lastIndexOf('```')); + + if (!code) + break; + + agent.coder.queueCode(code); + let code_return = await agent.coder.execute(); + + if (code_return.interrupted && !custom_return.timedout) + break; + + messages.push({ + role: 'assistant', + content: res + }); + messages.push({ + role: 'user', + content: code_return.message + }); + final_message = code_return.message; + } + + return final_message; + } // returns {success: bool, message: string, interrupted: bool, timedout: false} async execute() { diff --git a/src/agent/commands.js b/src/agent/commands.js new file mode 100644 index 0000000..c6e45e0 --- /dev/null +++ b/src/agent/commands.js @@ -0,0 +1,119 @@ +import * as skills from './skills.js'; +import * as world from './world.js'; + + +function wrapExecution(func) { + return async function (agent) { + agent.bot.output = ''; + agent.coder.executing = true; + let res = await func(agent); + if (res) + agent.bot.output += '\n' + res; + agent.coder.executing = false; + return '\n' + agent.bot.output + '\n'; + } +} + +const commandList = [ + { + name: '!execute_action', + description: 'Write and execute code to perform custom behaviors not available as a command.', + perform: async function (agent) { + let res = await agent.coder.generateCode(agent.history); + if (res) + return '\n' + res + '\n'; + } + }, + { + name: '!abort', + description: 'Force stop all actions and commands that are currently executing.', + perform: async function (agent) { + await agent.coder.stop(); + } + }, + // { + // name: '!gotoplayer', + // description: 'Go to the nearest player.', + // perform: wrapExecution(async (agent) => { + // let player_name = world.getNearbyPlayerNames(agent.bot); + // if (player_name.length == 0) + // return 'No players nearby.'; + // await skills.goToPlayer(agent.bot, player_name[0]); + // }) + // }, + // { + // name: '!followplayer', + // description: 'Follow the nearest player.', + // perform: wrapExecution(async (agent) => { + // let player_name = world.getNearbyPlayerNames(agent.bot); + // if (player_name.length == 0) + // return 'No players nearby.'; + // await skills.followPlayer(agent.bot, player_name[0]); + // }) + // }, + // { + // name: '!collectwood', + // description: 'Collect 3 wood logs of any type.', + // perform: wrapExecution(async (agent) => { + // let blocks = world.getNearbyBlockTypes(agent.bot); + // for (let block of blocks) { + // if (block.includes('log')) { + // await skills.collectBlock(agent.bot, block, 3); + // return; + // } + // } + // return 'No wood nearby.'; + // }) + // }, + // { + // name: '!collectstone', + // description: 'Collect 3 cobblestone blocks.', + // perform: wrapExecution(async (agent) => { + // let inventory = world.getInventoryCounts(agent.bot); + // for (const item in inventory) { + // if (inventory[item] && inventory[item] > 0 && item.includes('pickaxe')) { + // if (await skills.equip(agent.bot, 'pickaxe')) + // await skills.collectBlock(agent.bot, 'stone', 3); + // return; + // } + // } + // return 'No pickaxe in inventory.'; + // }) + // }, + // { + // name: '!fightmob', + // description: 'Fight the nearest mob.', + // perform: wrapExecution(async (agent) => { + // let mobs = world.getNearbyMobTypes(agent.bot); + // if (mobs.length == 0) + // return 'No mobs nearby.'; + // await skills.attackMob(agent.bot, mobs[0], true); + // }) + // } +]; + +const commandMap = {}; +for (let command of commandList) { + commandMap[command.name] = command; +} + +export function getCommand(name) { + return commandMap[name]; +} + +export function containsCommand(message) { + for (let command of commandList) { + if (message.includes(command.name)) { + return command.name; + } + } + return null; +} + +export function getCommandDocs() { + let docs = `\n*COMMAND DOCS\n You can use the following commands to execute actions in the world. Use the command name in your response and the results of the command will be included in the next input. Do not use commands not listed below. If trying to perform an action outside of the scope the listed commands, use the !custom command to write custom code.\n`; + for (let command of commandList) { + docs += command.name + ': ' + command.description + '\n'; + } + return docs + '*\n'; +} \ No newline at end of file diff --git a/src/agent/history.js b/src/agent/history.js index 6f1cfd0..d26a97d 100644 --- a/src/agent/history.js +++ b/src/agent/history.js @@ -1,7 +1,8 @@ import { writeFileSync, readFileSync, mkdirSync } from 'fs'; import { getQueryDocs } from './queries.js'; -import { getSkillDocs } from './skill-library.js'; +import { getCommandDocs } from './commands.js'; import { sendRequest, embed, cosineSimilarity } from '../utils/gpt.js'; +import { stringifyTurns } from '../utils/text.js'; export class History { @@ -39,7 +40,7 @@ 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 writing and executing code. Act human-like as if you were a typical Minecraft player, rather than an AI. Be very brief in your responses, omit needless words, and do not give instructions unless asked.`; system_message += getQueryDocs(); - system_message += getSkillDocs(); + system_message += getCommandDocs(); if (this.bio != '') system_message += '\n\nBio:\n' + this.bio; if (this.memory != '') @@ -48,21 +49,6 @@ export class History { return system_message; } - stringifyTurns(turns) { - let res = ''; - for (let turn of turns) { - if (turn.role === 'assistant') { - res += `\nYour output:\n${turn.content}`; - } else if (turn.role === 'system') { - res += `\nSystem output: ${turn.content}`; - } else { - res += `\nUser input: ${turn.content}`; - - } - } - return res.trim(); - } - async storeMemories(turns) { console.log("To summarize:", turns) let memory_prompt = 'Update your "Memory" by summarizing the following conversation. Your "Memory" is for storing information that will help you improve as a Minecraft bot. Include details about your interactions with other players that you may need to remember for later. Also include things that you have learned through player feedback or by executing code. Do not include information found in your Docs or that you got right on the first try. Be extremely brief and clear.'; @@ -75,7 +61,7 @@ export class History { memory_prompt += '- I learned that player [name]...\n'; memory_prompt += 'This is the conversation to summarize:\n'; - memory_prompt += this.stringifyTurns(turns); + memory_prompt += stringifyTurns(turns); memory_prompt += 'Summarize relevant information from your previous memory and this conversation:\n'; diff --git a/src/agent/skills.js b/src/agent/skills.js index 1c9396e..2073eed 100644 --- a/src/agent/skills.js +++ b/src/agent/skills.js @@ -547,7 +547,7 @@ export async function goToPlayer(bot, username) { log(bot, `Failed to reach ${username}.`); return false; } - log(bot, `Player is now at ${player.position}.`); + log(bot, `You have reached the player at position ${player.position}.`); return true; } diff --git a/src/examples.json b/src/examples.json index 5e71f40..6e07d6b 100644 --- a/src/examples.json +++ b/src/examples.json @@ -15,56 +15,56 @@ {"role": "user", "content": "greg: Collect 10 wood"}, {"role": "assistant", "content": "Let me see what's nearby... !blocks"}, {"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.\n```\n// I am going to collect 10 oak_log.\nawait skills.collectBlock(bot, 'oak_log', 10);\n```"} + {"role": "assistant", "content": "I see some oak logs, dirt, and cobblestone. I'll collect oak logs. !execute_action"} ], [ {"role": "user", "content": "bobby: Craft a plank"}, {"role": "assistant", "content": "Okay! !craftable"}, {"role": "system", "content": "CRAFTABLE_ITEMS\n- spruce_planks\n- spruce_wood\n- ladder"}, - {"role": "assistant", "content": "```\n// I am going to craft a spruce_planks.\nawait skills.craftRecipe(bot, 'spruce_planks');\n```"} + {"role": "assistant", "content": "!execute_action"} ], [ {"role": "user", "content": "zZZn98: come here"}, - {"role": "assistant", "content": "```\n// I am going to navigate to zZZn98.\nawait skills.goToPlayer(bot, 'zZZn98');\n```"}, + {"role": "assistant", "content": "!execute_action"}, {"role": "system", "content": "Code execution finished successfully."}, {"role": "assistant", "content": "Here!"} ], [ {"role": "user", "content": "hanky: collect some sand and give it to me please"}, - {"role": "assistant", "content": "Collecting sand...\n```\n// I am going to collect 3 sand and give to hanky.\nawait skills.collectBlock(bot, 'sand', 3);\nawait skills.giveToPlayer(bot, 'sand', 'hanky');\n```"}, + {"role": "assistant", "content": "Collecting sand... !execute_action"}, {"role": "system", "content": "Code Output:\nYou have reached player hanky.\nCode execution finished successfully."}, {"role": "assistant", "content": "Here!"} ], [ {"role": "user", "content": "joe: Follow me!"}, - {"role": "assistant", "content": "Sure!\n```\n// I am going to follow joe.\nawait skills.followPlayer(bot, 'joe');\n```"} + {"role": "assistant", "content": "Sure! !execute_action"} ], [ {"role": "user", "content": "hanky: kill that zombie!"}, - {"role": "assistant", "content": "I'm attacking!\n```\n//I'm going to attack the nearest zombie.\nawait skills.attackMob(bot, 'zombie');\n```"}, + {"role": "assistant", "content": "I'm attacking! !execute_action"}, {"role": "system", "content": "Code Output:\nNo zombie nearby"}, {"role": "assistant", "content": "I could not find a zombie nearby."} ], [ {"role": "user", "content": "billybob: stop"}, - {"role": "assistant", "content": "```\n// I am going to write empty code to stop whatever I am doing\n```"} + {"role": "assistant", "content": "!execute_action"} ], [ {"role": "user", "content": "maya: go to the nearest oak log"}, - {"role": "assistant", "content": "```\n// I am navigating to the nearest oak log.\nawait skills.goToPosition(bot, nearestOakLog.x, nearestOakLog.y, nearestOakLog.z);\n```"}, + {"role": "assistant", "content": "!execute_action`"}, {"role": "system", "content": "Code output:\n!!Code threw exception!! Error: ReferenceError: nearestOakLog is not defined\n Write code to fix the problem and try again."}, - {"role": "assistant", "content": "Apologies, I made an error. Let me try that again.```\n// I am navigating to the nearest oak log.\nlet position = world.getNearestBlock(bot, 'oak_log', 20);\nawait skills.goToPosition(bot, position.x, position.y, position.z);\n```"} + {"role": "assistant", "content": "Apologies, I made an error. Would you like me to try again?"} ], [ {"role": "user", "content": "trixy88: craft some sticks"}, - {"role": "assistant", "content": "```\n// I am going to craft 8 sticks\nawait skills.craftRecipe(bot, 'stick');\nlog(bot, crafted)\nawait skills.craftRecipe(bot, 'stick');```"} + {"role": "assistant", "content": "!execute_action"} ] ] \ No newline at end of file diff --git a/src/examples_coder.json b/src/examples_coder.json new file mode 100644 index 0000000..ad332f6 --- /dev/null +++ b/src/examples_coder.json @@ -0,0 +1,52 @@ +[ + { + "conversation": [ + {"role": "user", "content": "greg: Collect 10 wood"}, + {"role": "assistant", "content": "Let me see what's nearby... !blocks"}, + {"role": "system", "content": "NEARBY_BLOCKS\n- oak_log\n- dirt\n- cobblestone"} + ], + "coder": [ + {"role": "assistant", "content": "I see some oak logs, dirt, and cobblestone. I'll collect oak logs.\n```\n// I am going to collect 10 oak_log.\nawait skills.collectBlock(bot, 'oak_log', 10);\n```"}, + {"role": "user", "content": "Code execution finished successfully."}, + {"role": "assistant", "content": "DONE"} + ] + }, + + { + "conversation": [ + {"role": "user", "content": "bobby: Craft a plank"}, + {"role": "assistant", "content": "Okay! !craftable"}, + {"role": "system", "content": "CRAFTABLE_ITEMS\n- spruce_planks\n- spruce_wood\n- ladder"} + ], + "coder": [ + {"role": "assistant", "content": "```\n// I am going to craft a spruce_planks.\nawait skills.craftRecipe(bot, 'spruce_planks');\n```"}, + {"role": "user", "content": "Code execution finished successfully."}, + {"role": "assistant", "content": "DONE"} + ] + }, + + { + "conversation": [ + {"role": "user", "content": "zZZn98: come here"} + ], + "coder": [ + {"role": "assistant", "content": "```\n// I am going to navigate to zZZn98.\nawait skills.goToPlayer(bot, 'zZZn98');\n```"}, + {"role": "user", "content": "Code execution finished successfully."}, + {"role": "assistant", "content": "DONE"} + ] + }, + + { + "conversation": [ + {"role": "user", "content": "maya: go to the nearest oak log"} + ], + "coder": [ + {"role": "assistant", "content": "```\n// I am navigating to the nearest oak log.\nawait skills.goToPosition(bot, nearestOakLog.x, nearestOakLog.y, nearestOakLog.z);\n```"}, + {"role": "system", "content": "Code output:\n!!Code threw exception!! Error: ReferenceError: nearestOakLog is not defined\n Write code to fix the problem and try again."}, + {"role": "assistant", "content": "Apologies, I made an error. Let me try that again.```\n// I am navigating to the nearest oak log.\nlet position = world.getNearestBlock(bot, 'oak_log', 20);\nawait skills.goToPosition(bot, position.x, position.y, position.z);\n```"}, + {"role": "user", "content": "Code execution finished successfully."}, + {"role": "assistant", "content": "DONE"} + ] + } + +] \ No newline at end of file diff --git a/src/utils/text.js b/src/utils/text.js new file mode 100644 index 0000000..d06221a --- /dev/null +++ b/src/utils/text.js @@ -0,0 +1,14 @@ +export function stringifyTurns(turns) { + let res = ''; + for (let turn of turns) { + if (turn.role === 'assistant') { + res += `\nYour output:\n${turn.content}`; + } else if (turn.role === 'system') { + res += `\nSystem output: ${turn.content}`; + } else { + res += `\nUser input: ${turn.content}`; + + } + } + return res.trim(); +} \ No newline at end of file From a99909ac5c375a5cb2c3e919d39b74698b23327a Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Tue, 9 Jan 2024 13:42:39 -0600 Subject: [PATCH 02/16] basic execute_action functionality added --- src/agent/coder.js | 101 +++++++++++++------------------------ src/agent/history.js | 2 +- src/agent/skill-library.js | 2 +- 3 files changed, 37 insertions(+), 68 deletions(-) diff --git a/src/agent/coder.js b/src/agent/coder.js index 2446f08..25e1379 100644 --- a/src/agent/coder.js +++ b/src/agent/coder.js @@ -1,6 +1,7 @@ import { writeFile, readFile, mkdirSync } from 'fs'; import { sendRequest, embed, cosineSimilarity } from '../utils/gpt.js'; import { stringifyTurns } from '../utils/text.js'; +import { getSkillDocs } from './skill-library.js'; export class Coder { @@ -54,91 +55,59 @@ export class Coder { }); } - async loadExamples() { - let examples = []; - try { - const data = readFileSync('./src/examples.json', 'utf8'); - examples = JSON.parse(data); - } catch (err) { - console.log('No history examples found.'); - } - - this.examples = []; - for (let example of examples) { - let context = ''; - for (let turn of example.conversation) { - context += turn.content + '\n'; - } - context = context.trim(); - const embedding = await embed(context); - this.examples.push({'embedding': embedding, 'turns': example}); - } - - await this.setExamples(); - } - - async sortExamples(messages) { - let context = ''; - for (let turn of messages) { - context += turn.content + '\n'; - } - context = context.trim(); - const embedding = await embed(context); - this.examples.sort((a, b) => { - return cosineSimilarity(a.embedding, embedding) - cosineSimilarity(b.embedding, embedding); - }); - } async generateCode(agent_history) { - let system_message = "You are a minecraft bot that plays minecraft by writing javascript. Given the conversation between you and the user, use the provided skills and world queries to write your code. You will then be given a response to your code. If you are satisfied with the response, return output without writing any additional code. If you want to try again, output the code you want to try."; + let system_message = "You are a minecraft bot that plays minecraft by writing javascript codeblocks. Given the conversation between you and the user, use the provided skills and world queries 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(); - let messages = []; - this.sortExamples(agent_history.turns); - for (let example of this.examples.slice(-this.fewshot)) { - messages.push({ - role: 'user', - content: stringifyTurns(example.conversation) - }); - for (let i = 0; i < example.coder.length; i++) { - messages.push({ - role: i % 2 == 0 ? 'assistant' : 'user', - content: example.coder[i] - }); - } - } - messages.push({ - role: 'user', - content: stringifyTurns(agent_history.turns), - }); + 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."; - let final_message = 'No code generated.'; + let messages = agent_history.getHistory(false); + + let code_return = null; + let failures = 0; for (let i=0; i<5; i++) { - + console.log(messages) let res = await sendRequest(messages, system_message); + console.log('Code generation response:', res) + let contains_code = res.indexOf('```') !== -1; + if (!contains_code) { + if (code_return) { + agent_history.add('system', code_return.message); + agent_history.add(this.agent.name, res); + this.agent.bot.chat(res); + return; + } + if (failures >= 1) { + agent_history.add('system', 'Action failed, agent would not write code.'); + return; + } + messages.push({ + role: 'system', + content: 'Error: no code provided. Write code in codeblock in your response. ``` // example ```'} + ); + failures++; + continue; + } let code = res.substring(res.indexOf('```')+3, res.lastIndexOf('```')); - if (!code) - break; + this.queueCode(code); + code_return = await this.execute(); - agent.coder.queueCode(code); - let code_return = await agent.coder.execute(); - - if (code_return.interrupted && !custom_return.timedout) - break; + if (code_return.interrupted && !code_return.timedout) + return; messages.push({ role: 'assistant', content: res }); messages.push({ - role: 'user', + role: 'system', content: code_return.message }); - final_message = code_return.message; } - - return final_message; + + return } // returns {success: bool, message: string, interrupted: bool, timedout: false} diff --git a/src/agent/history.js b/src/agent/history.js index d26a97d..9dcb6b7 100644 --- a/src/agent/history.js +++ b/src/agent/history.js @@ -38,7 +38,7 @@ 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 writing and executing code. Act human-like as if you were a typical Minecraft player, rather than an AI. Be very brief in your responses, omit needless words, and do not give instructions unless asked.`; + 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 writing and executing code. Act human-like as if you were a typical Minecraft player, rather than an AI. Be very brief in your responses, use actions often, and do not give instructions unless asked.`; system_message += getQueryDocs(); system_message += getCommandDocs(); if (this.bio != '') diff --git a/src/agent/skill-library.js b/src/agent/skill-library.js index 246e3ab..3bca956 100644 --- a/src/agent/skill-library.js +++ b/src/agent/skill-library.js @@ -2,7 +2,7 @@ import * as skills from './skills.js'; import * as world from './world.js'; export function getSkillDocs() { - let docstring = "\n*SKILL DOCS\nThese skills are javascript functions that can be called with a js function by writing a code block. Ex: '```// write description comment and code here```' \nYour code block should return a bool indicating if the task was completed successfully. It will return true if you don't write a return statement.\n"; + let docstring = "\n*SKILL DOCS\nThese skills are javascript functions that can be called when writing actions and skills.\n"; docstring += docHelper(Object.values(skills), 'skills'); docstring += docHelper(Object.values(world), 'world'); return docstring + '*\n'; From 7d35ef1bfc0561baab2ba04ebd9bc0dfcb373425 Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Tue, 9 Jan 2024 22:50:22 -0600 Subject: [PATCH 03/16] combine queries+commands, allow user commands --- src/agent/agent.js | 26 +++++--- src/agent/commands.js | 99 ++--------------------------- src/agent/commands/actions.js | 94 +++++++++++++++++++++++++++ src/agent/{ => commands}/queries.js | 38 ++--------- src/agent/history.js | 2 - 5 files changed, 122 insertions(+), 137 deletions(-) create mode 100644 src/agent/commands/actions.js rename src/agent/{ => commands}/queries.js (78%) diff --git a/src/agent/agent.js b/src/agent/agent.js index 4dc2992..3a438de 100644 --- a/src/agent/agent.js +++ b/src/agent/agent.js @@ -2,7 +2,6 @@ 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 { getCommand, containsCommand } from './commands.js'; import { Events } from './events.js'; @@ -54,25 +53,32 @@ export class Agent { if (!!source && !!message) await this.history.add(source, message); + const user_command_name = containsCommand(message); + if (user_command_name) { + this.bot.chat(`*${source} used ${user_command_name.substring(1)}*`); + let execute_res = await getCommand(user_command_name).perform(this); + if (execute_res) + this.bot.chat(execute_res); + else + this.bot.chat('Finished command.'); + return; + } + for (let i=0; i<5; i++) { let res = await sendRequest(this.history.getHistory(), this.history.getSystemMessage()); this.history.add(this.name, res); - let query_name = containsQuery(res); let command_name = containsCommand(res); - if (query_name || command_name) { // contains query or command + if (command_name) { // contains query or command console.log('Query/Command response:', res); - let execute_name = query_name ? query_name : command_name; - let message = res.substring(0, res.indexOf(execute_name)).trim(); - if (message) - this.bot.chat(message); + let message = res.substring(0, res.indexOf(command_name)).trim(); - let execute_func = query_name ? getQuery(query_name) : getCommand(command_name); - let execute_res = await execute_func.perform(this); + this.bot.chat(`${message} *used ${command_name.substring(1)}*`); + let execute_res = await getCommand(command_name).perform(this); - console.log('Agent executed:', execute_name, 'and got:', execute_res); + console.log('Agent executed:', command_name, 'and got:', execute_res); if (execute_res) this.history.add('system', execute_res); diff --git a/src/agent/commands.js b/src/agent/commands.js index c6e45e0..8015fae 100644 --- a/src/agent/commands.js +++ b/src/agent/commands.js @@ -1,97 +1,8 @@ -import * as skills from './skills.js'; -import * as world from './world.js'; +import { actionsList } from './commands/actions.js'; +import { queryList } from './commands/queries.js'; -function wrapExecution(func) { - return async function (agent) { - agent.bot.output = ''; - agent.coder.executing = true; - let res = await func(agent); - if (res) - agent.bot.output += '\n' + res; - agent.coder.executing = false; - return '\n' + agent.bot.output + '\n'; - } -} - -const commandList = [ - { - name: '!execute_action', - description: 'Write and execute code to perform custom behaviors not available as a command.', - perform: async function (agent) { - let res = await agent.coder.generateCode(agent.history); - if (res) - return '\n' + res + '\n'; - } - }, - { - name: '!abort', - description: 'Force stop all actions and commands that are currently executing.', - perform: async function (agent) { - await agent.coder.stop(); - } - }, - // { - // name: '!gotoplayer', - // description: 'Go to the nearest player.', - // perform: wrapExecution(async (agent) => { - // let player_name = world.getNearbyPlayerNames(agent.bot); - // if (player_name.length == 0) - // return 'No players nearby.'; - // await skills.goToPlayer(agent.bot, player_name[0]); - // }) - // }, - // { - // name: '!followplayer', - // description: 'Follow the nearest player.', - // perform: wrapExecution(async (agent) => { - // let player_name = world.getNearbyPlayerNames(agent.bot); - // if (player_name.length == 0) - // return 'No players nearby.'; - // await skills.followPlayer(agent.bot, player_name[0]); - // }) - // }, - // { - // name: '!collectwood', - // description: 'Collect 3 wood logs of any type.', - // perform: wrapExecution(async (agent) => { - // let blocks = world.getNearbyBlockTypes(agent.bot); - // for (let block of blocks) { - // if (block.includes('log')) { - // await skills.collectBlock(agent.bot, block, 3); - // return; - // } - // } - // return 'No wood nearby.'; - // }) - // }, - // { - // name: '!collectstone', - // description: 'Collect 3 cobblestone blocks.', - // perform: wrapExecution(async (agent) => { - // let inventory = world.getInventoryCounts(agent.bot); - // for (const item in inventory) { - // if (inventory[item] && inventory[item] > 0 && item.includes('pickaxe')) { - // if (await skills.equip(agent.bot, 'pickaxe')) - // await skills.collectBlock(agent.bot, 'stone', 3); - // return; - // } - // } - // return 'No pickaxe in inventory.'; - // }) - // }, - // { - // name: '!fightmob', - // description: 'Fight the nearest mob.', - // perform: wrapExecution(async (agent) => { - // let mobs = world.getNearbyMobTypes(agent.bot); - // if (mobs.length == 0) - // return 'No mobs nearby.'; - // await skills.attackMob(agent.bot, mobs[0], true); - // }) - // } -]; - +const commandList = queryList.concat(actionsList); const commandMap = {}; for (let command of commandList) { commandMap[command.name] = command; @@ -111,7 +22,9 @@ export function containsCommand(message) { } export function getCommandDocs() { - let docs = `\n*COMMAND DOCS\n You can use the following commands to execute actions in the world. Use the command name in your response and the results of the command will be included in the next input. Do not use commands not listed below. If trying to perform an action outside of the scope the listed commands, use the !custom command to write custom code.\n`; + let docs = `\n*Command DOCS\n You can use the following commands to perform actions and get information about the world. + Use the commands with the syntax: !commandName \n + Don't use codeblocks. Only use one command in each response, trailing commands will be ignored. Use these commands frequently in your responses!\n`; for (let command of commandList) { docs += command.name + ': ' + command.description + '\n'; } diff --git a/src/agent/commands/actions.js b/src/agent/commands/actions.js new file mode 100644 index 0000000..1ff0419 --- /dev/null +++ b/src/agent/commands/actions.js @@ -0,0 +1,94 @@ +import * as skills from '../skills.js'; +import * as world from '../world.js'; + +function wrapExecution(func) { + return async function (agent) { + agent.bot.output = ''; + agent.coder.executing = true; + let res = await func(agent); + if (res) + agent.bot.output += '\n' + res; + agent.coder.executing = false; + return '\n' + agent.bot.output + '\n'; + } +} + +// const actionsList = [ +export const actionsList = [ + { + name: '!execute_action', + description: 'Write and execute code to perform custom behaviors not available as a command.', + perform: async function (agent) { + let res = await agent.coder.generateCode(agent.history); + if (res) + return '\n' + res + '\n'; + } + }, + { + name: '!stop', + description: 'Force stop all actions and commands that are currently executing.', + perform: async function (agent) { + await agent.coder.stop(); + return 'Agent stopped.'; + } + }, + // { + // name: '!gotoplayer', + // description: 'Go to the nearest player.', + // perform: wrapExecution(async (agent) => { + // let player_name = world.getNearbyPlayerNames(agent.bot); + // if (player_name.length == 0) + // return 'No players nearby.'; + // await skills.goToPlayer(agent.bot, player_name[0]); + // }) + // }, + // { + // name: '!followplayer', + // description: 'Follow the nearest player.', + // perform: wrapExecution(async (agent) => { + // let player_name = world.getNearbyPlayerNames(agent.bot); + // if (player_name.length == 0) + // return 'No players nearby.'; + // await skills.followPlayer(agent.bot, player_name[0]); + // }) + // }, + // { + // name: '!collectwood', + // description: 'Collect 3 wood logs of any type.', + // perform: wrapExecution(async (agent) => { + // let blocks = world.getNearbyBlockTypes(agent.bot); + // for (let block of blocks) { + // if (block.includes('log')) { + // await skills.collectBlock(agent.bot, block, 3); + // return; + // } + // } + // return 'No wood nearby.'; + // }) + // }, + // { + // name: '!collectstone', + // description: 'Collect 3 cobblestone blocks.', + // perform: wrapExecution(async (agent) => { + // let inventory = world.getInventoryCounts(agent.bot); + // for (const item in inventory) { + // if (inventory[item] && inventory[item] > 0 && item.includes('pickaxe')) { + // if (await skills.equip(agent.bot, 'pickaxe')) + // await skills.collectBlock(agent.bot, 'stone', 3); + // return; + // } + // } + // return 'No pickaxe in inventory.'; + // }) + // }, + // { + // name: '!fightmob', + // description: 'Fight the nearest mob.', + // perform: wrapExecution(async (agent) => { + // let mobs = world.getNearbyMobTypes(agent.bot); + // if (mobs.length == 0) + // return 'No mobs nearby.'; + // await skills.attackMob(agent.bot, mobs[0], true); + // }) + // } +]; diff --git a/src/agent/queries.js b/src/agent/commands/queries.js similarity index 78% rename from src/agent/queries.js rename to src/agent/commands/queries.js index d1408b9..ae98164 100644 --- a/src/agent/queries.js +++ b/src/agent/commands/queries.js @@ -1,15 +1,15 @@ -import { getNearestBlock, getNearbyMobTypes, getNearbyPlayerNames, getNearbyBlockTypes, getInventoryCounts } from './world.js'; -import { getAllItems } from '../utils/mcdata.js'; - +import { getNearestBlock, getNearbyMobTypes, getNearbyPlayerNames, getNearbyBlockTypes, getInventoryCounts } from '../world.js'; +import { getAllItems } from '../../utils/mcdata.js'; const pad = (str) => { return '\n' + str + '\n'; } -const queryList = [ +// queries are commands that just return strings and don't affect anything in the world +export const queryList = [ { name: "!stats", - description: "Get your bot's stats", + description: "Get your bot's location, health, and time of day.", perform: function (agent) { let bot = agent.bot; let res = 'STATS'; @@ -102,30 +102,4 @@ const queryList = [ 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 +]; \ No newline at end of file diff --git a/src/agent/history.js b/src/agent/history.js index 9dcb6b7..1ba13ea 100644 --- a/src/agent/history.js +++ b/src/agent/history.js @@ -1,5 +1,4 @@ import { writeFileSync, readFileSync, mkdirSync } from 'fs'; -import { getQueryDocs } from './queries.js'; import { getCommandDocs } from './commands.js'; import { sendRequest, embed, cosineSimilarity } from '../utils/gpt.js'; import { stringifyTurns } from '../utils/text.js'; @@ -39,7 +38,6 @@ 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 writing and executing code. Act human-like as if you were a typical Minecraft player, rather than an AI. Be very brief in your responses, use actions often, and do not give instructions unless asked.`; - system_message += getQueryDocs(); system_message += getCommandDocs(); if (this.bio != '') system_message += '\n\nBio:\n' + this.bio; From 0decd272f32fefcec29c974afb878a7e4d77a3c5 Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Thu, 11 Jan 2024 15:59:52 -0600 Subject: [PATCH 04/16] added command param parsing --- src/agent/agent.js | 10 ++--- src/agent/commands.js | 77 +++++++++++++++++++++++++++++++---- src/agent/commands/actions.js | 35 +++++++--------- src/agent/commands/queries.js | 2 +- src/examples.json | 18 ++++---- 5 files changed, 100 insertions(+), 42 deletions(-) diff --git a/src/agent/agent.js b/src/agent/agent.js index 3a438de..4e652ef 100644 --- a/src/agent/agent.js +++ b/src/agent/agent.js @@ -2,7 +2,7 @@ import { initBot } from '../utils/mcdata.js'; import { sendRequest } from '../utils/gpt.js'; import { History } from './history.js'; import { Coder } from './coder.js'; -import { getCommand, containsCommand } from './commands.js'; +import { containsCommand, executeCommand } from './commands.js'; import { Events } from './events.js'; @@ -56,7 +56,7 @@ export class Agent { const user_command_name = containsCommand(message); if (user_command_name) { this.bot.chat(`*${source} used ${user_command_name.substring(1)}*`); - let execute_res = await getCommand(user_command_name).perform(this); + let execute_res = await executeCommand(this, message); if (execute_res) this.bot.chat(execute_res); else @@ -73,10 +73,10 @@ export class Agent { if (command_name) { // contains query or command console.log('Query/Command response:', res); - let message = res.substring(0, res.indexOf(command_name)).trim(); + let pre_message = res.substring(0, res.indexOf(command_name)).trim(); - this.bot.chat(`${message} *used ${command_name.substring(1)}*`); - let execute_res = await getCommand(command_name).perform(this); + this.bot.chat(`${pre_message} *used ${command_name.substring(1)}*`); + let execute_res = await executeCommand(this, res); console.log('Agent executed:', command_name, 'and got:', execute_res); diff --git a/src/agent/commands.js b/src/agent/commands.js index 8015fae..88dcc7e 100644 --- a/src/agent/commands.js +++ b/src/agent/commands.js @@ -12,21 +12,84 @@ export function getCommand(name) { return commandMap[name]; } +const commandRegex = /!(\w+)(?:\(((?:[^)(]+|'[^']*'|"[^"]*")*)\))?/ +const argRegex = /(?:"[^"]*"|'[^']*'|[^,])+/g; + export function containsCommand(message) { - for (let command of commandList) { - if (message.includes(command.name)) { - return command.name; - } + const commandMatch = message.match(commandRegex); + if (commandMatch) { + const commandName = "!"+commandMatch[1]; + if (commandList.some((command) => command.name === commandName)) + return commandName; } return null; } +function parseCommandMessage(message) { + const commandMatch = message.match(commandRegex); + if (commandMatch) { + const commandName = "!"+commandMatch[1]; + if (!commandMatch[2]) + return { commandName, args: [] }; + let args = commandMatch[2].match(argRegex); + if (args) { + for (let i = 0; i < args.length; i++) { + args[i] = args[i].trim(); + } + + for (let i = 0; i < args.length; i++) { + let arg = args[i]; + if ((arg.startsWith('"') && arg.endsWith('"')) || (arg.startsWith("'") && arg.endsWith("'"))) { + args[i] = arg.substring(1, arg.length-1); + } else if (!isNaN(arg)) { + args[i] = Number(arg); + } + } + } + else + args = []; + + return { commandName, args }; + } + return null; +} + +function numParams(command) { + if (!command.params) + return 0; + return Object.keys(command.params).length; +} + +export async function executeCommand(agent, message) { + let parsed = parseCommandMessage(message); + if (parsed) { + const command = getCommand(parsed.commandName); + let numArgs = 0; + if (parsed.args) { + numArgs = parsed.args.length; + } + console.log('parsed command:', parsed); + if (numArgs !== numParams(command)) + return `Command ${command.name} was given incorrect number of arguments`; + else + return await command.perform(agent, ...parsed.args); + } + else + return `Command is incorrectly formatted`; +} + export function getCommandDocs() { - let docs = `\n*Command DOCS\n You can use the following commands to perform actions and get information about the world. - Use the commands with the syntax: !commandName \n - Don't use codeblocks. Only use one command in each response, trailing commands will be ignored. Use these commands frequently in your responses!\n`; + let docs = `\n*COMMAND DOCS\n You can use the following commands to perform actions and get information about the world. + Use the commands with the syntax: !commandName or !commandName("arg1", 1.2, ...) if the command takes arguments.\n + Do not use codeblocks. Only use one command in each response, trailing commands will be ignored. Use these commands frequently in your responses!\n`; for (let command of commandList) { docs += command.name + ': ' + command.description + '\n'; + if (command.params) { + docs += 'Params:\n'; + for (let param in command.params) { + docs += param + ': ' + command.params[param] + '\n'; + } + } } return docs + '*\n'; } \ No newline at end of file diff --git a/src/agent/commands/actions.js b/src/agent/commands/actions.js index 1ff0419..98b3af2 100644 --- a/src/agent/commands/actions.js +++ b/src/agent/commands/actions.js @@ -2,10 +2,10 @@ import * as skills from '../skills.js'; import * as world from '../world.js'; function wrapExecution(func) { - return async function (agent) { + return async function (agent, ...args) { agent.bot.output = ''; agent.coder.executing = true; - let res = await func(agent); + let res = await func(agent, ...args); if (res) agent.bot.output += '\n' + res; agent.coder.executing = false; @@ -16,8 +16,8 @@ function wrapExecution(func) { // const actionsList = [ export const actionsList = [ { - name: '!execute_action', - description: 'Write and execute code to perform custom behaviors not available as a command.', + name: '!newAction', + description: 'Perform new and unknown custom behaviors that are not available as a command by writing code.', perform: async function (agent) { let res = await agent.coder.generateCode(agent.history); if (res) @@ -32,24 +32,19 @@ export const actionsList = [ return 'Agent stopped.'; } }, + { + name: '!goToPlayer', + description: 'Go to the nearest 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); + }) + }, // { - // name: '!gotoplayer', - // description: 'Go to the nearest player.', - // perform: wrapExecution(async (agent) => { - // let player_name = world.getNearbyPlayerNames(agent.bot); - // if (player_name.length == 0) - // return 'No players nearby.'; - // await skills.goToPlayer(agent.bot, player_name[0]); - // }) - // }, - // { - // name: '!followplayer', + // name: '!followPlayer', // description: 'Follow the nearest player.', - // perform: wrapExecution(async (agent) => { - // let player_name = world.getNearbyPlayerNames(agent.bot); - // if (player_name.length == 0) - // return 'No players nearby.'; - // await skills.followPlayer(agent.bot, player_name[0]); + // perform: wrapExecution(async (agent, player_name) => { + // await skills.followPlayer(agent.bot, player_name); // }) // }, // { diff --git a/src/agent/commands/queries.js b/src/agent/commands/queries.js index ae98164..ac5f198 100644 --- a/src/agent/commands/queries.js +++ b/src/agent/commands/queries.js @@ -96,7 +96,7 @@ export const queryList = [ } }, { - name: "!action", + name: "!currentAction", description: "Get the currently executing code.", perform: function (agent) { return pad("Current code:\n`" + agent.coder.current_code +"`"); diff --git a/src/examples.json b/src/examples.json index 6e07d6b..df329e1 100644 --- a/src/examples.json +++ b/src/examples.json @@ -15,56 +15,56 @@ {"role": "user", "content": "greg: Collect 10 wood"}, {"role": "assistant", "content": "Let me see what's nearby... !blocks"}, {"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. !execute_action"} + {"role": "assistant", "content": "I see some oak logs, dirt, and cobblestone. I'll collect oak logs. !newAction"} ], [ {"role": "user", "content": "bobby: Craft a plank"}, {"role": "assistant", "content": "Okay! !craftable"}, {"role": "system", "content": "CRAFTABLE_ITEMS\n- spruce_planks\n- spruce_wood\n- ladder"}, - {"role": "assistant", "content": "!execute_action"} + {"role": "assistant", "content": "!newAction"} ], [ {"role": "user", "content": "zZZn98: come here"}, - {"role": "assistant", "content": "!execute_action"}, + {"role": "assistant", "content": "!goToPlayer('sumixam0')"}, {"role": "system", "content": "Code execution finished successfully."}, {"role": "assistant", "content": "Here!"} ], [ {"role": "user", "content": "hanky: collect some sand and give it to me please"}, - {"role": "assistant", "content": "Collecting sand... !execute_action"}, + {"role": "assistant", "content": "Collecting sand... !newAction"}, {"role": "system", "content": "Code Output:\nYou have reached player hanky.\nCode execution finished successfully."}, {"role": "assistant", "content": "Here!"} ], [ {"role": "user", "content": "joe: Follow me!"}, - {"role": "assistant", "content": "Sure! !execute_action"} + {"role": "assistant", "content": "Sure! !newAction"} ], [ {"role": "user", "content": "hanky: kill that zombie!"}, - {"role": "assistant", "content": "I'm attacking! !execute_action"}, + {"role": "assistant", "content": "I'm attacking! !newAction"}, {"role": "system", "content": "Code Output:\nNo zombie nearby"}, {"role": "assistant", "content": "I could not find a zombie nearby."} ], [ {"role": "user", "content": "billybob: stop"}, - {"role": "assistant", "content": "!execute_action"} + {"role": "assistant", "content": "!newAction"} ], [ {"role": "user", "content": "maya: go to the nearest oak log"}, - {"role": "assistant", "content": "!execute_action`"}, + {"role": "assistant", "content": "!newAction`"}, {"role": "system", "content": "Code output:\n!!Code threw exception!! Error: ReferenceError: nearestOakLog is not defined\n Write code to fix the problem and try again."}, {"role": "assistant", "content": "Apologies, I made an error. Would you like me to try again?"} ], [ {"role": "user", "content": "trixy88: craft some sticks"}, - {"role": "assistant", "content": "!execute_action"} + {"role": "assistant", "content": "!newAction"} ] ] \ No newline at end of file From 0eba82951bf0a2fa3a45131f130beefbe3575ada Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Thu, 11 Jan 2024 18:04:59 -0600 Subject: [PATCH 05/16] added follow, collect, attack commands --- src/agent/commands.js | 2 +- src/agent/commands/actions.js | 73 +++++++++++++---------------------- src/examples.json | 10 ++--- 3 files changed, 33 insertions(+), 52 deletions(-) diff --git a/src/agent/commands.js b/src/agent/commands.js index 88dcc7e..b7e6cf4 100644 --- a/src/agent/commands.js +++ b/src/agent/commands.js @@ -81,7 +81,7 @@ export async function executeCommand(agent, message) { export function getCommandDocs() { let docs = `\n*COMMAND DOCS\n You can use the following commands to perform actions and get information about the world. Use the commands with the syntax: !commandName or !commandName("arg1", 1.2, ...) if the command takes arguments.\n - Do not use codeblocks. Only use one command in each response, trailing commands will be ignored. Use these commands frequently in your responses!\n`; + Do not use codeblocks. Only use one command in each response, trailing commands and comments will be ignored. Use these commands frequently in your responses!\n`; for (let command of commandList) { docs += command.name + ': ' + command.description + '\n'; if (command.params) { diff --git a/src/agent/commands/actions.js b/src/agent/commands/actions.js index 98b3af2..e13c06a 100644 --- a/src/agent/commands/actions.js +++ b/src/agent/commands/actions.js @@ -40,50 +40,31 @@ export const actionsList = [ return await skills.goToPlayer(agent.bot, player_name); }) }, - // { - // name: '!followPlayer', - // description: 'Follow the nearest player.', - // perform: wrapExecution(async (agent, player_name) => { - // await skills.followPlayer(agent.bot, player_name); - // }) - // }, - // { - // name: '!collectwood', - // description: 'Collect 3 wood logs of any type.', - // perform: wrapExecution(async (agent) => { - // let blocks = world.getNearbyBlockTypes(agent.bot); - // for (let block of blocks) { - // if (block.includes('log')) { - // await skills.collectBlock(agent.bot, block, 3); - // return; - // } - // } - // return 'No wood nearby.'; - // }) - // }, - // { - // name: '!collectstone', - // description: 'Collect 3 cobblestone blocks.', - // perform: wrapExecution(async (agent) => { - // let inventory = world.getInventoryCounts(agent.bot); - // for (const item in inventory) { - // if (inventory[item] && inventory[item] > 0 && item.includes('pickaxe')) { - // if (await skills.equip(agent.bot, 'pickaxe')) - // await skills.collectBlock(agent.bot, 'stone', 3); - // return; - // } - // } - // return 'No pickaxe in inventory.'; - // }) - // }, - // { - // name: '!fightmob', - // description: 'Fight the nearest mob.', - // perform: wrapExecution(async (agent) => { - // let mobs = world.getNearbyMobTypes(agent.bot); - // if (mobs.length == 0) - // return 'No mobs nearby.'; - // await skills.attackMob(agent.bot, mobs[0], true); - // }) - // } + { + name: '!followPlayer', + description: 'Endlessly follow the nearest player. 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); + }) + }, + { + name: '!collectBlocks', + description: 'Collect the nearest blocks of a given type.', + params: { + 'type': '(string) The block type to collect. Ex: !collectBlocks("stone", 10)', + 'num': '(number) The number of blocks to collect.' + }, + perform: wrapExecution(async (agent, type, num) => { + await skills.collectBlock(agent.bot, type, num); + }) + }, + { + name: '!attack', + description: 'Attack and kill the nearest entity of a given type.', + params: {'type': '(string) The type of entity to attack.'}, + perform: wrapExecution(async (agent, type) => { + await skills.attackMob(agent.bot, type, true); + }) + } ]; diff --git a/src/examples.json b/src/examples.json index df329e1..9950928 100644 --- a/src/examples.json +++ b/src/examples.json @@ -15,7 +15,7 @@ {"role": "user", "content": "greg: Collect 10 wood"}, {"role": "assistant", "content": "Let me see what's nearby... !blocks"}, {"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. !newAction"} + {"role": "assistant", "content": "I see some oak logs, dirt, and cobblestone. I'll collect oak logs. !collectBlocks('oak_log', 10)"} ], [ @@ -27,7 +27,7 @@ [ {"role": "user", "content": "zZZn98: come here"}, - {"role": "assistant", "content": "!goToPlayer('sumixam0')"}, + {"role": "assistant", "content": "On my way! !goToPlayer('zZZn98')"}, {"role": "system", "content": "Code execution finished successfully."}, {"role": "assistant", "content": "Here!"} ], @@ -41,19 +41,19 @@ [ {"role": "user", "content": "joe: Follow me!"}, - {"role": "assistant", "content": "Sure! !newAction"} + {"role": "assistant", "content": "Sure! !followPlayer('joe')"} ], [ {"role": "user", "content": "hanky: kill that zombie!"}, - {"role": "assistant", "content": "I'm attacking! !newAction"}, + {"role": "assistant", "content": "I'm attacking! !attack('zombie')"}, {"role": "system", "content": "Code Output:\nNo zombie nearby"}, {"role": "assistant", "content": "I could not find a zombie nearby."} ], [ {"role": "user", "content": "billybob: stop"}, - {"role": "assistant", "content": "!newAction"} + {"role": "assistant", "content": "Sure. !stop"} ], [ From 24b8c17db0685c0d33b73bb55e99d8b7bbb8913b Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Thu, 11 Jan 2024 18:10:51 -0600 Subject: [PATCH 06/16] parse bools --- src/agent/commands.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/agent/commands.js b/src/agent/commands.js index b7e6cf4..54e6764 100644 --- a/src/agent/commands.js +++ b/src/agent/commands.js @@ -25,6 +25,7 @@ export function containsCommand(message) { return null; } +// todo: handle arrays? function parseCommandMessage(message) { const commandMatch = message.match(commandRegex); if (commandMatch) { @@ -43,6 +44,8 @@ function parseCommandMessage(message) { args[i] = arg.substring(1, arg.length-1); } else if (!isNaN(arg)) { args[i] = Number(arg); + } else if (arg === 'true' || arg === 'false') { + args[i] = arg === 'true'; } } } @@ -70,7 +73,7 @@ export async function executeCommand(agent, message) { } console.log('parsed command:', parsed); if (numArgs !== numParams(command)) - return `Command ${command.name} was given incorrect number of arguments`; + return `Command ${command.name} was given ${numArgs} args, but requires ${numParams(command)} args.`; else return await command.perform(agent, ...parsed.args); } From 4f48dea4f6ab69d3204dc391654e6c5f4f244172 Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Sat, 13 Jan 2024 12:10:15 -0600 Subject: [PATCH 07/16] improved example reading, commands --- src/agent/agent.js | 14 ++++++++++---- src/agent/coder.js | 1 + src/agent/history.js | 14 ++++++++------ 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/agent/agent.js b/src/agent/agent.js index 4e652ef..14a4e9c 100644 --- a/src/agent/agent.js +++ b/src/agent/agent.js @@ -18,7 +18,10 @@ export class Agent { this.events = new Events(this, this.history.events) this.bot.on('login', async () => { - this.bot.chat('Hello world! I am ' + this.name); + await this.history.loadExamples(); + + if (!init_message) + this.bot.chat('Hello world! I am ' + this.name); console.log(`${this.name} logged in.`); const ignore_messages = [ @@ -39,7 +42,6 @@ export class Agent { this.handleMessage(username, message); }); - await this.history.loadExamples(); if (init_message) { this.handleMessage('system', init_message); @@ -57,10 +59,14 @@ export class Agent { if (user_command_name) { this.bot.chat(`*${source} used ${user_command_name.substring(1)}*`); let execute_res = await executeCommand(this, message); + if (user_command_name === '!newAction') { + // all user initiated commands are ignored by the bot except for this one + // add the preceding message to the history to give context for newAction + let truncated_msg = message.substring(0, message.indexOf(user_command_name)).trim(); + this.history.add(source, truncated_msg); + } if (execute_res) this.bot.chat(execute_res); - else - this.bot.chat('Finished command.'); return; } diff --git a/src/agent/coder.js b/src/agent/coder.js index 25e1379..ed4e15b 100644 --- a/src/agent/coder.js +++ b/src/agent/coder.js @@ -96,6 +96,7 @@ export class Coder { if (code_return.interrupted && !code_return.timedout) return; + console.log(code_return.message); messages.push({ role: 'assistant', diff --git a/src/agent/history.js b/src/agent/history.js index 1ba13ea..7af9365 100644 --- a/src/agent/history.js +++ b/src/agent/history.js @@ -25,19 +25,21 @@ export class History { } getHistory(include_examples=true) { - let history = []; + // return deep copy of history + let turns = JSON.parse(JSON.stringify(this.turns)); if (include_examples && this.selected_examples.length > 0) { + let example_messages = 'Here are some examples of how to respond:\n'; for (let example of this.selected_examples) { - history = history.concat(example.turns); + example_messages += 'Example:\n' + stringifyTurns(example.turns) + '\n'; } + return [{'role': 'system', 'content': example_messages}].concat(turns); } - history = history.concat(this.turns); - - return history; + return turns; } 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 writing and executing code. Act human-like as if you were a typical Minecraft player, rather than an AI. Be very brief in your responses, use actions often, and do not give instructions unless asked.`; + 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`; system_message += getCommandDocs(); if (this.bio != '') system_message += '\n\nBio:\n' + this.bio; From ff9c24187648a4bdadf6102426a5140a84eb99e3 Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Sat, 13 Jan 2024 12:11:04 -0600 Subject: [PATCH 08/16] added defend, improved gotoplayer --- src/agent/commands/actions.js | 14 ++++-- src/agent/skills.js | 80 ++++++++++++++++++++++++++++------- 2 files changed, 75 insertions(+), 19 deletions(-) diff --git a/src/agent/commands/actions.js b/src/agent/commands/actions.js index e13c06a..6f97838 100644 --- a/src/agent/commands/actions.js +++ b/src/agent/commands/actions.js @@ -3,6 +3,7 @@ import * as world from '../world.js'; function wrapExecution(func) { return async function (agent, ...args) { + await agent.coder.stop(); agent.bot.output = ''; agent.coder.executing = true; let res = await func(agent, ...args); @@ -13,7 +14,6 @@ function wrapExecution(func) { } } -// const actionsList = [ export const actionsList = [ { name: '!newAction', @@ -34,7 +34,7 @@ export const actionsList = [ }, { name: '!goToPlayer', - description: 'Go to the nearest player. Ex: !goToPlayer("steve")', + 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); @@ -42,7 +42,7 @@ export const actionsList = [ }, { name: '!followPlayer', - description: 'Endlessly follow the nearest player. Ex: !followPlayer("stevie")', + description: 'Endlessly follow the given player. 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); @@ -66,5 +66,13 @@ export const actionsList = [ perform: wrapExecution(async (agent, type) => { await skills.attackMob(agent.bot, type, true); }) + }, + { + name: '!defend', + description: 'Follow the given player and attack any nearby monsters.', + params: {'player_name': '(string) The name of the player to defend.'}, + perform: wrapExecution(async (agent, player_name) => { + await skills.defendPlayer(agent.bot, player_name); + }) } ]; diff --git a/src/agent/skills.js b/src/agent/skills.js index 2073eed..ae3e052 100644 --- a/src/agent/skills.js +++ b/src/agent/skills.js @@ -44,13 +44,15 @@ export async function smeltItem(bot, itemName, num=1) { /** * Puts 1 coal in furnace and smelts the given item name, waits until the furnace runs out of fuel or input items. * @param {MinecraftBot} bot, reference to the minecraft bot. - * @param {string} itemName, the item name to smelt. Must contain "raw" + * @param {string} itemName, the item name to smelt. Ores must contain "raw" like raw_iron. * @param {number} num, the number of items to smelt. Defaults to 1. * @returns {Promise} true if the item was smelted, false otherwise. Fail * @example * await skills.smeltItem(bot, "raw_iron"); + * await skills.smeltItem(bot, "beef"); **/ - if (!itemName.includes('raw')) { + const foods = ['beef', 'chicken', 'cod', 'mutton', 'porkchop', 'rabbit', 'salmon', 'tropical_fish']; + if (!itemName.includes('raw') && !foods.includes(itemName)) { log(bot, `Cannot smelt ${itemName}, must be a "raw" item, like "raw_iron".`); return false; } // TODO: allow cobblestone, sand, clay, etc. @@ -166,6 +168,13 @@ export async function clearNearestFurnace(bot) { } +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]; + if (weapon) + bot.equip(weapon, 'hand'); +} + export async function attackMob(bot, mobType, kill=true) { /** * Attack mob of the given type. @@ -177,16 +186,11 @@ export async function attackMob(bot, mobType, kill=true) { * await skills.attackMob(bot, "zombie", true); **/ const mob = bot.nearestEntity(entity => entity.name && entity.name.toLowerCase() === mobType.toLowerCase()); - const attackable = ['animal', 'monster', 'mob']; - if (mob && attackable.includes(mob.type)) { + if (mob) { let pos = mob.position; console.log(bot.entity.position.distanceTo(pos)) - // equip highest damage weapon - 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]; - if (weapon) - await bot.equip(weapon, 'hand'); + equipHighestAttack(bot) if (!kill) { if (bot.entity.position.distanceTo(pos) > 5) { @@ -542,13 +546,10 @@ export async function goToPlayer(bot, username) { return false; } - let arrived = await goToPosition(bot, player.position.x, player.position.y, player.position.z); - if (!arrived) { - log(bot, `Failed to reach ${username}.`); - return false; - } - log(bot, `You have reached the player at position ${player.position}.`); - return true; + bot.pathfinder.setMovements(new pf.Movements(bot)); + await bot.pathfinder.goto(new pf.goals.GoalFollow(player, 2), true); + + log(bot, `You have reached ${username}.`); } @@ -573,5 +574,52 @@ export async function followPlayer(bot, username) { await new Promise(resolve => setTimeout(resolve, 1000)); } + return true; +} + +export async function defendPlayer(bot, username) { + /** + * Defend the given player endlessly, attacking any nearby monsters. Will not return until the code is manually stopped. + * @param {MinecraftBot} bot, reference to the minecraft bot. + * @param {string} username, the username of the player to defend. + * @returns {Promise} true if the player was found, false otherwise. + * @example + * await skills.defendPlayer(bot, "bob"); + **/ + let player = bot.players[username].entity + if (!player) + return false; + + bot.pathfinder.setMovements(new pf.Movements(bot)); + bot.pathfinder.setGoal(new pf.goals.GoalFollow(player, 5), true); + log(bot, `Actively defending player ${username}.`); + + while (!bot.interrupt_code) { + if (bot.entity.position.distanceTo(player.position) < 10) { + const mobs = getNearbyMobs(bot, 8).filter(mob => mob.type === 'mob' || mob.type === 'hostile'); + const mob = mobs.sort((a, b) => a.position.distanceTo(player.position) - b.position.distanceTo(player.position))[0]; // get closest to player + if (mob) { + bot.pathfinder.stop(); + log(bot, `Found ${mob.name}, attacking!`); + bot.chat(`Found ${mob.name}, attacking!`); + equipHighestAttack(bot); + bot.pvp.attack(mob); + while (getNearbyMobs(bot, 8).includes(mob)) { + await new Promise(resolve => setTimeout(resolve, 500)); + console.log('attacking...') + if (bot.interrupt_code || bot.entity.position.distanceTo(player.position) > 16) { + console.log('stopping pvp...'); + bot.pvp.stop(); + break; + } + } + console.log('resuming pathfinder...') + bot.pathfinder.setMovements(new pf.Movements(bot)); + bot.pathfinder.setGoal(new pf.goals.GoalFollow(player, 5), true); + } + } + await new Promise(resolve => setTimeout(resolve, 500)); + } + return true; } \ No newline at end of file From 44fe31a66e9f0d138627f3f4459d1f2e9be1cc92 Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Sun, 14 Jan 2024 14:33:25 -0600 Subject: [PATCH 09/16] autoeat, improved command parsing --- src/agent/agent.js | 19 ++++++++++++++----- src/agent/commands.js | 13 ++++++++----- src/utils/mcdata.js | 9 +++++++++ 3 files changed, 31 insertions(+), 10 deletions(-) diff --git a/src/agent/agent.js b/src/agent/agent.js index 14a4e9c..534d582 100644 --- a/src/agent/agent.js +++ b/src/agent/agent.js @@ -2,7 +2,7 @@ import { initBot } from '../utils/mcdata.js'; import { sendRequest } from '../utils/gpt.js'; import { History } from './history.js'; import { Coder } from './coder.js'; -import { containsCommand, executeCommand } from './commands.js'; +import { containsCommand, commandExists, executeCommand } from './commands.js'; import { Events } from './events.js'; @@ -19,9 +19,7 @@ export class Agent { this.bot.on('login', async () => { await this.history.loadExamples(); - - if (!init_message) - this.bot.chat('Hello world! I am ' + this.name); + console.log(`${this.name} logged in.`); const ignore_messages = [ @@ -42,10 +40,17 @@ export class Agent { this.handleMessage(username, message); }); + // set the bot to automatically eat food when hungry + this.bot.autoEat.options = { + priority: 'foodPoints', + startAt: 14, + bannedFood: [] + }; if (init_message) { this.handleMessage('system', init_message); } else { + this.bot.chat('Hello world! I am ' + this.name); this.bot.emit('finished_executing'); } }); @@ -77,7 +82,11 @@ export class Agent { let command_name = containsCommand(res); if (command_name) { // contains query or command - console.log('Query/Command response:', res); + console.log('Command message:', res); + if (!commandExists(command_name)) { + this.history.add('system', `Command ${command_name} does not exist. Use !newAction to perform custom actions.`); + continue; + } let pre_message = res.substring(0, res.indexOf(command_name)).trim(); diff --git a/src/agent/commands.js b/src/agent/commands.js index 54e6764..2944b18 100644 --- a/src/agent/commands.js +++ b/src/agent/commands.js @@ -17,14 +17,17 @@ const argRegex = /(?:"[^"]*"|'[^']*'|[^,])+/g; export function containsCommand(message) { const commandMatch = message.match(commandRegex); - if (commandMatch) { - const commandName = "!"+commandMatch[1]; - if (commandList.some((command) => command.name === commandName)) - return commandName; - } + if (commandMatch) + return commandName; return null; } +export function commandExists(commandName) { + if (!commandName.startsWith("!")) + commandName = "!" + commandName; + return commandMap[commandName] !== undefined; +} + // todo: handle arrays? function parseCommandMessage(message) { const commandMatch = message.match(commandRegex); diff --git a/src/utils/mcdata.js b/src/utils/mcdata.js index 641e82d..057d706 100644 --- a/src/utils/mcdata.js +++ b/src/utils/mcdata.js @@ -3,6 +3,8 @@ import { createBot } from 'mineflayer'; import { pathfinder } from 'mineflayer-pathfinder'; import { plugin as pvp } from 'mineflayer-pvp'; import { plugin as collectblock } from 'mineflayer-collectblock'; +import { plugin as autoEat } from 'mineflayer-auto-eat'; + const mc_version = '1.19.3' const mcdata = minecraftData(mc_version); @@ -18,6 +20,8 @@ export function initBot(username) { bot.loadPlugin(pathfinder); bot.loadPlugin(pvp); bot.loadPlugin(collectblock); + bot.loadPlugin(autoEat); + return bot; } @@ -77,3 +81,8 @@ export function getAllBlockIds(ignore) { } return blockIds; } + +export function getBiomeName(bot) { + const biomeId = bot.world.getBiome(bot.entity.position); + return mcdata.biomes[biomeId].name; +} From 237175e944fb9265bd83622a7bc02aca37df3acd Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Sun, 14 Jan 2024 14:33:37 -0600 Subject: [PATCH 10/16] improved defend --- src/agent/skills.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/agent/skills.js b/src/agent/skills.js index ae3e052..a025822 100644 --- a/src/agent/skills.js +++ b/src/agent/skills.js @@ -590,13 +590,17 @@ export async function defendPlayer(bot, username) { if (!player) return false; + const follow_distance = 3; + const attack_distance = 12; + const return_distance = 16; + bot.pathfinder.setMovements(new pf.Movements(bot)); - bot.pathfinder.setGoal(new pf.goals.GoalFollow(player, 5), true); + bot.pathfinder.setGoal(new pf.goals.GoalFollow(player, follow_distance), true); log(bot, `Actively defending player ${username}.`); while (!bot.interrupt_code) { - if (bot.entity.position.distanceTo(player.position) < 10) { - const mobs = getNearbyMobs(bot, 8).filter(mob => mob.type === 'mob' || mob.type === 'hostile'); + if (bot.entity.position.distanceTo(player.position) < return_distance) { + const mobs = getNearbyMobs(bot, attack_distance).filter(mob => mob.type === 'mob' || mob.type === 'hostile'); const mob = mobs.sort((a, b) => a.position.distanceTo(player.position) - b.position.distanceTo(player.position))[0]; // get closest to player if (mob) { bot.pathfinder.stop(); @@ -604,10 +608,12 @@ export async function defendPlayer(bot, username) { bot.chat(`Found ${mob.name}, attacking!`); equipHighestAttack(bot); bot.pvp.attack(mob); - while (getNearbyMobs(bot, 8).includes(mob)) { + while (getNearbyMobs(bot, attack_distance).includes(mob)) { await new Promise(resolve => setTimeout(resolve, 500)); console.log('attacking...') - if (bot.interrupt_code || bot.entity.position.distanceTo(player.position) > 16) { + if (bot.interrupt_code) + return; + if (bot.entity.position.distanceTo(player.position) > return_distance) { console.log('stopping pvp...'); bot.pvp.stop(); break; @@ -616,6 +622,7 @@ export async function defendPlayer(bot, username) { console.log('resuming pathfinder...') bot.pathfinder.setMovements(new pf.Movements(bot)); bot.pathfinder.setGoal(new pf.goals.GoalFollow(player, 5), true); + await new Promise(resolve => setTimeout(resolve, 3000)); } } await new Promise(resolve => setTimeout(resolve, 500)); From 69a49f98030bc7dfa2e83485ee04e9e4e3086e34 Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Sun, 14 Jan 2024 14:35:04 -0600 Subject: [PATCH 11/16] autoeat --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 39ea3ce..ec6f905 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "dependencies": { "minecraft-data": "^3.46.2", "mineflayer": "^4.14.0", + "mineflayer-auto-eat": "^3.3.6", "mineflayer-collectblock": "^1.4.1", "mineflayer-pathfinder": "^2.4.4", "mineflayer-pvp": "^1.3.2", From 193f794f3d526d0204cf6a716a7fa32dbf9a1c69 Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Sun, 14 Jan 2024 14:35:23 -0600 Subject: [PATCH 12/16] attempted improve stats --- src/agent/commands/queries.js | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/agent/commands/queries.js b/src/agent/commands/queries.js index ac5f198..6e95765 100644 --- a/src/agent/commands/queries.js +++ b/src/agent/commands/queries.js @@ -1,5 +1,5 @@ import { getNearestBlock, getNearbyMobTypes, getNearbyPlayerNames, getNearbyBlockTypes, getInventoryCounts } from '../world.js'; -import { getAllItems } from '../../utils/mcdata.js'; +import { getAllItems, getBiomeName } from '../../utils/mcdata.js'; const pad = (str) => { return '\n' + str + '\n'; @@ -9,18 +9,27 @@ const pad = (str) => { export const queryList = [ { name: "!stats", - description: "Get your bot's location, health, and time of day.", + description: "Get your bot's location, health, hunger, and time of day.", 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`; + let pos = bot.entity.position; + // display position to 2 decimal places + res += `\n- Position: x: ${pos.x.toFixed(2)}, y: ${pos.y.toFixed(2)}, z: ${pos.z.toFixed(2)}`; + res += `\n- Health: ${Math.round(bot.health)} / 20`; + res += `\n- Hunger: ${Math.round(bot.food)} / 20`; + res += `\n- Biome: ${getBiomeName(bot)}`; + // let block = bot.blockAt(pos); + // res += `\n- Artficial light: ${block.skyLight}`; + // res += `\n- Sky light: ${block.light}`; + // light properties are bugged, they are not accurate + if (bot.time.timeOfDay < 6000) { - res += '\n- time: Morning'; + res += '\n- Time: Morning'; } else if (bot.time.timeOfDay < 12000) { - res += '\n- time: Afternoon'; + res += '\n- Time: Afternoon'; } else { - res += '\n- time: Night'; + res += '\n- Time: Night'; } return pad(res); } From 2f4ecfada34326f58c071ad1d2d023bb8c29c32d Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Mon, 15 Jan 2024 11:04:50 -0600 Subject: [PATCH 13/16] refactored examples/startup --- src/agent/agent.js | 15 +++++-- src/agent/coder.js | 15 +++---- src/agent/commands.js | 2 +- src/agent/examples.js | 64 ++++++++++++++++++++++++++++++ src/agent/history.js | 62 +++-------------------------- src/examples_coder.json | 82 +++++++++++++++------------------------ src/process/init-agent.js | 2 +- 7 files changed, 123 insertions(+), 119 deletions(-) create mode 100644 src/agent/examples.js diff --git a/src/agent/agent.js b/src/agent/agent.js index 534d582..a4181c3 100644 --- a/src/agent/agent.js +++ b/src/agent/agent.js @@ -1,26 +1,31 @@ import { initBot } from '../utils/mcdata.js'; import { sendRequest } from '../utils/gpt.js'; import { History } from './history.js'; +import { Examples } from './examples.js'; import { Coder } from './coder.js'; import { containsCommand, commandExists, executeCommand } from './commands.js'; import { Events } from './events.js'; export class Agent { - constructor(name, profile=null, init_message=null) { + async start(name, profile=null, init_message=null) { this.name = name; - this.bot = initBot(name); + this.examples = new Examples(); this.history = new History(this); this.coder = new Coder(this); this.history.load(profile); + await this.examples.load('./src/examples.json'); + await this.coder.load(); + + this.bot = initBot(name); this.events = new Events(this, this.history.events) this.bot.on('login', async () => { - await this.history.loadExamples(); console.log(`${this.name} logged in.`); + this.coder.clear(); const ignore_messages = [ "Set own game mode to", @@ -76,7 +81,8 @@ export class Agent { } for (let i=0; i<5; i++) { - let res = await sendRequest(this.history.getHistory(), this.history.getSystemMessage()); + let history = await this.history.getHistory(this.examples); + let res = await sendRequest(history, this.history.getSystemMessage()); this.history.add(this.name, res); let command_name = containsCommand(res); @@ -85,6 +91,7 @@ export class Agent { console.log('Command message:', res); if (!commandExists(command_name)) { this.history.add('system', `Command ${command_name} does not exist. Use !newAction to perform custom actions.`); + console.log('Agent hallucinated command:', command_name) continue; } diff --git a/src/agent/coder.js b/src/agent/coder.js index ed4e15b..264f39b 100644 --- a/src/agent/coder.js +++ b/src/agent/coder.js @@ -1,7 +1,7 @@ import { writeFile, readFile, mkdirSync } from 'fs'; -import { sendRequest, embed, cosineSimilarity } from '../utils/gpt.js'; -import { stringifyTurns } from '../utils/text.js'; +import { sendRequest } from '../utils/gpt.js'; import { getSkillDocs } from './skill-library.js'; +import { Examples } from './examples.js'; export class Coder { @@ -11,13 +11,14 @@ export class Coder { this.current_code = ''; this.file_counter = 0; 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; - this.fewshot = 3; - this.examples = []; + } + + async load() { + this.examples = new Examples(); + await this.examples.load('./src/examples_coder.json'); readFile('./bots/template.js', 'utf8', (err, data) => { if (err) throw err; @@ -62,7 +63,7 @@ export class Coder { 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."; - let messages = agent_history.getHistory(false); + let messages = await agent_history.getHistory(this.examples); let code_return = null; let failures = 0; diff --git a/src/agent/commands.js b/src/agent/commands.js index 2944b18..0b13775 100644 --- a/src/agent/commands.js +++ b/src/agent/commands.js @@ -18,7 +18,7 @@ const argRegex = /(?:"[^"]*"|'[^']*'|[^,])+/g; export function containsCommand(message) { const commandMatch = message.match(commandRegex); if (commandMatch) - return commandName; + return "!" + commandMatch[1]; return null; } diff --git a/src/agent/examples.js b/src/agent/examples.js new file mode 100644 index 0000000..1f2a3e1 --- /dev/null +++ b/src/agent/examples.js @@ -0,0 +1,64 @@ +import { readFileSync } from 'fs'; +import { embed, cosineSimilarity } from '../utils/gpt.js'; +import { stringifyTurns } from '../utils/text.js'; + + +export class Examples { + constructor(select_num=3) { + this.examples = []; + this.select_num = select_num; + } + + async load(path) { + let examples = []; + try { + const data = readFileSync(path, 'utf8'); + examples = JSON.parse(data); + } catch (err) { + console.error('Examples failed to load!', err); + } + + this.examples = []; + for (let example of examples) { + let messages = ''; + for (let turn of example) { + if (turn.role != 'assistant') + messages += turn.content.substring(turn.content.indexOf(':')+1).trim() + '\n'; + } + messages = messages.trim(); + const embedding = await embed(messages); + this.examples.push({'embedding': embedding, 'turns': example}); + } + } + + async getRelevant(turns) { + let messages = ''; + for (let turn of turns) { + if (turn.role != 'assistant') + messages += turn.content.substring(turn.content.indexOf(':')+1).trim() + '\n'; + } + messages = messages.trim(); + const embedding = await embed(messages); + this.examples.sort((a, b) => { + return cosineSimilarity(b.embedding, embedding) - cosineSimilarity(a.embedding, embedding); + }); + let selected = this.examples.slice(0, this.select_num); + return JSON.parse(JSON.stringify(selected)); // deep copy + } + + async createExampleMessage(turns) { + let selected_examples = await this.getRelevant(turns); + + console.log('selected examples:'); + for (let example of selected_examples) { + console.log(example.turns[0]) + } + + let msg = 'Here are some examples of how to respond:\n'; + for (let i=0; i 0) { - let example_messages = 'Here are some examples of how to respond:\n'; - for (let example of this.selected_examples) { - example_messages += 'Example:\n' + stringifyTurns(example.turns) + '\n'; - } - return [{'role': 'system', 'content': example_messages}].concat(turns); + if (examples) { + let examples_msg = await examples.createExampleMessage(turns); + turns = examples_msg.concat(turns); } + return turns; } @@ -69,47 +63,6 @@ export class History { this.memory = await sendRequest(memory_turns, this.getSystemMessage()); } - async loadExamples() { - let examples = []; - try { - const data = readFileSync('./src/examples.json', 'utf8'); - examples = JSON.parse(data); - } catch (err) { - console.log('No history examples found.'); - } - - this.examples = []; - for (let example of examples) { - let messages = ''; - for (let turn of example) { - if (turn.role != 'assistant') - messages += turn.content.substring(turn.content.indexOf(':')+1).trim() + '\n'; - } - messages = messages.trim(); - const embedding = await embed(messages); - this.examples.push({'embedding': embedding, 'turns': example}); - } - - await this.setExamples(); - } - - async setExamples() { - let messages = ''; - for (let turn of this.turns) { - if (turn.role != 'assistant') - messages += turn.content.substring(turn.content.indexOf(':')+1).trim() + '\n'; - } - messages = messages.trim(); - const embedding = await embed(messages); - this.examples.sort((a, b) => { - return cosineSimilarity(a.embedding, embedding) - cosineSimilarity(b.embedding, embedding); - }); - this.selected_examples = this.examples.slice(-this.fewshot); - for (let example of this.selected_examples) { - console.log('selected example: ', example.turns[0].content); - } - } - async add(name, content) { let role = 'assistant'; if (name === 'system') { @@ -129,9 +82,6 @@ export class History { to_summarize.push(this.turns.shift()); await this.storeMemories(to_summarize); } - - if (role != 'assistant') - await this.setExamples(); } save() { diff --git a/src/examples_coder.json b/src/examples_coder.json index ad332f6..fd73422 100644 --- a/src/examples_coder.json +++ b/src/examples_coder.json @@ -1,52 +1,34 @@ [ - { - "conversation": [ - {"role": "user", "content": "greg: Collect 10 wood"}, - {"role": "assistant", "content": "Let me see what's nearby... !blocks"}, - {"role": "system", "content": "NEARBY_BLOCKS\n- oak_log\n- dirt\n- cobblestone"} - ], - "coder": [ - {"role": "assistant", "content": "I see some oak logs, dirt, and cobblestone. I'll collect oak logs.\n```\n// I am going to collect 10 oak_log.\nawait skills.collectBlock(bot, 'oak_log', 10);\n```"}, - {"role": "user", "content": "Code execution finished successfully."}, - {"role": "assistant", "content": "DONE"} - ] - }, - - { - "conversation": [ - {"role": "user", "content": "bobby: Craft a plank"}, - {"role": "assistant", "content": "Okay! !craftable"}, - {"role": "system", "content": "CRAFTABLE_ITEMS\n- spruce_planks\n- spruce_wood\n- ladder"} - ], - "coder": [ - {"role": "assistant", "content": "```\n// I am going to craft a spruce_planks.\nawait skills.craftRecipe(bot, 'spruce_planks');\n```"}, - {"role": "user", "content": "Code execution finished successfully."}, - {"role": "assistant", "content": "DONE"} - ] - }, - - { - "conversation": [ - {"role": "user", "content": "zZZn98: come here"} - ], - "coder": [ - {"role": "assistant", "content": "```\n// I am going to navigate to zZZn98.\nawait skills.goToPlayer(bot, 'zZZn98');\n```"}, - {"role": "user", "content": "Code execution finished successfully."}, - {"role": "assistant", "content": "DONE"} - ] - }, - - { - "conversation": [ - {"role": "user", "content": "maya: go to the nearest oak log"} - ], - "coder": [ - {"role": "assistant", "content": "```\n// I am navigating to the nearest oak log.\nawait skills.goToPosition(bot, nearestOakLog.x, nearestOakLog.y, nearestOakLog.z);\n```"}, - {"role": "system", "content": "Code output:\n!!Code threw exception!! Error: ReferenceError: nearestOakLog is not defined\n Write code to fix the problem and try again."}, - {"role": "assistant", "content": "Apologies, I made an error. Let me try that again.```\n// I am navigating to the nearest oak log.\nlet position = world.getNearestBlock(bot, 'oak_log', 20);\nawait skills.goToPosition(bot, position.x, position.y, position.z);\n```"}, - {"role": "user", "content": "Code execution finished successfully."}, - {"role": "assistant", "content": "DONE"} - ] - } - + [ + {"role": "user", "content": "greg: Collect 10 wood"}, + {"role": "assistant", "content": "```await skills.collectBlock(bot, 'oak_log', 10);\n```"}, + {"role": "system", "content": "Successfully collected 9 wood."}, + {"role": "assistant", "content": "I collected 9 oak logs, what next?"} + ], + [ + {"role": "user", "content": "bobby: Craft a plank"}, + {"role": "assistant", "content": "```\nawait skills.craftRecipe(bot, 'spruce_planks');\n```"}, + {"role": "system", "content": "Successfully crafted 4 spruce_planks."}, + {"role": "assistant", "content": "I crafted 4 spruce planks!"} + ], + [ + {"role": "user", "content": "zZZn98: come here"}, + {"role": "assistant", "content": "```await skills.goToPlayer(bot, 'zZZn98');\n```"}, + {"role": "system", "content": "Code execution finished successfully."}, + {"role": "assistant", "content": "Here!"} + ], + [ + {"role": "user", "content": "maya: go to the nearest oak log"}, + {"role": "assistant", "content": "```await skills.goToPosition(bot, nearestOakLog.x, nearestOakLog.y, nearestOakLog.z);\n```"}, + {"role": "system", "content": "Code output:\n!!Code threw exception!! Error: ReferenceError: nearestOakLog is not defined\n Write code to fix the problem and try again."}, + {"role": "assistant", "content": "Apologies, I made an error. Let me try that again.```\n// I am navigating to the nearest oak log.\nlet position = world.getNearestBlock(bot, 'oak_log', 20);\nawait skills.goToPosition(bot, position.x, position.y, position.z);\n```"}, + {"role": "system", "content": "Arrived at location."}, + {"role": "assistant", "content": "I found an oak log and I am now standing next to it. What next?"} + ], + [ + {"role": "user", "content": "234jeb: build a little tower"}, + {"role": "assistant", "content": "```let pos = bot.entity.position;\nfor (let i = 0; i < 5; i++) {\n await skills.placeBlock(bot, 'dirt', pos.x, pos.y + i, pos.z);\n}\n```"}, + {"role": "system", "content": "Successfully placed 5 dirt."}, + {"role": "assistant", "content": "I built a little tower!"} + ] ] \ No newline at end of file diff --git a/src/process/init-agent.js b/src/process/init-agent.js index 584f1bb..5eaadbb 100644 --- a/src/process/init-agent.js +++ b/src/process/init-agent.js @@ -20,4 +20,4 @@ const argv = yargs(args) }).argv const name = args[0]; -new Agent(name, argv.profile, argv.init_message); +new Agent().start(name, argv.profile, argv.init_message); From bcb244e025b3e2ba640a8234abcb6552a2132069 Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Mon, 15 Jan 2024 12:04:18 -0600 Subject: [PATCH 14/16] commands/newActions share execution wrapper --- src/agent/coder.js | 92 +++++++++++++++++++---------------- src/agent/commands/actions.js | 14 +++--- src/agent/examples.js | 2 +- 3 files changed, 56 insertions(+), 52 deletions(-) diff --git a/src/agent/coder.js b/src/agent/coder.js index 264f39b..33de9da 100644 --- a/src/agent/coder.js +++ b/src/agent/coder.js @@ -7,7 +7,6 @@ import { Examples } from './examples.js'; export class Coder { constructor(agent) { this.agent = agent; - this.queued_code = ''; this.current_code = ''; this.file_counter = 0; this.fp = '/bots/'+agent.name+'/action-code/'; @@ -28,8 +27,40 @@ export class Coder { mkdirSync('.' + this.fp, { recursive: true }); } - queueCode(code) { - this.queued_code = this.santitizeCode(code); + // write custom code to file and import it + async stageCode(code) { + code = this.santitizeCode(code); + let src = ''; + code = code.replaceAll('console.log(', 'log(bot,'); + code = code.replaceAll('log("', 'log(bot,"'); + + // this may cause problems in callback functions + code = code.replaceAll(';\n', '; if(bot.interrupt_code) {log(bot, "Code interrupted.");return;}\n'); + for (let line of code.split('\n')) { + src += ` ${line}\n`; + } + src = this.code_template.replace('/* CODE HERE */', src); + + console.log("writing to file...", src) + + 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) => { + // console.log("deleted file " + prev_filename); + // if (err) console.error(err); + // }); + // } commented for now, useful to keep files for debugging + this.file_counter++; + + let write_result = await this.writeFilePromise('.' + this.fp + filename, src) + + if (write_result) { + console.error('Error writing code execution file: ' + result); + return null; + } + this.current_code = code; + return await import('../..' + this.fp + filename); } santitizeCode(code) { @@ -92,8 +123,14 @@ export class Coder { } let code = res.substring(res.indexOf('```')+3, res.lastIndexOf('```')); - this.queueCode(code); - code_return = await this.execute(); + const execution_file = await this.stageCode(code); + if (!execution_file) { + agent_history.add('system', 'Failed to stage code, something is wrong.'); + return; + } + code_return = await this.execute(async ()=>{ + return await execution_file.main(this.agent.bot); + }); if (code_return.interrupted && !code_return.timedout) return; @@ -113,50 +150,19 @@ export class Coder { } // returns {success: bool, message: string, interrupted: bool, timedout: false} - async execute() { - if (!this.queued_code) return {success: false, message: "No code to execute.", interrupted: false, timedout: false}; + async execute(func, timeout=1) { if (!this.code_template) return {success: false, message: "Code template not loaded.", interrupted: false, timedout: false}; - let src = ''; - let code = this.queued_code; - code = code.replaceAll('console.log(', 'log(bot,'); - code = code.replaceAll('log("', 'log(bot,"'); - - // this may cause problems in callback functions - code = code.replaceAll(';\n', '; if(bot.interrupt_code) {log(bot, "Code interrupted.");return;}\n'); - for (let line of code.split('\n')) { - src += ` ${line}\n`; - } - src = this.code_template.replace('/* CODE HERE */', src); - - console.log("writing to file...", src) - - 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) => { - // console.log("deleted file " + prev_filename); - // if (err) console.error(err); - // }); - // } commented for now, useful to keep files for debugging - this.file_counter++; - - let write_result = await this.writeFilePromise('.' + this.fp + filename, src) - - if (write_result) { - console.error('Error writing code execution file: ' + result); - return {success: false, message: result, interrupted: false, timedout: false}; - } let TIMEOUT; try { console.log('executing code...\n'); - let execution_file = await import('../..' + this.fp + filename); await this.stop(); - this.current_code = this.queued_code; + this.clear(); this.executing = true; - TIMEOUT = this._startTimeout(10); - await execution_file.main(this.agent.bot); // open fire + if (timeout > 0) + TIMEOUT = this._startTimeout(timeout); + await func(); // open fire this.executing = false; clearTimeout(TIMEOUT); @@ -171,10 +177,11 @@ export class Coder { clearTimeout(TIMEOUT); console.error("Code execution triggered catch: " + err); + await this.stop(); let message = this.formatOutput(this.agent.bot); message += '!!Code threw exception!! Error: ' + err; let interrupted = this.agent.bot.interrupt_code; - await this.stop(); + this.clear(); this.agent.bot.emit("code_terminated"); return {success: false, message, interrupted, timedout: false}; } @@ -204,7 +211,6 @@ export class Coder { console.log('waiting for code to finish executing... interrupt:', this.agent.bot.interrupt_code); await new Promise(resolve => setTimeout(resolve, 1000)); } - this.clear(); } clear() { diff --git a/src/agent/commands/actions.js b/src/agent/commands/actions.js index 6f97838..84f4963 100644 --- a/src/agent/commands/actions.js +++ b/src/agent/commands/actions.js @@ -3,14 +3,12 @@ import * as world from '../world.js'; function wrapExecution(func) { return async function (agent, ...args) { - await agent.coder.stop(); - agent.bot.output = ''; - agent.coder.executing = true; - let res = await func(agent, ...args); - if (res) - agent.bot.output += '\n' + res; - agent.coder.executing = false; - return '\n' + agent.bot.output + '\n'; + let code_return = await agent.coder.execute(async () => { + await func(agent, ...args); + }, -1); // no timeout + if (code_return.interrupted && !code_return.timedout) + return; + return code_return.message; } } diff --git a/src/agent/examples.js b/src/agent/examples.js index 1f2a3e1..ee76c85 100644 --- a/src/agent/examples.js +++ b/src/agent/examples.js @@ -51,7 +51,7 @@ export class Examples { console.log('selected examples:'); for (let example of selected_examples) { - console.log(example.turns[0]) + console.log(example.turns[0].content) } let msg = 'Here are some examples of how to respond:\n'; From 84294898392eeddf8dfea947ada01dc96cc214ce Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Tue, 16 Jan 2024 15:52:52 -0600 Subject: [PATCH 15/16] added auto-equip armor --- package.json | 1 + src/utils/mcdata.js | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index ec6f905..e7699a0 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "dependencies": { "minecraft-data": "^3.46.2", "mineflayer": "^4.14.0", + "mineflayer-armor-manager": "^2.0.1", "mineflayer-auto-eat": "^3.3.6", "mineflayer-collectblock": "^1.4.1", "mineflayer-pathfinder": "^2.4.4", diff --git a/src/utils/mcdata.js b/src/utils/mcdata.js index 057d706..425f914 100644 --- a/src/utils/mcdata.js +++ b/src/utils/mcdata.js @@ -4,7 +4,8 @@ import { pathfinder } from 'mineflayer-pathfinder'; import { plugin as pvp } from 'mineflayer-pvp'; import { plugin as collectblock } from 'mineflayer-collectblock'; import { plugin as autoEat } from 'mineflayer-auto-eat'; - +import plugin from 'mineflayer-armor-manager'; +const armorManager = plugin; const mc_version = '1.19.3' const mcdata = minecraftData(mc_version); @@ -21,6 +22,7 @@ export function initBot(username) { bot.loadPlugin(pvp); bot.loadPlugin(collectblock); bot.loadPlugin(autoEat); + bot.loadPlugin(armorManager); // auto equip armor return bot; } From cf14b7ca6da0030759cb0d2fbe9045840c673790 Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Tue, 16 Jan 2024 15:53:27 -0600 Subject: [PATCH 16/16] added sleep,craft,place commands --- src/agent/coder.js | 2 +- src/agent/commands/actions.js | 29 +++++++++++++++++++++++++++++ src/agent/skills.js | 35 +++++++++++++++++++++++++++++++++-- src/examples.json | 8 ++++++-- 4 files changed, 69 insertions(+), 5 deletions(-) diff --git a/src/agent/coder.js b/src/agent/coder.js index 33de9da..1cbfe76 100644 --- a/src/agent/coder.js +++ b/src/agent/coder.js @@ -150,7 +150,7 @@ export class Coder { } // returns {success: bool, message: string, interrupted: bool, timedout: false} - async execute(func, timeout=1) { + async execute(func, timeout=10) { if (!this.code_template) return {success: false, message: "Code template not loaded.", interrupted: false, timedout: false}; let TIMEOUT; diff --git a/src/agent/commands/actions.js b/src/agent/commands/actions.js index 84f4963..76577b3 100644 --- a/src/agent/commands/actions.js +++ b/src/agent/commands/actions.js @@ -57,6 +57,28 @@ export const actionsList = [ await skills.collectBlock(agent.bot, type, num); }) }, + { + name: '!craftRecipe', + description: 'Craft the given recipe a given number of times. Ex: I will craft 8 sticks !craftRecipe("stick", 2)', + params: { + 'recipe_name': '(string) The name of the output item to craft.', + 'num': '(number) The number of times to craft the recipe. This is NOT the number of output items, as it may craft many more items depending on the recipe.' + }, + perform: wrapExecution(async (agent, recipe_name, num) => { + for (let i=0; i { + let pos = agent.bot.entity.position; + await skills.placeBlock(agent.bot, type, pos.x, pos.y, pos.z); + }) + }, { name: '!attack', description: 'Attack and kill the nearest entity of a given type.', @@ -72,5 +94,12 @@ export const actionsList = [ perform: wrapExecution(async (agent, player_name) => { await skills.defendPlayer(agent.bot, player_name); }) + }, + { + name: '!goToBed', + description: 'Go to the nearest bed and sleep.', + perform: wrapExecution(async (agent) => { + await skills.goToBed(agent.bot); + }) } ]; diff --git a/src/agent/skills.js b/src/agent/skills.js index a025822..1148728 100644 --- a/src/agent/skills.js +++ b/src/agent/skills.js @@ -13,7 +13,7 @@ export async function craftRecipe(bot, itemName) { * Attempt to craft the given item name from a recipe. May craft many items. * @param {MinecraftBot} bot, reference to the minecraft bot. * @param {string} itemName, the item name to craft. - * @returns {Promise} true if the item was crafted, false otherwise. + * @returns {Promise} true if the recipe was crafted, false otherwise. * @example * await skills.craftRecipe(bot, "stick"); **/ @@ -629,4 +629,35 @@ export async function defendPlayer(bot, username) { } return true; -} \ No newline at end of file +} + +export async function goToBed(bot) { + /** + * Sleep in the nearest bed. + * @param {MinecraftBot} bot, reference to the minecraft bot. + * @returns {Promise} true if the bed was found, false otherwise. + * @example + * await skills.goToBed(bot); + **/ + const beds = bot.findBlocks({ + matching: (block) => { + return block.name.includes('bed'); + }, + maxDistance: 32, + count: 1 + }); + if (beds.length === 0) { + log(bot, `Could not find a bed to sleep in.`); + return false; + } + let loc = beds[0]; + await goToPosition(bot, loc.x, loc.y, loc.z); + const bed = bot.blockAt(loc); + await bot.sleep(bed); + log(bot, `You are in bed.`); + while (bot.isSleeping) { + await new Promise(resolve => setTimeout(resolve, 500)); + } + log(bot, `You have woken up.`); + return true; +} diff --git a/src/examples.json b/src/examples.json index 9950928..6a73f41 100644 --- a/src/examples.json +++ b/src/examples.json @@ -22,7 +22,9 @@ {"role": "user", "content": "bobby: Craft a plank"}, {"role": "assistant", "content": "Okay! !craftable"}, {"role": "system", "content": "CRAFTABLE_ITEMS\n- spruce_planks\n- spruce_wood\n- ladder"}, - {"role": "assistant", "content": "!newAction"} + {"role": "assistant", "content": "!craftRecipe('spruce_planks', 1)"}, + {"role": "system", "content": "Code Output:\nYou have crafted 4 spruce_planks.\nCode execution finished successfully."}, + {"role": "assistant", "content": "I've crafted 4 spruce planks!"} ], [ @@ -65,6 +67,8 @@ [ {"role": "user", "content": "trixy88: craft some sticks"}, - {"role": "assistant", "content": "!newAction"} + {"role": "assistant", "content": "!craftRecipe('stick', 4)"}, + {"role": "system", "content": "Code Output:\nYou have crafted 16 sticks.\nCode execution finished successfully."}, + {"role": "assistant", "content": "I've crafted 16 sticks!"} ] ] \ No newline at end of file