From 825c369e8d000b52e2b4e1d4abd6a4d9180558b2 Mon Sep 17 00:00:00 2001 From: Kolby Nottingham Date: Thu, 2 May 2024 22:32:16 -0700 Subject: [PATCH 01/19] always load npc --- src/agent/commands/actions.js | 1 - src/agent/npc/controller.js | 3 +-- src/agent/npc/data.js | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/agent/commands/actions.js b/src/agent/commands/actions.js index 2a84982..23b401b 100644 --- a/src/agent/commands/actions.js +++ b/src/agent/commands/actions.js @@ -207,7 +207,6 @@ export const actionsList = [ 'quantity': '(number) The quantity of the goal to set. Default is 1.' }, perform: async function (agent, name=null, quantity=1) { - if (!agent.npc.data) return 'NPC module is not loaded.'; await agent.npc.setGoal(name, quantity); return 'Set goal: ' + agent.npc.data.curr_goal.name; } diff --git a/src/agent/npc/controller.js b/src/agent/npc/controller.js index 7636dcc..84770cd 100644 --- a/src/agent/npc/controller.js +++ b/src/agent/npc/controller.js @@ -39,8 +39,6 @@ export class NPCContoller { } init() { - if (this.data === null) return; - for (let file of readdirSync('src/agent/npc/construction')) { if (file.endsWith('.json')) { try { @@ -68,6 +66,7 @@ export class NPCContoller { } this.agent.bot.on('idle', async () => { + if (this.data.goals.length === 0 && !this.data.curr_goal) return; // Wait a while for inputs before acting independently await new Promise((resolve) => setTimeout(resolve, 5000)); if (!this.agent.isIdle()) return; diff --git a/src/agent/npc/data.js b/src/agent/npc/data.js index b590d15..6d12f0a 100644 --- a/src/agent/npc/data.js +++ b/src/agent/npc/data.js @@ -24,8 +24,8 @@ export class NPCData { } static fromObject(obj) { - if (!obj) return null; let npc = new NPCData(); + if (!obj) return npc; if (obj.goals) { npc.goals = []; for (let goal of obj.goals) { From 74156d118d8f30d15355248aa058ae5554aa4e19 Mon Sep 17 00:00:00 2001 From: Kolby Nottingham Date: Sat, 4 May 2024 14:41:02 -0700 Subject: [PATCH 02/19] trigger goal to start --- src/agent/commands/actions.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/agent/commands/actions.js b/src/agent/commands/actions.js index 02c1fbf..12ccacb 100644 --- a/src/agent/commands/actions.js +++ b/src/agent/commands/actions.js @@ -230,6 +230,7 @@ export const actionsList = [ }, perform: async function (agent, name=null, quantity=1) { await agent.npc.setGoal(name, quantity); + agent.bot.emit('idle'); // to trigger the goal return 'Set goal: ' + agent.npc.data.curr_goal.name; } } From 453de3535f9c857ace9e8dba9a56bbabb96c9e6b Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Mon, 6 May 2024 00:18:04 -0500 Subject: [PATCH 03/19] added replicate api --- package.json | 1 + src/agent/prompter.js | 7 ++++ src/models/replicate.js | 81 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+) create mode 100644 src/models/replicate.js diff --git a/package.json b/package.json index ae200aa..247c2dc 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", + "replicate": "^0.29.4", "vec3": "^0.1.10", "yargs": "^17.7.2" }, diff --git a/src/agent/prompter.js b/src/agent/prompter.js index b895727..5e6a412 100644 --- a/src/agent/prompter.js +++ b/src/agent/prompter.js @@ -8,6 +8,7 @@ import { getCommand } from './commands/index.js'; import { Gemini } from '../models/gemini.js'; import { GPT } from '../models/gpt.js'; import { Claude } from '../models/claude.js'; +import { ReplicateAPI } from '../models/replicate.js'; import { Local } from '../models/local.js'; @@ -28,6 +29,8 @@ export class Prompter { chat.api = 'openai'; else if (chat.model.includes('claude')) chat.api = 'anthropic'; + else if (chat.model.includes('meta/') || chat.model.includes('mistralai/') || chat.model.includes('replicate/')) + chat.api = 'replicate'; else chat.api = 'ollama'; } @@ -40,6 +43,8 @@ export class Prompter { this.chat_model = new GPT(chat.model, chat.url); else if (chat.api == 'anthropic') this.chat_model = new Claude(chat.model, chat.url); + else if (chat.api == 'replicate') + this.chat_model = new ReplicateAPI(chat.model, chat.url); else if (chat.api == 'ollama') this.chat_model = new Local(chat.model, chat.url); else @@ -57,6 +62,8 @@ export class Prompter { this.embedding_model = new Gemini(embedding.model, embedding.url); else if (embedding.api == 'openai') this.embedding_model = new GPT(embedding.model, embedding.url); + else if (embedding.api == 'replicate') + this.embedding_model = new ReplicateAPI(embedding.model, embedding.url); else if (embedding.api == 'ollama') this.embedding_model = new Local(embedding.model, embedding.url); else { diff --git a/src/models/replicate.js b/src/models/replicate.js new file mode 100644 index 0000000..8ff22b4 --- /dev/null +++ b/src/models/replicate.js @@ -0,0 +1,81 @@ +import Replicate from 'replicate'; + +// llama, mistral +export class ReplicateAPI { + constructor(model_name, url) { + this.model_name = model_name; + this.url = url; + + if (!process.env.REPLICATE_API_KEY) { + throw new Error('Replicate API key missing! Make sure you set your REPLICATE_API_KEY environment variable.'); + } + + this.replicate = new Replicate({ + auth: process.env.REPLICATE_API_KEY, + }); + } + + async sendRequest(turns, systemMessage) { + if (this.url) { + + } + + let prev_role = null; + let messages = []; + let filler = { role: 'user', content: '_' }; + + for (let msg of turns) { + if (msg.role === 'system') { + msg.role = 'user'; + msg.content = 'SYSTEM: ' + msg.content; + } + if (msg.role === prev_role && msg.role === 'assistant') { + // insert empty user message to separate assistant messages + messages.push(filler); + messages.push(msg); + } else if (msg.role === prev_role) { + // combine new message with previous message instead of adding a new one + messages[messages.length - 1].content += '\n' + msg.content; + } else { + messages.push(msg); + } + prev_role = msg.role; + } + + const prompt = '\n\n' + messages.map(msg => `${msg.role}: ${msg.content}`).join('\n'); + const input = { + prompt: prompt, + top_p: 0.95, + prompt_template: "<|begin_of_text|><|start_header_id|>system<|end_header_id|>\n\n"+systemMessage+"<|eot_id|><|start_header_id|>user<|end_header_id|>\n\n{prompt}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n", + presence_penalty: 0, + frequency_penalty: 0 + }; + + + let res = null; + try { + console.log('Awaiting Replicate API response...'); + console.log('Input:', input); + let result = ''; + for await (const event of this.replicate.stream(this.model_name, { input })) { + result += event; + } + console.log('Received.'); + res = result; + } catch (err) { + console.log(err); + res = 'My brain disconnected, try again.'; + } + + return res; + } + "You are a playful Minecraft bot named andy 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 sto…ll automatically choose a goal.\nquantity: (number) The quantity of the goal to set. Default is 1.\n*\n\nExamples of how to respond:\nExample 1:\nUser input: miner_32: Hey! What are you up to?\nYour output:\nNothing much miner_32, what do you need?\n\nExample 2:\nUser input: grombo_Xx: What do you see?\nYour output:\nLet me see... !nearbyBlocks\nSystem output: NEARBY_BLOCKS\n- oak_log\n- dirt\n- cobblestone\nYour output:\nI see some oak logs, dirt, and cobblestone.\n\n\nConversation Begin:\n\nuser: SYSTEM: SAY HELLO." + + async embed(text) { + const output = await this.replicate.run( + this.model_name || "mark3labs/embeddings-gte-base:d619cff29338b9a37c3d06605042e1ff0594a8c3eff0175fd6967f5643fc4d47", + { input: {text} } + ); + return output; + } +} \ No newline at end of file From 7ac919a62f36b29fa1ddc10392a44e6d8ccb7189 Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Tue, 7 May 2024 15:06:05 -0500 Subject: [PATCH 04/19] fixed prompter mutating history, improved coding --- andy.json | 2 +- src/agent/coder.js | 3 ++- src/agent/history.js | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/andy.json b/andy.json index 70d7355..d4c5fc9 100644 --- a/andy.json +++ b/andy.json @@ -5,7 +5,7 @@ "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. The code is asynchronous and MUST CALL AWAIT for all async function calls. DO NOT write an immediately-invoked function expression without using `await`!! DO NOT WRITE LIKE THIS: ```(async () => {console.log('not properly awaited')})();``` 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:", + "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. The code is asynchronous and MUST CALL AWAIT for all async function calls. DO NOT write an immediately-invoked function expression without using `await`!! DO NOT WRITE LIKE THIS: ```(async () => {console.log('not properly awaited')})();``` 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\nConversation:", "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/src/agent/coder.js b/src/agent/coder.js index 0a97144..7ba66e3 100644 --- a/src/agent/coder.js +++ b/src/agent/coder.js @@ -91,6 +91,7 @@ export class Coder { async generateCodeLoop(agent_history) { let messages = agent_history.getHistory(); + messages.push({role: 'system', content: 'Code generation started. Write code in codeblock in your response:'}); let code_return = null; let failures = 0; @@ -99,7 +100,7 @@ export class Coder { if (this.agent.bot.interrupt_code) return interrupt_return; console.log(messages) - let res = await this.agent.prompter.promptCoding(messages); + let res = await this.agent.prompter.promptCoding(JSON.parse(JSON.stringify(messages))); if (this.agent.bot.interrupt_code) return interrupt_return; let contains_code = res.indexOf('```') !== -1; diff --git a/src/agent/history.js b/src/agent/history.js index 659113b..b276823 100644 --- a/src/agent/history.js +++ b/src/agent/history.js @@ -22,7 +22,7 @@ export class History { async storeMemories(turns) { console.log("Storing memories..."); - this.memory = await this.agent.prompter.promptMemSaving(this.memory, turns); + this.memory = await this.agent.prompter.promptMemSaving(this.getHistory(), turns); console.log("Memory updated to: ", this.memory); } From e4187787900f49d65cd2801e8daf10f65cd9361a Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Tue, 7 May 2024 15:08:22 -0500 Subject: [PATCH 05/19] improved replicate, fixed gemini, shared toSinglePrompt --- src/models/gemini.js | 21 +++---- src/models/helper.js | 14 +++++ src/models/replicate.js | 122 +++++++++++++++++----------------------- 3 files changed, 75 insertions(+), 82 deletions(-) create mode 100644 src/models/helper.js diff --git a/src/models/gemini.js b/src/models/gemini.js index c27d34e..504e3f6 100644 --- a/src/models/gemini.js +++ b/src/models/gemini.js @@ -1,5 +1,5 @@ import { GoogleGenerativeAI } from '@google/generative-ai'; - +import { toSinglePrompt } from './helper.js'; export class Gemini { constructor(model_name, url) { @@ -13,6 +13,7 @@ export class Gemini { } async sendRequest(turns, systemMessage) { + let model; if (this.url) { model = this.genAI.getGenerativeModel( {model: this.model_name || "gemini-pro"}, @@ -24,23 +25,19 @@ export class Gemini { ); } - const messages = [{'role': 'system', 'content': systemMessage}].concat(turns); - let prompt = ""; - let role = ""; - messages.forEach((message) => { - role = message.role; - if (role === 'assistant') role = 'model'; - prompt += `${role}: ${message.content}\n`; - }); - if (role !== "model") // if the last message was from the user/system, add a prompt for the model. otherwise, pretend we are extending the model's own message - prompt += "model: "; + const stop_seq = '***'; + const prompt = toSinglePrompt(turns, systemMessage, stop_seq, 'model'); console.log(prompt) const result = await model.generateContent(prompt); const response = await result.response; - return response.text(); + const text = response.text(); + if (!text.includes(stop_seq)) return text; + const idx = text.indexOf(stop_seq); + return text.slice(0, idx); } async embed(text) { + let model; if (this.url) { model = this.genAI.getGenerativeModel( {model: this.model_name || "embedding-001"}, diff --git a/src/models/helper.js b/src/models/helper.js new file mode 100644 index 0000000..7b45fe1 --- /dev/null +++ b/src/models/helper.js @@ -0,0 +1,14 @@ +export function toSinglePrompt(turns, system=null, stop_seq='***', model_nickname='assistant') { + let messages = turns; + if (system) messages.unshift({role: 'system', content: system}); + let prompt = ""; + let role = ""; + messages.forEach((message) => { + role = message.role; + if (role === 'assistant') role = model_nickname; + prompt += `${role}: ${message.content}${stop_seq}`; + }); + if (role !== model_nickname) // if the last message was from the user/system, add a prompt for the model. otherwise, pretend we are extending the model's own message + prompt += model_nickname + ": "; + return prompt; +} diff --git a/src/models/replicate.js b/src/models/replicate.js index 8ff22b4..d9f8382 100644 --- a/src/models/replicate.js +++ b/src/models/replicate.js @@ -1,81 +1,63 @@ import Replicate from 'replicate'; +import { toSinglePrompt } from './helper.js'; // llama, mistral export class ReplicateAPI { - constructor(model_name, url) { - this.model_name = model_name; - this.url = url; + constructor(model_name, url) { + this.model_name = model_name; + this.url = url; - if (!process.env.REPLICATE_API_KEY) { - throw new Error('Replicate API key missing! Make sure you set your REPLICATE_API_KEY environment variable.'); - } + if (this.url) { + console.warn('Replicate API does not support custom URLs. Ignoring provided URL.'); + } - this.replicate = new Replicate({ - auth: process.env.REPLICATE_API_KEY, - }); - } + if (!process.env.REPLICATE_API_KEY) { + throw new Error('Replicate API key missing! Make sure you set your REPLICATE_API_KEY environment variable.'); + } - async sendRequest(turns, systemMessage) { - if (this.url) { - - } + this.replicate = new Replicate({ + auth: process.env.REPLICATE_API_KEY, + }); + } - let prev_role = null; - let messages = []; - let filler = { role: 'user', content: '_' }; + async sendRequest(turns, systemMessage) { + const stop_seq = '***'; + let prompt_template; + const prompt = toSinglePrompt(turns, systemMessage, stop_seq); + if (this.model_name.includes('llama')) { // llama + prompt_template = "<|begin_of_text|><|start_header_id|>system<|end_header_id|>\n\n<|eot_id|><|start_header_id|>user<|end_header_id|>\n\n{prompt}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n" + } + else { // mistral + prompt_template = "[INST] {prompt} [/INST] " + } - for (let msg of turns) { - if (msg.role === 'system') { - msg.role = 'user'; - msg.content = 'SYSTEM: ' + msg.content; - } - if (msg.role === prev_role && msg.role === 'assistant') { - // insert empty user message to separate assistant messages - messages.push(filler); - messages.push(msg); - } else if (msg.role === prev_role) { - // combine new message with previous message instead of adding a new one - messages[messages.length - 1].content += '\n' + msg.content; - } else { - messages.push(msg); - } - prev_role = msg.role; - } + const input = { prompt, prompt_template }; + let res = null; + try { + console.log('Awaiting Replicate API response...'); + let result = ''; + for await (const event of this.replicate.stream(this.model_name, { input })) { + result += event; + if (result === '') break; + if (result.includes(stop_seq)) { + result = result.slice(0, result.indexOf(stop_seq)); + break; + } + } + res = result; + } catch (err) { + console.log(err); + res = 'My brain disconnected, try again.'; + } + console.log('Received.'); + return res; + } - const prompt = '\n\n' + messages.map(msg => `${msg.role}: ${msg.content}`).join('\n'); - const input = { - prompt: prompt, - top_p: 0.95, - prompt_template: "<|begin_of_text|><|start_header_id|>system<|end_header_id|>\n\n"+systemMessage+"<|eot_id|><|start_header_id|>user<|end_header_id|>\n\n{prompt}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n", - presence_penalty: 0, - frequency_penalty: 0 - }; - - - let res = null; - try { - console.log('Awaiting Replicate API response...'); - console.log('Input:', input); - let result = ''; - for await (const event of this.replicate.stream(this.model_name, { input })) { - result += event; - } - console.log('Received.'); - res = result; - } catch (err) { - console.log(err); - res = 'My brain disconnected, try again.'; - } - - return res; - } - "You are a playful Minecraft bot named andy 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 sto…ll automatically choose a goal.\nquantity: (number) The quantity of the goal to set. Default is 1.\n*\n\nExamples of how to respond:\nExample 1:\nUser input: miner_32: Hey! What are you up to?\nYour output:\nNothing much miner_32, what do you need?\n\nExample 2:\nUser input: grombo_Xx: What do you see?\nYour output:\nLet me see... !nearbyBlocks\nSystem output: NEARBY_BLOCKS\n- oak_log\n- dirt\n- cobblestone\nYour output:\nI see some oak logs, dirt, and cobblestone.\n\n\nConversation Begin:\n\nuser: SYSTEM: SAY HELLO." - - async embed(text) { - const output = await this.replicate.run( - this.model_name || "mark3labs/embeddings-gte-base:d619cff29338b9a37c3d06605042e1ff0594a8c3eff0175fd6967f5643fc4d47", - { input: {text} } - ); - return output; - } + async embed(text) { + const output = await this.replicate.run( + this.model_name || "mark3labs/embeddings-gte-base:d619cff29338b9a37c3d06605042e1ff0594a8c3eff0175fd6967f5643fc4d47", + { input: {text} } + ); + return output.vectors; + } } \ No newline at end of file From 0bd92f7521bba9cab72041d971d0fe305d902bcd Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Fri, 10 May 2024 13:41:29 -0500 Subject: [PATCH 06/19] refactored and added defaul replicate --- src/models/gemini.js | 3 +-- src/models/helper.js | 14 -------------- src/models/replicate.js | 7 ++++--- src/utils/text.js | 15 +++++++++++++++ 4 files changed, 20 insertions(+), 19 deletions(-) delete mode 100644 src/models/helper.js diff --git a/src/models/gemini.js b/src/models/gemini.js index 504e3f6..61f4f1a 100644 --- a/src/models/gemini.js +++ b/src/models/gemini.js @@ -1,5 +1,5 @@ import { GoogleGenerativeAI } from '@google/generative-ai'; -import { toSinglePrompt } from './helper.js'; +import { toSinglePrompt } from '../utils/text.js'; export class Gemini { constructor(model_name, url) { @@ -27,7 +27,6 @@ export class Gemini { const stop_seq = '***'; const prompt = toSinglePrompt(turns, systemMessage, stop_seq, 'model'); - console.log(prompt) const result = await model.generateContent(prompt); const response = await result.response; const text = response.text(); diff --git a/src/models/helper.js b/src/models/helper.js deleted file mode 100644 index 7b45fe1..0000000 --- a/src/models/helper.js +++ /dev/null @@ -1,14 +0,0 @@ -export function toSinglePrompt(turns, system=null, stop_seq='***', model_nickname='assistant') { - let messages = turns; - if (system) messages.unshift({role: 'system', content: system}); - let prompt = ""; - let role = ""; - messages.forEach((message) => { - role = message.role; - if (role === 'assistant') role = model_nickname; - prompt += `${role}: ${message.content}${stop_seq}`; - }); - if (role !== model_nickname) // if the last message was from the user/system, add a prompt for the model. otherwise, pretend we are extending the model's own message - prompt += model_nickname + ": "; - return prompt; -} diff --git a/src/models/replicate.js b/src/models/replicate.js index d9f8382..4301448 100644 --- a/src/models/replicate.js +++ b/src/models/replicate.js @@ -1,5 +1,5 @@ import Replicate from 'replicate'; -import { toSinglePrompt } from './helper.js'; +import { toSinglePrompt } from '../utils/text.js'; // llama, mistral export class ReplicateAPI { @@ -24,7 +24,8 @@ export class ReplicateAPI { const stop_seq = '***'; let prompt_template; const prompt = toSinglePrompt(turns, systemMessage, stop_seq); - if (this.model_name.includes('llama')) { // llama + let model_name = this.model_name || 'meta/meta-llama-3-70b-instruct'; + if (model_name.includes('llama')) { // llama prompt_template = "<|begin_of_text|><|start_header_id|>system<|end_header_id|>\n\n<|eot_id|><|start_header_id|>user<|end_header_id|>\n\n{prompt}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n" } else { // mistral @@ -36,7 +37,7 @@ export class ReplicateAPI { try { console.log('Awaiting Replicate API response...'); let result = ''; - for await (const event of this.replicate.stream(this.model_name, { input })) { + for await (const event of this.replicate.stream(model_name, { input })) { result += event; if (result === '') break; if (result.includes(stop_seq)) { diff --git a/src/utils/text.js b/src/utils/text.js index d06221a..c075d50 100644 --- a/src/utils/text.js +++ b/src/utils/text.js @@ -11,4 +11,19 @@ export function stringifyTurns(turns) { } } return res.trim(); +} + +export function toSinglePrompt(turns, system=null, stop_seq='***', model_nickname='assistant') { + let messages = turns; + if (system) messages.unshift({role: 'system', content: system}); + let prompt = ""; + let role = ""; + messages.forEach((message) => { + role = message.role; + if (role === 'assistant') role = model_nickname; + prompt += `${role}: ${message.content}${stop_seq}`; + }); + if (role !== model_nickname) // if the last message was from the user/system, add a prompt for the model. otherwise, pretend we are extending the model's own message + prompt += model_nickname + ": "; + return prompt; } \ No newline at end of file From 017aaa2779898f8b9027719f1cdef00cc4e19331 Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Fri, 10 May 2024 13:41:58 -0500 Subject: [PATCH 07/19] clean up, added replicate --- README.md | 50 ++++++++++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 7d6cf78..3566f6a 100644 --- a/README.md +++ b/README.md @@ -8,16 +8,21 @@ 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 Installed](https://ollama.com/download) +- [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), [Replicate API Subscription](https://replicate.com/) or [Ollama Installed](https://ollama.com/download) - [Minecraft Java Edition](https://www.minecraft.net/en-us/store/minecraft-java-bedrock-edition-pc) - [Node.js](https://nodejs.org/) (at least v14) ## Installation -Add one of these environment variables: - - `OPENAI_API_KEY` (and optionally `OPENAI_ORG_ID`) - - `GEMINI_API_KEY` - - `ANTHROPIC_API_KEY` (and optionally `OPENAI_API_KEY` for embeddings. not necessary, but without embeddings performance will suffer) +Add the environment variable for the model you want to use: + +| API | Env Variable | Example Model name | Docs | +|------|------|------|------| +| OpenAI | `OPENAI_API_KEY` | `gpt-3.5-turbo` | [docs](https://platform.openai.com/docs/models) | (optionally add `OPENAI_ORG_ID`) +| Google | `GEMINI_API_KEY` | `gemini-pro` | [docs](https://ai.google.dev/gemini-api/docs/models/gemini) | +| Anthropic | `ANTHROPIC_API_KEY` | `claude-3-haiku-20240307` | [docs](https://docs.anthropic.com/claude/docs/models-overview) | +| Replicate | `REPLICATE_API_KEY` | `meta/meta-llama-3-70b-instruct` | [docs](https://replicate.com/collections/language-models) | +| Ollama (local) | n/a | `llama3` | [docs](https://ollama.com/library) | ⭐[How do I add the API key as an environment variable?](https://phoenixnap.com/kb/windows-set-environment-variable)⭐ @@ -30,7 +35,7 @@ Run `npm install` Install the minecraft version specified in `settings.json`, currently supports up to 1.20.4 -## Running Locally +### Running Locally Start a minecraft world and open it to LAN on localhost port `55916` @@ -40,13 +45,27 @@ You can configure the agent's name, model, and prompts in their profile like `an You can configure project details in `settings.json`. -## Bot Profiles +### 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.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. -Bot profiles are json files (such as `andy.json`) that define a bot's behavior in three ways: +### Bot Profiles + +Bot profiles are json files (such as `andy.json`) that define: 1. Bot backend LLMs to use for chat and embeddings. 2. Prompts used to influence the bot's behavior. -3. Examples retrieved and provided to the bot to help it better perform tasks. +3. Examples help the bot perform tasks. + ### Model Specifications @@ -84,19 +103,6 @@ Thus, all the below specifications are equivalent to the above example: "embedding": "openai" ``` -## 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.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 Some of the node modules that we depend on have bugs in them. To add a patch, change your local node module file and run `npx patch-package [package-name]` From 3ac742395327b01d19028794fb5fbc3c174d91d0 Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Sat, 11 May 2024 12:31:43 -0500 Subject: [PATCH 08/19] sneak in patch for creative mode pathfinding --- patches/mineflayer-pathfinder+2.4.5.patch | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 patches/mineflayer-pathfinder+2.4.5.patch diff --git a/patches/mineflayer-pathfinder+2.4.5.patch b/patches/mineflayer-pathfinder+2.4.5.patch new file mode 100644 index 0000000..8c6765e --- /dev/null +++ b/patches/mineflayer-pathfinder+2.4.5.patch @@ -0,0 +1,17 @@ +diff --git a/node_modules/mineflayer-pathfinder/lib/movements.js b/node_modules/mineflayer-pathfinder/lib/movements.js +index a7e3505..77e428f 100644 +--- a/node_modules/mineflayer-pathfinder/lib/movements.js ++++ b/node_modules/mineflayer-pathfinder/lib/movements.js +@@ -143,7 +143,11 @@ class Movements { + for (const id of this.scafoldingBlocks) { + for (const j in items) { + const item = items[j] +- if (item.type === id) count += item.count ++ if (item.type === id) { ++ count += item.count ++ if (this.bot.game.gameMode === 'creative') ++ count = 1000 ++ } + } + } + return count From dc520a9ea10bbe95a91b98188bcc467ec707b278 Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Sun, 12 May 2024 22:46:33 -0500 Subject: [PATCH 09/19] fix unloaded blocks in mode check --- src/agent/modes.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/agent/modes.js b/src/agent/modes.js index ba170fa..aed2fc4 100644 --- a/src/agent/modes.js +++ b/src/agent/modes.js @@ -26,13 +26,15 @@ const modes = [ const bot = agent.bot; const block = bot.blockAt(bot.entity.position); const blockAbove = bot.blockAt(bot.entity.position.offset(0, 1, 0)); - if (blockAbove && (blockAbove.name === 'water' || blockAbove.name === 'flowing_water')) { + if (!block) block = {name: 'air'}; // hacky fix when blocks are not loaded + if (!blockAbove) blockAbove = {name: 'air'}; + if (blockAbove.name === 'water' || blockAbove.name === 'flowing_water') { // does not call execute so does not interrupt other actions if (!bot.pathfinder.goal) { bot.setControlState('jump', true); } } - else if (blockAbove && this.fall_blocks.some(name => blockAbove.name.includes(name))) { + else if (this.fall_blocks.some(name => blockAbove.name.includes(name))) { execute(this, agent, async () => { await skills.moveAway(bot, 2); }); From c579366e7a5b1f663c66d9eef3ff78dd0816f265 Mon Sep 17 00:00:00 2001 From: Kolby Nottingham Date: Mon, 13 May 2024 11:47:35 -0700 Subject: [PATCH 10/19] change npc defaults --- src/agent/npc/controller.js | 5 ++++- src/agent/npc/data.js | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/agent/npc/controller.js b/src/agent/npc/controller.js index 84770cd..d65107c 100644 --- a/src/agent/npc/controller.js +++ b/src/agent/npc/controller.js @@ -80,12 +80,15 @@ export class NPCContoller { } async setGoal(name=null, quantity=1) { + this.data.curr_goal = null; this.last_goals = {}; if (name) { this.data.curr_goal = {name: name, quantity: quantity}; return; } - + + if (!this.data.do_set_goal) return; + let past_goals = {...this.last_goals}; for (let goal in this.data.goals) { if (past_goals[goal.name] === undefined) past_goals[goal.name] = true; diff --git a/src/agent/npc/data.js b/src/agent/npc/data.js index 6d12f0a..b5de0eb 100644 --- a/src/agent/npc/data.js +++ b/src/agent/npc/data.js @@ -4,8 +4,8 @@ export class NPCData { this.curr_goal = null; this.built = {}; this.home = null; - this.do_routine = true; - this.do_set_goal = true; + this.do_routine = false; + this.do_set_goal = false; } toObject() { From 5c6de46882b9f20808313cc1f3f736c6421bb5e2 Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Tue, 14 May 2024 21:05:09 -0500 Subject: [PATCH 11/19] added pathfinder patch --- patches/mineflayer-pathfinder+2.4.5.patch | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 patches/mineflayer-pathfinder+2.4.5.patch diff --git a/patches/mineflayer-pathfinder+2.4.5.patch b/patches/mineflayer-pathfinder+2.4.5.patch new file mode 100644 index 0000000..2b268d5 --- /dev/null +++ b/patches/mineflayer-pathfinder+2.4.5.patch @@ -0,0 +1,17 @@ +diff --git a/node_modules/mineflayer-pathfinder/lib/movements.js b/node_modules/mineflayer-pathfinder/lib/movements.js +index a7e3505..77e428f 100644 +--- a/node_modules/mineflayer-pathfinder/lib/movements.js ++++ b/node_modules/mineflayer-pathfinder/lib/movements.js +@@ -143,7 +143,11 @@ class Movements { + for (const id of this.scafoldingBlocks) { + for (const j in items) { + const item = items[j] +- if (item.type === id) count += item.count ++ if (item.type === id) { ++ count += item.count ++ if (this.bot.game.gameMode === 'creative') ++ count = 1000 ++ } + } + } + return count \ No newline at end of file From b4a60cb11a83772eeaaffa54c8f913f24b3dfe03 Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Tue, 14 May 2024 21:05:21 -0500 Subject: [PATCH 12/19] fix unloaded blocks crash --- src/agent/modes.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/agent/modes.js b/src/agent/modes.js index ba170fa..aed2fc4 100644 --- a/src/agent/modes.js +++ b/src/agent/modes.js @@ -26,13 +26,15 @@ const modes = [ const bot = agent.bot; const block = bot.blockAt(bot.entity.position); const blockAbove = bot.blockAt(bot.entity.position.offset(0, 1, 0)); - if (blockAbove && (blockAbove.name === 'water' || blockAbove.name === 'flowing_water')) { + if (!block) block = {name: 'air'}; // hacky fix when blocks are not loaded + if (!blockAbove) blockAbove = {name: 'air'}; + if (blockAbove.name === 'water' || blockAbove.name === 'flowing_water') { // does not call execute so does not interrupt other actions if (!bot.pathfinder.goal) { bot.setControlState('jump', true); } } - else if (blockAbove && this.fall_blocks.some(name => blockAbove.name.includes(name))) { + else if (this.fall_blocks.some(name => blockAbove.name.includes(name))) { execute(this, agent, async () => { await skills.moveAway(bot, 2); }); From 537b1da2bc8da143c8eeaa07d01ef6fab02b0844 Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Tue, 14 May 2024 21:05:39 -0500 Subject: [PATCH 13/19] add creative msg to stats/inventory --- src/agent/commands/queries.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/agent/commands/queries.js b/src/agent/commands/queries.js index 99f5fa9..0669d36 100644 --- a/src/agent/commands/queries.js +++ b/src/agent/commands/queries.js @@ -17,10 +17,11 @@ export const queryList = [ 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- Gamemode: ${bot.game.gameMode}`; res += `\n- Health: ${Math.round(bot.health)} / 20`; res += `\n- Hunger: ${Math.round(bot.food)} / 20`; res += `\n- Biome: ${world.getBiomeName(bot)}`; - let weather = "clear"; + let weather = "Clear"; if (bot.rainState > 0) weather = "Rain"; if (bot.thunderState > 0) @@ -60,6 +61,9 @@ export const queryList = [ if (res == 'INVENTORY') { res += ': none'; } + if (agent.bot.game.gameMode === 'creative') { + res += '\n(You have infinite items in creative mode)'; + } return pad(res); } }, From cb253f03b69531ee29cef6de4449dd06fa5ebde1 Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Tue, 14 May 2024 21:24:41 -0500 Subject: [PATCH 14/19] don't check tools for digging in creative --- src/agent/library/skills.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/agent/library/skills.js b/src/agent/library/skills.js index 4889357..56f31f1 100644 --- a/src/agent/library/skills.js +++ b/src/agent/library/skills.js @@ -463,11 +463,13 @@ export async function breakBlockAt(bot, x, y, z) { bot.pathfinder.setMovements(movements); await bot.pathfinder.goto(new pf.goals.GoalNear(pos.x, pos.y, pos.z, 4)); } - await bot.tool.equipForBlock(block); - const itemId = bot.heldItem ? bot.heldItem.type : null - if (!block.canHarvest(itemId)) { - log(bot, `Don't have right tools to break ${block.name}.`); - return false; + if (bot.gameMode !== 'creative') { + await bot.tool.equipForBlock(block); + const itemId = bot.heldItem ? bot.heldItem.type : null + if (!block.canHarvest(itemId)) { + log(bot, `Don't have right tools to break ${block.name}.`); + return false; + } } await bot.dig(block, true); log(bot, `Broke ${block.name} at x:${x.toFixed(1)}, y:${y.toFixed(1)}, z:${z.toFixed(1)}.`); From a9b64950ca683caff588719cbd63e1144e0a60a7 Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Tue, 14 May 2024 21:34:14 -0500 Subject: [PATCH 15/19] init prompt as system --- src/utils/text.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/utils/text.js b/src/utils/text.js index c075d50..b0a273a 100644 --- a/src/utils/text.js +++ b/src/utils/text.js @@ -14,11 +14,9 @@ export function stringifyTurns(turns) { } export function toSinglePrompt(turns, system=null, stop_seq='***', model_nickname='assistant') { - let messages = turns; - if (system) messages.unshift({role: 'system', content: system}); - let prompt = ""; - let role = ""; - messages.forEach((message) => { + let prompt = system ? `${system}${stop_seq}` : ''; + let role = ''; + turns.forEach((message) => { role = message.role; if (role === 'assistant') role = model_nickname; prompt += `${role}: ${message.content}${stop_seq}`; From 988c541c8742da84b5dd1fe5a37a8b703b619fa8 Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Tue, 14 May 2024 22:57:14 -0500 Subject: [PATCH 16/19] kolby suggestion --- src/agent/commands/queries.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/agent/commands/queries.js b/src/agent/commands/queries.js index 0669d36..aa60b0b 100644 --- a/src/agent/commands/queries.js +++ b/src/agent/commands/queries.js @@ -58,10 +58,10 @@ export const queryList = [ if (inventory[item] && inventory[item] > 0) res += `\n- ${item}: ${inventory[item]}`; } - if (res == 'INVENTORY') { + if (res === 'INVENTORY') { res += ': none'; } - if (agent.bot.game.gameMode === 'creative') { + else if (agent.bot.game.gameMode === 'creative') { res += '\n(You have infinite items in creative mode)'; } return pad(res); From e0177badc5d5cf69db4772ad5fe20b265bf8efd9 Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Wed, 15 May 2024 10:19:24 -0500 Subject: [PATCH 17/19] improved replicate params --- src/models/replicate.js | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/models/replicate.js b/src/models/replicate.js index 4301448..ea5a4a9 100644 --- a/src/models/replicate.js +++ b/src/models/replicate.js @@ -22,17 +22,10 @@ export class ReplicateAPI { async sendRequest(turns, systemMessage) { const stop_seq = '***'; - let prompt_template; - const prompt = toSinglePrompt(turns, systemMessage, stop_seq); + const prompt = toSinglePrompt(turns, null, stop_seq); let model_name = this.model_name || 'meta/meta-llama-3-70b-instruct'; - if (model_name.includes('llama')) { // llama - prompt_template = "<|begin_of_text|><|start_header_id|>system<|end_header_id|>\n\n<|eot_id|><|start_header_id|>user<|end_header_id|>\n\n{prompt}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n" - } - else { // mistral - prompt_template = "[INST] {prompt} [/INST] " - } - const input = { prompt, prompt_template }; + const input = { prompt, system_prompt: systemMessage }; let res = null; try { console.log('Awaiting Replicate API response...'); From b45f49aefdca2ef21b13750572a2d688c8f6660f Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Sat, 18 May 2024 12:06:50 -0500 Subject: [PATCH 18/19] check creative --- src/agent/library/skills.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/agent/library/skills.js b/src/agent/library/skills.js index 56f31f1..cd9f5b3 100644 --- a/src/agent/library/skills.js +++ b/src/agent/library/skills.js @@ -463,7 +463,7 @@ export async function breakBlockAt(bot, x, y, z) { bot.pathfinder.setMovements(movements); await bot.pathfinder.goto(new pf.goals.GoalNear(pos.x, pos.y, pos.z, 4)); } - if (bot.gameMode !== 'creative') { + if (bot.game.gameMode !== 'creative') { await bot.tool.equipForBlock(block); const itemId = bot.heldItem ? bot.heldItem.type : null if (!block.canHarvest(itemId)) { From 5454252dde206369b3dcf42f8a496bcc14c29b9f Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Mon, 20 May 2024 00:52:08 -0500 Subject: [PATCH 19/19] save memory before restart --- src/agent/agent.js | 11 +++++++++-- src/agent/coder.js | 17 +++-------------- src/agent/commands/actions.js | 2 +- src/process/agent-process.js | 2 +- 4 files changed, 14 insertions(+), 18 deletions(-) diff --git a/src/agent/agent.js b/src/agent/agent.js index fe44a25..4581166 100644 --- a/src/agent/agent.js +++ b/src/agent/agent.js @@ -170,7 +170,7 @@ export class Agent { }); this.bot.on('end', (reason) => { console.warn('Bot disconnected! Killing agent process.', reason) - process.exit(1); + this.cleanKill('Bot disconnected! Killing agent process.'); }); this.bot.on('death', () => { this.coder.cancelResume(); @@ -178,7 +178,7 @@ export class Agent { }); this.bot.on('kicked', (reason) => { console.warn('Bot kicked!', reason); - process.exit(1); + this.cleanKill('Bot kicked! Killing agent process.'); }); this.bot.on('messagestr', async (message, _, jsonMsg) => { if (jsonMsg.translate && jsonMsg.translate.startsWith('death') && message.startsWith(this.name)) { @@ -215,4 +215,11 @@ export class Agent { isIdle() { return !this.coder.executing && !this.coder.generating; } + + cleanKill(msg='Killing agent process...') { + this.history.add('system', msg); + this.bot.chat('Goodbye world.') + this.history.save(); + process.exit(1); + } } diff --git a/src/agent/coder.js b/src/agent/coder.js index 7ba66e3..c26d4d0 100644 --- a/src/agent/coder.js +++ b/src/agent/coder.js @@ -240,7 +240,7 @@ export class Coder { console.log('waiting for code to finish executing...'); await new Promise(resolve => setTimeout(resolve, 1000)); if (Date.now() - start > 10 * 1000) { - process.exit(1); // force exit program after 10 seconds of failing to stop + this.agent.cleanKill('Code execution refused stop after 10 seconds. Killing process.'); } } } @@ -255,19 +255,8 @@ export class Coder { return setTimeout(async () => { console.warn(`Code execution timed out after ${TIMEOUT_MINS} minutes. Attempting force stop.`); this.timedout = true; - this.agent.bot.output += `\nAction performed for ${TIMEOUT_MINS} minutes and then timed out and stopped. You may want to continue or do something else.`; - this.stop(); // last attempt to stop - await new Promise(resolve => setTimeout(resolve, 5 * 1000)); // wait 5 seconds - if (this.executing) { - console.error(`Failed to stop. Killing process. Goodbye.`); - this.agent.bot.output += `\nForce stop failed! Process was killed and will be restarted. Goodbye world.`; - this.agent.bot.chat('Goodbye world.'); - let output = this.formatOutput(this.agent.bot); - this.agent.history.add('system', output); - this.agent.history.save(); - process.exit(1); // force exit program - } - console.log('Code execution stopped successfully.'); + this.agent.history.add('system', `Code execution timed out after ${TIMEOUT_MINS} minutes. Attempting force stop.`); + await this.stop(); // last attempt to stop }, TIMEOUT_MINS*60*1000); } } \ No newline at end of file diff --git a/src/agent/commands/actions.js b/src/agent/commands/actions.js index 12ccacb..53cfd6b 100644 --- a/src/agent/commands/actions.js +++ b/src/agent/commands/actions.js @@ -45,7 +45,7 @@ export const actionsList = [ description: 'Restart the agent process.', perform: async function (agent) { await agent.history.save(); - process.exit(1); + agent.cleanKill(); } }, { diff --git a/src/process/agent-process.js b/src/process/agent-process.js index 8d8383d..21b6c2c 100644 --- a/src/process/agent-process.js +++ b/src/process/agent-process.js @@ -25,7 +25,7 @@ export class AgentProcess { process.exit(1); } console.log('Restarting agent...'); - this.start(profile, true, 'Agent process restarted. Notify the user and decide what to do.'); + this.start(profile, true, 'Agent process restarted.'); last_restart = Date.now(); } });