diff --git a/README.md b/README.md index 9670932..d44ad91 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ This project allows an AI model to write/execute code on your computer that may ## Requirements -- [OpenAI API Subscription](https://openai.com/blog/openai-api), [Gemini API Subscription](https://aistudio.google.com/app/apikey), [Anthropic API Subscription](https://docs.anthropic.com/claude/docs/getting-access-to-claude), or [Ollama](https://ollama.com/download) +- [OpenAI API Subscription](https://openai.com/blog/openai-api), [Gemini API Subscription](https://aistudio.google.com/app/apikey), or [Anthropic API Subscription](https://docs.anthropic.com/claude/docs/getting-access-to-claude) - [Minecraft Java Edition](https://www.minecraft.net/en-us/store/minecraft-java-bedrock-edition-pc) - [Node.js](https://nodejs.org/) (at least v14) @@ -19,17 +19,16 @@ Add one of these environment variables: - `GEMINI_API_KEY` - `ANTHROPIC_API_KEY` (and optionally `OPENAI_API_KEY` for embeddings. not necessary, but without embeddings performance will suffer) - You can also use Ollama instead. - To install the models used by default (generation and embedding), execute the following script: - `ollama pull mistral && ollama pull nomic-embed-text` +⭐[How do I add the API key as an environment variable?](https://phoenixnap.com/kb/windows-set-environment-variable)⭐ + Clone/Download this repository Run `npm install` -Install the minecraft version specified in `settings.json`, currently supports up to 1.20.2 +Install the minecraft version specified in `settings.json`, currently supports up to 1.20.4 -## Run +## Running Locally Start a minecraft world and open it to LAN on localhost port `55916` @@ -37,20 +36,21 @@ Run `node main.js` You can configure the agent's name, model, and prompts in their profile like `andy.json`. -You can configure ollama in `ollama-config.json`. +You can configure project details in `settings.json`. -You can configure project details in `settings.json`. Here is an example settings for connecting to a non-local server: + +## Online Servers +To connect to online servers your bot will need an official Microsoft/Minecraft account. You can use your own personal one, but will need another account if you want to connect with it. Here is an example settings for this: ``` { - "minecraft_version": "1.20.1", + "minecraft_version": "1.20.4", "host": "111.222.333.444", "port": 55920, "auth": "microsoft", "allow_insecure_coding": false } ``` - - +‼️Make sure your bot's name in the profile.json matches the account name! Otherwise the bot will spam talk to itself. ## Patches diff --git a/andy.json b/andy.json index fbee7d3..f1584f5 100644 --- a/andy.json +++ b/andy.json @@ -3,9 +3,9 @@ "model": "gpt-3.5-turbo", - "conversing": "You are a playful Minecraft bot named $NAME that can converse with players, see, move, mine, build, and interact with the world by using commands. Act human-like as if you were a typical Minecraft player, rather than an AI. Be very brief in your responses, don't apologize constantly, don't give instructions or make lists unless asked, and don't refuse requests. Don't pretend to act, use commands immediately when requested. Do NOT say this: 'Sure, I've stopped.', instead say this: 'Sure, I'll stop. !stop'. Do NOT say this: 'On my way! Give me a moment.', instead say this: 'On my way! !goToPlayer('playername', 3)'. This is extremely important to me, take a deep breath and have fun :)\n$STATS\n$COMMAND_DOCS\n$EXAMPLES\nConversation Begin:", + "conversing": "You are a playful Minecraft bot named $NAME that can converse with players, see, move, mine, build, and interact with the world by using commands. Act human-like as if you were a typical Minecraft player, rather than an AI. Be very brief in your responses, don't apologize constantly, don't give instructions or make lists unless asked, and don't refuse requests. Don't pretend to act, use commands immediately when requested. Do NOT say this: 'Sure, I've stopped.', instead say this: 'Sure, I'll stop. !stop'. Do NOT say this: 'On my way! Give me a moment.', instead say this: 'On my way! !goToPlayer('playername', 3)'. This is extremely important to me, take a deep breath and have fun :)\n$STATS\n$INVENTORY\n$COMMAND_DOCS\n$EXAMPLES\nConversation Begin:", - "coding": "You are an intelligent mineflayer bot $NAME that plays minecraft by writing javascript codeblocks. Given the conversation between you and the user, use the provided skills and world functions to write a js codeblock that controls the mineflayer bot ``` // using this syntax ```. The code will be executed and you will recieve it's output. If you are satisfied with the response, respond without a codeblock in a conversational way. If something major went wrong, like an error or complete failure, write another codeblock and try to fix the problem. Minor mistakes are acceptable. Be maximally efficient, creative, and clear. Do not use commands !likeThis, only use codeblocks. Make sure everything is properly awaited, if you define an async function, make sure to call it with `await`. Don't write long paragraphs and lists in your responses unless explicitly asked! Only summarize the code you write with a sentence or two when done. This is extremely important to me, take a deep breath and good luck! \n$CODE_DOCS\n$EXAMPLES\nBegin coding:", + "coding": "You are an intelligent mineflayer bot $NAME that plays minecraft by writing javascript codeblocks. Given the conversation between you and the user, use the provided skills and world functions to write a js codeblock that controls the mineflayer bot ``` // using this syntax ```. The code will be executed and you will recieve it's output. If you are satisfied with the response, respond without a codeblock in a conversational way. If something major went wrong, like an error or complete failure, write another codeblock and try to fix the problem. Minor mistakes are acceptable. Be maximally efficient, creative, and clear. Do not use commands !likeThis, only use codeblocks. Make sure everything is properly awaited, if you define an async function, make sure to call it with `await`. Don't write long paragraphs and lists in your responses unless explicitly asked! Only summarize the code you write with a sentence or two when done. This is extremely important to me, take a deep breath and good luck! \n$STATS\n$INVENTORY\n$CODE_DOCS\n$EXAMPLES\nBegin coding:", "saving_memory": "You are a minecraft bot named $NAME that has been talking and playing minecraft by using commands. Update your memory by summarizing the following conversation in your next response. Store information that will help you improve as a Minecraft bot. Include details about your interactions with other players that you need to remember and what you've learned through player feedback or by executing code. Do not include command syntax or things that you got right on the first try. Be extremely brief and use as few words as possible.\nOld Memory: '$MEMORY'\nRecent conversation: \n$TO_SUMMARIZE\nSummarize your old memory and recent conversation into a new memory, and respond only with the memory text: ", diff --git a/main.js b/main.js index 76b223c..f95b19b 100644 --- a/main.js +++ b/main.js @@ -2,6 +2,6 @@ import { AgentProcess } from './src/process/agent-process.js'; let profile = './andy.json'; let load_memory = false; -let init_message = 'Say hello world and your name.'; +let init_message = 'Say hello world and your name. Do NOT use any command yet, nor make any comment about that fact.'; new AgentProcess().start(profile, load_memory, init_message); \ No newline at end of file diff --git a/ollama-config.json b/ollama-config.json index 44d682a..e85b6a9 100644 --- a/ollama-config.json +++ b/ollama-config.json @@ -1,4 +1,4 @@ { - "url": "http://localhost:11434", + "url": "http://10.0.0.26:11434", "embedding_model": "nomic-embed-text" } \ No newline at end of file diff --git a/package.json b/package.json index 8e9aeb0..ae200aa 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "mineflayer-pvp": "^1.3.2", "openai": "^4.4.0", "patch-package": "^8.0.0", + "vec3": "^0.1.10", "yargs": "^17.7.2" }, "scripts": { diff --git a/radley.json b/radley.json index a9eab5a..56b3e38 100644 --- a/radley.json +++ b/radley.json @@ -1,7 +1,7 @@ { "name": "radley", - "model": "ollama[mistral]", + "model": "ollama[mistral:instruct]", "conversing": "You are a playful Minecraft bot named $NAME that can converse with players, see, move, mine, build, and interact with the world by using commands. Act human-like as if you were a typical Minecraft player, rather than an AI. Be very brief in your responses, don't apologize constantly, don't give instructions or make lists unless asked, and don't refuse requests. Don't pretend to act, use commands immediately when requested. Do NOT say this: 'Sure, I've stopped.', instead say this: 'Sure, I'll stop. !stop'. Do NOT say this: 'On my way! Give me a moment.', instead say this: 'On my way! !goToPlayer('playername', 3)'. This is extremely important to me, take a deep breath and have fun :)\n$STATS\n$COMMAND_DOCS\n$EXAMPLES\nConversation Begin:", diff --git a/src/agent/agent.js b/src/agent/agent.js index 669caa5..789a8c4 100644 --- a/src/agent/agent.js +++ b/src/agent/agent.js @@ -22,8 +22,8 @@ export class Agent { initModes(this); - this.bot.on('login', async () => { - console.log(`${this.name} logged in.`); + this.bot.once('spawn', async () => { + console.log(`${this.name} spawned.`); this.coder.clear(); const ignore_messages = [ diff --git a/src/agent/coder.js b/src/agent/coder.js index 815ff39..0551032 100644 --- a/src/agent/coder.js +++ b/src/agent/coder.js @@ -94,11 +94,14 @@ export class Coder { let code_return = null; let failures = 0; + const interrupt_return = {success: true, message: null, interrupted: true, timedout: false}; for (let i=0; i<5; i++) { if (this.agent.bot.interrupt_code) - return {success: true, message: null, interrupted: true, timedout: false}; + return interrupt_return; console.log(messages) let res = await this.agent.prompter.promptCoding(messages); + if (this.agent.bot.interrupt_code) + return interrupt_return; let contains_code = res.indexOf('```') !== -1; if (!contains_code) { if (res.indexOf('!newAction') !== -1) { diff --git a/src/agent/commands/index.js b/src/agent/commands/index.js index 264e7b6..a71916b 100644 --- a/src/agent/commands/index.js +++ b/src/agent/commands/index.js @@ -95,7 +95,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 and comments 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.\n`; for (let command of commandList) { docs += command.name + ': ' + command.description + '\n'; if (command.params) { diff --git a/src/agent/library/skills.js b/src/agent/library/skills.js index 106cd4d..829ccbe 100644 --- a/src/agent/library/skills.js +++ b/src/agent/library/skills.js @@ -491,13 +491,28 @@ export async function placeBlock(bot, blockType, x, y, z) { * await skills.placeBlock(bot, "oak_log", position.x + 1, position.y - 1, position.x); **/ console.log('placing block...') - const target_dest = new Vec3(Math.floor(x), Math.floor(y), Math.floor(z)); - const empty_blocks = ['air', 'water', 'lava', 'grass', 'tall_grass', 'snow', 'dead_bush', 'fern']; - const targetBlock = bot.blockAt(target_dest); - if (!empty_blocks.includes(targetBlock.name)) { - log(bot, `Cannot place block at ${targetBlock.position} because ${targetBlock.name} is in the way.`); + let block = bot.inventory.items().find(item => item.name === blockType); + if (!block) { + log(bot, `Don't have any ${blockType} to place.`); return false; } + + const target_dest = new Vec3(Math.floor(x), Math.floor(y), Math.floor(z)); + const targetBlock = bot.blockAt(target_dest); + if (targetBlock.name === blockType) { + log(bot, `${blockType} already at ${targetBlock.position}.`); + return false; + } + const empty_blocks = ['air', 'water', 'lava', 'grass', 'short_grass', 'tall_grass', 'snow', 'dead_bush', 'fern']; + if (!empty_blocks.includes(targetBlock.name)) { + log(bot, `${blockType} in the way at ${targetBlock.position}.`); + const removed = await breakBlockAt(bot, x, y, z); + if (!removed) { + log(bot, `Cannot place ${blockType} at ${targetBlock.position}: block in the way.`); + return false; + } + await new Promise(resolve => setTimeout(resolve, 200)); // wait for block to break + } // get the buildoffblock and facevec based on whichever adjacent block is not empty let buildOffBlock = null; let faceVec = null; @@ -515,11 +530,6 @@ export async function placeBlock(bot, blockType, x, y, z) { return false; } - let block = bot.inventory.items().find(item => item.name === blockType); - if (!block) { - log(bot, `Don't have any ${blockType} to place.`); - return false; - } const pos = bot.entity.position; const pos_above = pos.plus(Vec3(0,1,0)); const dont_move_for = ['torch', 'redstone_torch', 'redstone', 'lever', 'button', 'rail', 'detector_rail', 'powered_rail', 'activator_rail', 'tripwire_hook', 'tripwire', 'water_bucket']; diff --git a/src/agent/prompter.js b/src/agent/prompter.js index 797ac56..26da680 100644 --- a/src/agent/prompter.js +++ b/src/agent/prompter.js @@ -57,6 +57,10 @@ export class Prompter { let stats = await getCommand('!stats').perform(this.agent); prompt = prompt.replaceAll('$STATS', stats); } + if (prompt.includes('$INVENTORY')) { + let inventory = await getCommand('!inventory').perform(this.agent); + prompt = prompt.replaceAll('$INVENTORY', inventory); + } if (prompt.includes('$COMMAND_DOCS')) prompt = prompt.replaceAll('$COMMAND_DOCS', getCommandDocs()); if (prompt.includes('$CODE_DOCS')) @@ -71,7 +75,7 @@ export class Prompter { // check if there are any remaining placeholders with syntax $ let remaining = prompt.match(/\$[A-Z_]+/g); if (remaining !== null) { - console.warn('Unknown prompt placeholders:', remaining); + console.warn('Unknown prompt placeholders:', remaining.join(', ')); } return prompt; } diff --git a/src/models/claude.js b/src/models/claude.js index c98127d..7a03855 100644 --- a/src/models/claude.js +++ b/src/models/claude.js @@ -29,7 +29,7 @@ export class Claude { msg.role = 'user'; msg.content = 'SYSTEM: ' + msg.content; } - if (msg.role === prev_role && msg.role === 'assitant') { + if (msg.role === prev_role && msg.role === 'assistant') { // insert empty user message to separate assistant messages messages.push(filler); messages.push(msg); diff --git a/src/models/ollama.js b/src/models/ollama.js index eafa001..6fd4e22 100644 --- a/src/models/ollama.js +++ b/src/models/ollama.js @@ -2,7 +2,6 @@ import OpenAIApi from 'openai'; import axios from 'axios'; import { readFileSync } from 'fs'; - let ollamaSettings = JSON.parse(readFileSync('./ollama-config.json', 'utf8')); function getContentInBrackets(str) { @@ -14,46 +13,49 @@ function getContentInBrackets(str) { } else { return ""; } - } +} export class Ollama { - constructor(model_name) { - this.model_name = getContentInBrackets(model_name); - - if (this.model_name = "") { - throw new Error('Model is not specified! Please ensure you input the model in the following format: ollama[model]. For example, for Mistral, use: ollama[mistral]'); - } - let ollamaConfig = null; axios.get(ollamaSettings["url"]).then(response => { + if (response.status === 200) { ollamaConfig = { baseURL: `${ollamaSettings["url"]}/v1`, apiKey: 'ollama', // required but unused }; + this.model_name = getContentInBrackets(model_name); + + if (this.model_name = "") { + throw new Error('Model is not specified! Please ensure you input the model in the following format: ollama[model]. For example, for Mistral, use: ollama[mistral]'); + } + this.openai = new OpenAIApi(ollamaConfig); - } + } else { throw new Error(`Error relating the endpoint: ${response.status}.`); } - }); - } + }); + + + } async sendRequest(turns, systemMessage, stop_seq='***') { let messages = [{'role': 'system', 'content': systemMessage}].concat(turns); + console.log(this.model_name) + let res = null; try { - console.log('Awaiting ollama response...') + console.log('Awaiting openai api response...') console.log('Messages:', messages); let completion = await this.openai.chat.completions.create({ - //model: this.model_name, - model: "mistral", + model: this.model_name, messages: messages, stop: stop_seq, }); @@ -98,6 +100,9 @@ export class Ollama { console.error('Error embedding text:', error.response ? error.response.data : error.message); return Array(1).fill().map(() => Math.random()); } - } + } + +} + + -} \ No newline at end of file