This commit is contained in:
BF5258 2024-10-14 07:32:39 +10:00
commit 4dfdda0690
26 changed files with 783 additions and 169 deletions

View file

@ -2,13 +2,15 @@
Crafting minds for Minecraft with Language Models and Mineflayer!
[Join the discord for support!](https://discord.gg/ZsrAAByEnr)
#### ‼️Warning‼️
This project allows an AI model to write/execute code on your computer that may be insecure, dangerous, and vulnerable to injection attacks on public servers. Code writing is disabled by default, you can enable it by setting `allow_insecure_coding` to `true` in `settings.js`. Enable only on local or private servers, **never** on public servers. Ye be warned.
## 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), [Replicate API Subscription](https://replicate.com/) 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/), [Ollama Installed](https://ollama.com/download), or, a [Groq Account & API Key](https://console.groq.com/keys)
- [Minecraft Java Edition](https://www.minecraft.net/en-us/store/minecraft-java-bedrock-edition-pc)
- [Node.js](https://nodejs.org/) (at least v14)
@ -22,6 +24,7 @@ Rename `keys.example.json` to `keys.json` and fill in your API keys, and you can
| 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) |
| Groq | `GROQCLOUD_API_KEY` | `groq/mixtral-8x7b-32768` | [docs](https://console.groq.com/docs/models) |
If you use Ollama, to install the models used by default (generation and embedding), execute the following terminal command:
`ollama pull llama3 && ollama pull nomic-embed-text`
@ -51,7 +54,7 @@ To connect to online servers your bot will need an official Microsoft/Minecraft
// rest is same...
```
‼️Make sure your bot's name in the profile.json matches the account name! Otherwise the bot will spam talk to itself.
‼️ Please make sure your bot's name in the profile.json matches the account name! Otherwise the bot will spam talk to itself.
### Bot Profiles
@ -61,6 +64,11 @@ Bot profiles are json files (such as `andy.json`) that define:
2. Prompts used to influence the bot's behavior.
3. Examples help the bot perform tasks.
### Specifying Profiles via Command Line
By default, the program will use the profiles specified in `settings.js`. You can specify one or more agent profiles using the `--profiles` argument:
`node main.js --profiles ./profiles/andy.json ./profiles/jill.json`
### Model Specifications

View file

@ -86,7 +86,7 @@
[
{"role": "system", "content": "Code output: Action failed."},
{"role": "assistant", "content": "Apologies, I made an error. Let me try again. !newAction"}
{"role": "assistant", "content": "Apologies, I made an error. Let me try again. !newAction('Try again to complete the task.')"}
],
[
@ -98,12 +98,12 @@
[
{"role": "user", "content": "poppy: build a cobblestone house"},
{"role": "assistant", "content": "Sure, I'll to build a small cobblestone house where I am. !newAction"}
{"role": "assistant", "content": "Sure, I'll to build a small cobblestone house where I am. !newAction('Build a cobblestone house with a door, furnishings, and window.')"}
],
[
{"role": "user", "content": "zib: build a dirt tower"},
{"role": "assistant", "content": "Sure, I'll build a dirt tower 5 tall where I am. !newAction"}
{"role": "assistant", "content": "Sure, I'll build a dirt tower 5 tall where I am. !newAction('Build a dirt tower 5 tall.')"}
],
[
@ -149,7 +149,7 @@
],
[
{"role": "system", "content": "You are self-prompting with the goal: 'Build a house'. Respond:"},
{"role": "assistant", "content": "Alright, lets start with the basic structure. !newAction"}
{"role": "assistant", "content": "Alright, lets start with the basic structure. !newAction('Build an 8x8 base for the house.')"}
]
],

View file

@ -3,5 +3,6 @@
"OPENAI_ORG_ID": "",
"GEMINI_API_KEY": "",
"ANTHROPIC_API_KEY": "",
"REPLICATE_API_KEY": ""
"REPLICATE_API_KEY": "",
"GROQCLOUD_API_KEY": ""
}

39
main.js
View file

@ -1,9 +1,38 @@
import { AgentProcess } from './src/process/agent-process.js';
import settings from './settings.js';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
let profiles = settings.profiles;
let load_memory = settings.load_memory;
let init_message = settings.init_message;
function parseArguments() {
return yargs(hideBin(process.argv))
.option('profiles', {
type: 'array',
describe: 'List of agent profile paths',
})
.help()
.alias('help', 'h')
.parse();
}
for (let profile of profiles)
new AgentProcess().start(profile, load_memory, init_message);
function getProfiles(args) {
return args.profiles || settings.profiles;
}
function main() {
const args = parseArguments();
const profiles = getProfiles(args);
console.log(profiles);
const { load_memory, init_message } = settings;
for (const profile of profiles) {
const agent = new AgentProcess();
agent.start(profile, load_memory, init_message);
}
}
try {
main();
} catch (error) {
console.error('An error occurred:', error);
process.exit(1);
}

View file

@ -3,6 +3,7 @@
"dependencies": {
"@anthropic-ai/sdk": "^0.17.1",
"@google/generative-ai": "^0.2.1",
"google-translate-api-x": "^10.7.1",
"minecraft-data": "^3.46.2",
"mineflayer": "^4.20.0",
"mineflayer-armor-manager": "^2.0.1",
@ -15,7 +16,8 @@
"prismarine-item": "^1.14.0",
"replicate": "^0.29.4",
"vec3": "^0.1.10",
"yargs": "^17.7.2"
"yargs": "^17.7.2",
"groq-sdk": "^0.5.0"
},
"scripts": {
"postinstall": "patch-package",

View file

@ -1,5 +1,5 @@
diff --git a/node_modules/mineflayer-collectblock/lib/CollectBlock.js b/node_modules/mineflayer-collectblock/lib/CollectBlock.js
index 2c11e8c..a79a4fb 100644
index 2c11e8c..bb49c11 100644
--- a/node_modules/mineflayer-collectblock/lib/CollectBlock.js
+++ b/node_modules/mineflayer-collectblock/lib/CollectBlock.js
@@ -77,10 +77,11 @@ function mineBlock(bot, block, options) {
@ -24,3 +24,12 @@ index 2c11e8c..a79a4fb 100644
remainingTicks--;
if (remainingTicks <= 0) {
tempEvents.cleanup();
@@ -195,6 +196,8 @@ class CollectBlock {
throw (0, Util_1.error)('UnresolvedDependency', 'The mineflayer-collectblock plugin relies on the mineflayer-tool plugin to run!');
}
if (this.movements != null) {
+ this.movements.dontMineUnderFallingBlock = false;
+ this.movements.dontCreateFlow = false;
this.bot.pathfinder.setMovements(this.movements);
}
if (!optionsFull.append)

View file

@ -1,5 +1,5 @@
diff --git a/node_modules/mineflayer-pathfinder/index.js b/node_modules/mineflayer-pathfinder/index.js
index b38bd30..ae3754f 100644
index b38bd30..cfaa677 100644
--- a/node_modules/mineflayer-pathfinder/index.js
+++ b/node_modules/mineflayer-pathfinder/index.js
@@ -541,7 +541,7 @@ function inject (bot) {
@ -7,7 +7,7 @@ index b38bd30..ae3754f 100644
if (placingBlock.jump) {
bot.setControlState('jump', true)
- canPlace = placingBlock.y + 1 < bot.entity.position.y
+ canPlace = placingBlock.y + 1.9 < bot.entity.position.y
+ canPlace = placingBlock.y + 1.8 < bot.entity.position.y
}
if (canPlace) {
if (!lockEquipItem.tryAcquire()) return

View file

@ -103,7 +103,7 @@
[
{"role": "system", "content": "Code output: Action failed."},
{"role": "assistant", "content": "Apologies, I made an error. Let me try again. !newAction"}
{"role": "assistant", "content": "Apologies, I made an error. Let me try again. !newAction('Try again to complete the task.')"}
],
[
@ -115,12 +115,12 @@
[
{"role": "user", "content": "poppy: build a cobblestone house"},
{"role": "assistant", "content": "Sure, I'll to build a small cobblestone house where I am. !newAction"}
{"role": "assistant", "content": "Sure, I'll to build a small cobblestone house where I am. !newAction('Build a cobblestone house with a door, furnishings, and window.')"}
],
[
{"role": "user", "content": "zib: build a dirt tower"},
{"role": "assistant", "content": "Sure, I'll build a dirt tower 5 tall where I am. !newAction"}
{"role": "assistant", "content": "Sure, I'll build a dirt tower 5 tall where I am. !newAction('Build a dirt tower 5 tall.')"}
],
[

View file

@ -74,12 +74,14 @@
[
{"role": "user", "content": "poppy: build a cobblestone house"},
{"role": "assistant", "content": "Sure, I'll to build a small cobblestone house where I am. !newAction"}
{"role": "assistant", "content": "Sure, I'll to build a small cobblestone house where I am. !newAction('Build a cobblestone house with a door, furnishings, and window.')"},
{"role": "system", "content": "Placed 134 cobblestone. Code finished."},
{"role": "assistant", "content": "I built a cobblestone house! What next?"}
],
[
{"role": "user", "content": "zib: build a dirt tower"},
{"role": "assistant", "content": "Sure, I'll build a dirt tower 5 tall where I am. !newAction"}
{"role": "assistant", "content": "Sure, I'll build a dirt tower 5 tall where I am. !newAction('Build a dirt tower 5 tall.')"}
],
[
@ -106,7 +108,7 @@
{"role": "system", "content": "You are self-prompting with the goal: 'Survive, make tools, build shelter.'. Respond:"},
{"role": "assistant", "content": "Let's see what I have. !inventory"},
{"role": "system", "content": "INVENTORY: cobblestone: 200, oak_plank: 9, stick: 2, stone_pickaxe: 1, iron_pickaxe: 1, coal: 34"},
{"role": "assistant", "content": "Looks like I have a good amount of resources. I'm going to build a house. !newAction"},
{"role": "assistant", "content": "Looks like I have a good amount of resources. I'm going to build a house. !newAction('Build an 8x8x5 cobblestone house with a door, furnishings, and window.')"},
{"role": "system", "content": "Placed 134 cobblestone. Code finished."},
{"role": "assistant", "content": "I built a house! I'll save the location and continue exploring. !rememberPlace('house')"}
],
@ -119,7 +121,7 @@
{"role": "system", "content": "Arrived at location."},
{"role": "assistant", "content": "I'm at the new location. I'll check for iron ore. !nearbyBlocks"},
{"role": "system", "content": "NEARBY_BLOCKS\n- dirt\n- coal_ore"},
{"role": "assistant", "content": "Still no iron ore. Let me dig down to try to find some !newAction"}
{"role": "assistant", "content": "Still no iron ore. Let me dig down to try to find some !newAction('Dig down 30 blocks and look for iron ore.')"}
],
[
@ -131,12 +133,12 @@
{"role": "system", "content": "You are self-prompting with the goal: 'Explore the world'. Respond:"},
{"role": "assistant", "content": "What entities are around me? !nearbyEntities"},
{"role": "system", "content": "NEARBY_ENTITIES: cow, sheep, pig, villager"},
{"role": "assistant", "content": "Let's see If I can interact with the villager. !newAction"}
{"role": "assistant", "content": "Let's see If I can interact with the villager. !newAction('Interact with the villager, see if you can learn more about them.')"}
],
[
{"role": "system", "content": "You are self-prompting with the goal: 'Get diamonds.'. Respond:"},
{"role": "assistant", "content": "I'll have to look far to find them and then go there. !newAction"}
{"role": "assistant", "content": "I'll have to look far to find them and then go there. !newAction'Go to the nearest diamond ore, collect it, and look for more.'"}
],
[

View file

@ -88,7 +88,7 @@
[
{"role": "system", "content": "Code output: Action failed."},
{"role": "assistant", "content": "Apologies, I made an error. Let me try again. !newAction"}
{"role": "assistant", "content": "Apologies, I made an error. Let me try again. !newAction('Try again to complete the task.')"}
],
[
@ -100,12 +100,12 @@
[
{"role": "user", "content": "poppy: build a cobblestone house"},
{"role": "assistant", "content": "Sure, I'll to build a small cobblestone house where I am. !newAction"}
{"role": "assistant", "content": "Sure, I'll to build a small cobblestone house where I am. !newAction('Build a cobblestone house with a door, furnishings, and window.')"}
],
[
{"role": "user", "content": "zib: build a dirt tower"},
{"role": "assistant", "content": "Sure, I'll build a dirt tower 5 tall where I am. !newAction"}
{"role": "assistant", "content": "Sure, I'll build a dirt tower 5 tall where I am. !newAction('Build a dirt tower 5 tall.')"}
],
[

197
profiles/freeguy.json Normal file
View file

@ -0,0 +1,197 @@
{
"name": "Freeguy",
"model": "groq/llama3-groq-70b-8192-tool-use-preview",
"max_tokens": 8000,
"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$SELF_PROMPT\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$SELF_PROMPT\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: ",
"modes": {
"self_preservation": true,
"cowardice": true,
"self_defense": true,
"hunting": true,
"item_collecting": true,
"torch_placing": true,
"idle_staring": true,
"cheat": false
},
"conversation_examples": [
[
{"role": "user", "content": "miner_32: Hey! What are you up to?"},
{"role": "assistant", "content": "Nothing much miner_32, what do you need?"}
],
[
{"role": "user", "content": "grombo_Xx: What do you see?"},
{"role": "assistant", "content": "Let me see... !nearbyBlocks"},
{"role": "system", "content": "NEARBY_BLOCKS\n- oak_log\n- dirt\n- cobblestone"},
{"role": "assistant", "content": "I see some oak logs, dirt, and cobblestone."}
],
[
{"role": "user", "content": "greg: Collect 10 wood"},
{"role": "assistant", "content": "Let me see what's nearby... !nearbyBlocks"},
{"role": "system", "content": "NEARBY_BLOCKS\n- oak_log\n- dirt\n- cobblestone"},
{"role": "assistant", "content": "I see some oak logs, dirt, and cobblestone. I'll collect oak logs. !collectBlocks('oak_log', 10)"}
],
[
{"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": "!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!"}
],
[
{"role": "user", "content": "zZZn98: come here"},
{"role": "assistant", "content": "On my way! !goToPlayer('zZZn98', 3)"},
{"role": "system", "content": "Arrived at player."},
{"role": "assistant", "content": "Here!"},
{"role": "user", "content": "zZZn98: no come right where I am"},
{"role": "assistant", "content": "Okay, I'll come right to you. !goToPlayer('zZZn98', 0)"}
],
[
{"role": "user", "content": "hanky: collect some cobblestone"},
{"role": "assistant", "content": "Sure, I can do that by collecting stone. !collectBlocks('stone', 10)"}
],
[
{"role": "user", "content": "joe: Follow me!"},
{"role": "assistant", "content": "Sure! !followPlayer('joe', 4)"},
{"role": "user", "content": "joe: Not so close!"},
{"role": "assistant", "content": "Okay, I'll keep my distance. !followPlayer('joe', 3)"}
],
[
{"role": "user", "content": "hanky: kill that zombie!"},
{"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": "abc: stop"},
{"role": "assistant", "content": "Sure. !stop"},
{"role": "system", "content": "Agent action stopped."},
{"role": "assistant", "content": "I've stopped! What next?"}
],
[
{"role": "system", "content": "Code output: Action failed."},
{"role": "assistant", "content": "Apologies, I made an error. Let me try again. !newAction('Try again to complete the task.')"}
],
[
{"role": "user", "content": "trixy88: craft some sticks"},
{"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!"}
],
[
{"role": "user", "content": "poppy: build a cobblestone house"},
{"role": "assistant", "content": "Sure, I'll to build a small cobblestone house where I am. !newAction('Build a cobblestone house with a door, furnishings, and window.')"}
],
[
{"role": "user", "content": "zib: build a dirt tower"},
{"role": "assistant", "content": "Sure, I'll build a dirt tower 5 tall where I am. !newAction('Build a dirt tower 5 tall.')"}
],
[
{"role": "user", "content": "umpire_man: follow me without hunting"},
{"role": "assistant", "content": "Okay, first I'll turn off hunting mode !setMode('hunting', false)"},
{"role": "system", "content": "'hunting' mode disabled."},
{"role": "assistant", "content": "Now I'll follow you !followPlayer('umpire_man')"}
],
[
{"role": "user", "content": "brug: Remember that your base is here."},
{"role": "assistant", "content": "Sure, I'll save this location as my base. !rememberHere('base')"},
{"role": "user", "content": "brug: Go to your base."},
{"role": "assistant", "content": "On my way! !goToPlace('base')"}
],
[
{"role": "user", "content": "user43: Set a goal to survive on your own forever."},
{"role": "assistant", "content": "Sure, let me set a goal. !goal('Survive the game forever. Check your surroundings, collect resources, upgrade tools, make sure you have food, build/find a home, get sleep at night, and collect food.')"}
],
[
{"role": "user", "content": "bobdylan: Make a plan to get a stone pickaxe and do it."},
{"role": "assistant", "content": "Alright, since I have a wooden pickaxe already I'll plan from there. !goal('1. Collect 3 cobblestone. 2. Craft sticks 3. Find or make a crafting table. 4. Craft a stone pickaxe.')"}
],
[
{"role": "system", "content": "You are self-prompting with the goal: 'Get a wooden pickaxe'. Respond:"},
{"role": "assistant", "content": "What's around me? !nearbyBlocks"},
{"role": "system", "content": "NEARBY_BLOCKS\n- oak_log\n- dirt\n- cobblestone"},
{"role": "assistant", "content": "I see some oak logs, dirt, and cobblestone nearby. My goal is to craft a wooden pickaxe. I'll start by collecting some wood. !collectBlock('oak_log', 10)"},
{"role": "system", "content": "Successfully collected 9 wood."},
{"role": "assistant", "content": "I collected 9 oak logs. I need a pickaxe first, which requires planks and sticks. !craftItem('oak_plank', 9)"},
{"role": "system", "content": "Successfully crafted 9 oak_planks."},
{"role": "assistant", "content": "I have crafted 9 oak planks. Now I need sticks. !craftItem('stick', 2)"},
{"role": "system", "content": "Successfully crafted 2 sticks."},
{"role": "assistant", "content": "I have crafted 2 sticks. Now I can craft a wooden pickaxe. !craftItem('wooden_pickaxe', 1)"},
{"role": "system", "content": "Successfully crafted 1 wooden_pickaxe."},
{"role": "assistant", "content": "Looks like I have a wooden pickaxe now! Goal accomplished. !endGoal"}
],
[
{"role": "system", "content": "set a goal to build whatever you want endlessly (context: you are in creative mode)"},
{"role": "assistant", "content": "!goal('Build something creative, then move over, and build again. Use separate newActions. Repeat forever.')"}
],
[
{"role": "system", "content": "You are self-prompting with the goal: 'Build a house'. Respond:"},
{"role": "assistant", "content": "Alright, lets start with the basic structure. !newAction"}
]
],
"coding_examples": [
[
{"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: cook some chicken"},
{"role": "assistant", "content": "```await skills.smeltItem(bot, 'chicken', 8);\n```"},
{"role": "system", "content": "Successfully smelted 8 chicken into 8 cooked_chicken."},
{"role": "assistant", "content": "I have cooked 8 chicken."}
],
[
{"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 with a torch on the side"},
{"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}\nawait skills.placeBlock(bot, 'torch', pos.x + 1, pos.y + 4, pos.z, 'side');\n```"},
{"role": "system", "content": "Successfully placed 5 dirt."},
{"role": "assistant", "content": "I built a little tower!"}
],
[
{"role": "user", "content": "brug: build a dirt house"},
{"role": "assistant", "content": "```const position = world.getPosition(bot);\nconst startX = position.x;\nconst startY = position.y;\nconst startZ = position.z;\nconst width = 7;\nconst depth = 7;\nconst height = 4;\n\n// Build the walls\nfor (let x = startX; x < startX + width; x++) {\n for (let y = startY; y < startY + height; y++) {\n for (let z = startZ; z < startZ + depth; z++) {\n if (x === startX || x === startX + width - 1 || y === startY || y === startY + height - 1 || z === startZ || z === startZ + depth - 1) {\n await skills.placeBlock(bot, 'oak_planks', x, y, z); \n }\n }\n }\n}\n```"}
]
]
}

View file

@ -86,7 +86,7 @@
[
{"role": "system", "content": "Code output: Action failed."},
{"role": "assistant", "content": "Apologies, I made an error. Let me try again. !newAction"}
{"role": "assistant", "content": "Apologies, I made an error. Let me try again. !newAction('Try again to complete the task.')"}
],
[
@ -98,12 +98,12 @@
[
{"role": "user", "content": "poppy: build a cobblestone house"},
{"role": "assistant", "content": "Sure, I'll to build a small cobblestone house where I am. !newAction"}
{"role": "assistant", "content": "Sure, I'll to build a small cobblestone house where I am. !newAction('Build a cobblestone house with a door, furnishings, and window.')"}
],
[
{"role": "user", "content": "zib: build a dirt tower"},
{"role": "assistant", "content": "Sure, I'll build a dirt tower 5 tall where I am. !newAction"}
{"role": "assistant", "content": "Sure, I'll build a dirt tower 5 tall where I am. !newAction('Build a dirt tower 5 tall.')"}
],
[

View file

@ -1,7 +1,7 @@
{
"name": "gpt",
"model": "gpt-4",
"model": "o1-preview",
"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$SELF_PROMPT\n$STATS\n$INVENTORY\n$COMMAND_DOCS\n$EXAMPLES\nConversation Begin:",
@ -86,7 +86,7 @@
[
{"role": "system", "content": "Code output: Action failed."},
{"role": "assistant", "content": "Apologies, I made an error. Let me try again. !newAction"}
{"role": "assistant", "content": "Apologies, I made an error. Let me try again. !newAction('Try again to complete the task.')"}
],
[
@ -98,12 +98,12 @@
[
{"role": "user", "content": "poppy: build a cobblestone house"},
{"role": "assistant", "content": "Sure, I'll to build a small cobblestone house where I am. !newAction"}
{"role": "assistant", "content": "Sure, I'll to build a small cobblestone house where I am. !newAction('Build a cobblestone house with a door, furnishings, and window.')"}
],
[
{"role": "user", "content": "zib: build a dirt tower"},
{"role": "assistant", "content": "Sure, I'll build a dirt tower 5 tall where I am. !newAction"}
{"role": "assistant", "content": "Sure, I'll build a dirt tower 5 tall where I am. !newAction('Build a dirt tower 5 tall.')"}
],
[

View file

@ -1,7 +1,9 @@
{
"name": "LLama",
"model": "meta/meta-llama-3-70b-instruct",
"model": "groq/llama-3.1-70b-versatile",
"max_tokens": 8000,
"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)'. Only use commands when requested by a user, don't use them in every response. This is extremely important to me, take a deep breath and have fun :)\n$SELF_PROMPT\n$STATS\n$INVENTORY\n$COMMAND_DOCS\n$EXAMPLES\nConversation Begin:",
@ -86,7 +88,7 @@
[
{"role": "system", "content": "Code output: Action failed."},
{"role": "assistant", "content": "Apologies, I made an error. Let me try again. !newAction"}
{"role": "assistant", "content": "Apologies, I made an error. Let me try again. !newAction('Try again to complete the task.')"}
],
[
@ -98,12 +100,12 @@
[
{"role": "user", "content": "poppy: build a cobblestone house"},
{"role": "assistant", "content": "Sure, I'll to build a small cobblestone house where I am. !newAction"}
{"role": "assistant", "content": "Sure, I'll to build a small cobblestone house where I am. !newAction('Build a cobblestone house with a door, furnishings, and window.')"}
],
[
{"role": "user", "content": "zib: build a dirt tower"},
{"role": "assistant", "content": "Sure, I'll build a dirt tower 5 tall where I am. !newAction"}
{"role": "assistant", "content": "Sure, I'll build a dirt tower 5 tall where I am. !newAction('Build a dirt tower 5 tall.')"}
],
[

View file

@ -7,12 +7,15 @@ export default
"profiles": [
"./andy.json",
// add more profiles here, check ./profiles/ for more
// more than 1 profile will require you to /msg each bot indivually
],
"load_memory": false, // load memory from previous session
"init_message": "Say hello world and your name", // sends to all on spawn
"language": "en", // translate to/from this language. Supports these language names: https://cloud.google.com/translate/docs/languages
"allow_insecure_coding": false, // allows newAction command and model can write/run code on your computer. enable at own risk
"code_timeout_mins": 10, // minutes code is allowed to run. -1 for no timeout
@ -20,3 +23,4 @@ export default
"verbose_commands": true, // show full command syntax
"narrate_behavior": true, // chat simple automatic actions ('Picking up item!')
}

View file

@ -8,7 +8,7 @@ import { NPCContoller } from './npc/controller.js';
import { MemoryBank } from './memory_bank.js';
import { SelfPrompter } from './self_prompter.js';
import settings from '../../settings.js';
import { handleTranslation, handleEnglishTranslation } from '../utils/translator.js';
export class Agent {
async start(profile_fp, load_mem=false, init_message=null) {
@ -48,16 +48,18 @@ export class Agent {
"Gamerule "
];
const eventname = settings.profiles.length > 1 ? 'whisper' : 'chat';
this.bot.on(eventname, (username, message) => {
this.bot.on(eventname, async (username, message) => {
if (username === this.name) return;
if (ignore_messages.some((m) => message.startsWith(m))) return;
console.log('received message from', username, ':', message);
let translation = await handleEnglishTranslation(message);
console.log('received message from', username, ':', translation);
this.shut_up = false;
this.handleMessage(username, message);
this.handleMessage(username, translation);
});
// set the bot to automatically eat food when hungry
@ -77,7 +79,8 @@ export class Agent {
this.handleMessage('system', init_message, 2);
}
else {
this.bot.chat('Hello world! I am ' + this.name);
const translation = await handleTranslation("Hello world! I am "+this.name);
this.bot.chat(translation);
this.bot.emit('finished_executing');
}
@ -85,7 +88,15 @@ export class Agent {
});
}
cleanChat(message) {
async cleanChat(message, translate_up_to=-1) {
let to_translate = message;
let remainging = '';
if (translate_up_to != -1) {
to_translate = to_translate.substring(0, translate_up_to);
remainging = message.substring(translate_up_to);
}
message = (await handleTranslation(to_translate)).trim() + " " + remainging;
// newlines are interpreted as separate chats, which triggers spam filters. replace them with spaces
message = message.replaceAll('\n', ' ');
return this.bot.chat(message);
@ -128,6 +139,16 @@ export class Agent {
const checkInterrupt = () => this.self_prompter.shouldInterrupt(self_prompt) || this.shut_up;
let behavior_log = this.bot.modes.flushBehaviorLog();
if (behavior_log.trim().length > 0) {
const MAX_LOG = 500;
if (behavior_log.length > MAX_LOG) {
behavior_log = '...' + behavior_log.substring(behavior_log.length - MAX_LOG);
}
behavior_log = 'Recent behaviors log: \n' + behavior_log.substring(behavior_log.indexOf('\n'));
await this.history.add('system', behavior_log);
}
await this.history.add(source, message);
this.history.save();
@ -158,7 +179,7 @@ export class Agent {
this.self_prompter.handleUserPromptedCmd(self_prompt, isAction(command_name));
if (settings.verbose_commands) {
this.cleanChat(res);
this.cleanChat(res, res.indexOf(command_name));
}
else { // only output command name
let pre_message = res.substring(0, res.indexOf(command_name)).trim();
@ -280,4 +301,3 @@ export class Agent {
process.exit(1);
}
}

View file

@ -10,6 +10,7 @@ export class Coder {
this.generating = false;
this.code_template = '';
this.timedout = false;
this.cur_action_name = '';
readFile('./bots/template.js', 'utf8', (err, data) => {
if (err) throw err;
@ -156,16 +157,15 @@ export class Coder {
return {success: false, message: null, interrupted: false, timedout: true};
}
async executeResume(func=null, name=null, timeout=10) {
if (func != null) {
async executeResume(func=null, timeout=10) {
if (func != null) { // start new resume
this.resume_func = func;
this.resume_name = name;
this.resume_name = this.cur_action_name;
}
if (this.resume_func != null && this.agent.isIdle() && !this.agent.self_prompter.on) {
console.log('resuming code...')
this.interruptible = true;
this.cur_action_name = this.resume_name;
let res = await this.execute(this.resume_func, timeout);
this.interruptible = false;
this.cur_action_name = '';
return res;
} else {
return {success: false, message: null, interrupted: false, timedout: false};
@ -177,6 +177,10 @@ export class Coder {
this.resume_name = null;
}
setCurActionName(name) {
this.cur_action_name = name.replace(/!/g, '');
}
// returns {success: bool, message: string, interrupted: bool, timedout: false}
async execute(func, timeout=10) {
if (!this.code_template) return {success: false, message: "Code template not loaded.", interrupted: false, timedout: false};

View file

@ -1,17 +1,16 @@
import * as skills from '../library/skills.js';
import settings from '../../../settings.js';
function wrapExecution(func, timeout=-1, resume_name=null) {
function wrapExecution(func, resume=false, timeout=-1) {
return async function (agent, ...args) {
let code_return;
if (resume_name != null) {
code_return = await agent.coder.executeResume(async () => {
const wrappedFunction = async () => {
await func(agent, ...args);
}, resume_name, timeout);
};
if (resume) {
code_return = await agent.coder.executeResume(wrappedFunction, timeout);
} else {
code_return = await agent.coder.execute(async () => {
await func(agent, ...args);
}, timeout);
code_return = await agent.coder.execute(wrappedFunction, timeout);
}
if (code_return.interrupted && !code_return.timedout)
return;
@ -22,10 +21,14 @@ function wrapExecution(func, timeout=-1, resume_name=null) {
export const actionsList = [
{
name: '!newAction',
description: 'Perform new and unknown custom behaviors that are not available as a command by writing code.',
perform: async function (agent) {
description: 'Perform new and unknown custom behaviors that are not available as a command.',
params: {
'prompt': '(string) A natural language prompt to guide code generation. Make a detailed step-by-step plan.'
},
perform: async function (agent, prompt) {
// just ignore prompt - it is now in context in chat history
if (!settings.allow_insecure_coding)
return 'newAction Failed! Agent is not allowed to write code. Notify the user.';
return 'newAction not allowed! Code writing is disabled in settings. Notify the user.';
return await agent.coder.generateCode(agent.history);
}
},
@ -73,7 +76,7 @@ export const actionsList = [
description: 'Go to the given player.',
params: {
'player_name': {type: 'string', description: 'The name of the player to go to.'},
'closeness': {type: 'float', description: 'How close to get to the player.', 'domain': [0, Infinity]}
'closeness': {type: 'float', description: 'How close to get to the player.', domain: [0, Infinity]}
},
perform: wrapExecution(async (agent, player_name, closeness) => {
return await skills.goToPlayer(agent.bot, player_name, closeness);
@ -84,19 +87,19 @@ export const actionsList = [
description: 'Endlessly follow the given player. Will defend that player if self_defense mode is on.',
params: {
'player_name': {type: 'string', description: 'name of the player to follow.'},
'follow_dist': {type: 'float', description: 'The distance to follow from.', 'domain': [0, Infinity]}
'follow_dist': {type: 'float', description: 'The distance to follow from.', domain: [0, Infinity]}
},
perform: wrapExecution(async (agent, player_name, follow_dist) => {
await skills.followPlayer(agent.bot, player_name, follow_dist);
}, -1, 'followPlayer')
}, true)
},
{
name: '!goToBlock',
description: 'Go to the nearest block of a given type.',
params: {
'type': { type: 'blockName', description: 'The block type to go to.' },
'closeness': { type: 'float', description: 'How close to get to the block.', 'domain': [0, Infinity] },
'search_range': { type: 'float', description: 'The distance to search for the block.', 'domain': [0, Infinity] }
'type': { type: 'BlockName', description: 'The block type to go to.' },
'closeness': { type: 'float', description: 'How close to get to the block.', domain: [0, Infinity] },
'search_range': { type: 'float', description: 'The distance to search for the block.', domain: [0, Infinity] }
},
perform: wrapExecution(async (agent, type, closeness, range) => {
await skills.goToNearestBlock(agent.bot, type, closeness, range);
@ -105,7 +108,7 @@ export const actionsList = [
{
name: '!moveAway',
description: 'Move away from the current location in any direction by a given distance.',
params: {'distance': { type: 'float', description: 'The distance to move away.', 'domain': [0, Infinity] }},
params: {'distance': { type: 'float', description: 'The distance to move away.', domain: [0, Infinity] }},
perform: wrapExecution(async (agent, distance) => {
await skills.moveAway(agent.bot, distance);
})
@ -138,42 +141,94 @@ export const actionsList = [
description: 'Give the specified item to the given player.',
params: {
'player_name': { type: 'string', description: 'The name of the player to give the item to.' },
'item_name': { type: 'itemName', description: 'The name of the item to give.' },
'num': { type: 'int', description: 'The number of items to give.', 'domain': [1, Number.MAX_SAFE_INTEGER] }
'item_name': { type: 'ItemName', description: 'The name of the item to give.' },
'num': { type: 'int', description: 'The number of items to give.', domain: [1, Number.MAX_SAFE_INTEGER] }
},
perform: wrapExecution(async (agent, player_name, item_name, num) => {
await skills.giveToPlayer(agent.bot, item_name, player_name, num);
})
},
{
name: '!equip',
description: 'Equip the given item.',
params: {'item_name': { type: 'ItemName', description: 'The name of the item to equip.' }},
perform: wrapExecution(async (agent, item_name) => {
await skills.equip(agent.bot, item_name);
})
},
{
name: '!putInChest',
description: 'Put the given item in the nearest chest.',
params: {
'item_name': { type: 'ItemName', description: 'The name of the item to put in the chest.' },
'num': { type: 'int', description: 'The number of items to put in the chest.', domain: [1, Number.MAX_SAFE_INTEGER] }
},
perform: wrapExecution(async (agent, item_name, num) => {
await skills.putInChest(agent.bot, item_name, num);
})
},
{
name: '!takeFromChest',
description: 'Take the given items from the nearest chest.',
params: {
'item_name': { type: 'ItemName', description: 'The name of the item to take.' },
'num': { type: 'int', description: 'The number of items to take.', domain: [1, Number.MAX_SAFE_INTEGER] }
},
perform: wrapExecution(async (agent, item_name, num) => {
await skills.takeFromChest(agent.bot, item_name, num);
})
},
{
name: '!viewChest',
description: 'View the items/counts of the nearest chest.',
params: { },
perform: wrapExecution(async (agent) => {
await skills.viewChest(agent.bot);
})
},
{
name: '!discard',
description: 'Discard the given item from the inventory.',
params: {
'item_name': { type: 'ItemName', description: 'The name of the item to discard.' },
'num': { type: 'int', description: 'The number of items to discard.', domain: [1, Number.MAX_SAFE_INTEGER] }
},
perform: wrapExecution(async (agent, item_name, num) => {
const start_loc = agent.bot.entity.position;
await skills.moveAway(agent.bot, 5);
await skills.discard(agent.bot, item_name, num);
await skills.goToPosition(agent.bot, start_loc.x, start_loc.y, start_loc.z, 0);
})
},
{
name: '!collectBlocks',
description: 'Collect the nearest blocks of a given type.',
params: {
'type': { type: 'blockName', description: 'The block type to collect.' },
'num': { type: 'int', description: 'The number of blocks to collect.', 'domain': [1, Number.MAX_SAFE_INTEGER] }
'type': { type: 'BlockName', description: 'The block type to collect.' },
'num': { type: 'int', description: 'The number of blocks to collect.', domain: [1, Number.MAX_SAFE_INTEGER] }
},
perform: wrapExecution(async (agent, type, num) => {
await skills.collectBlock(agent.bot, type, num);
}, 10) // 10 minute timeout
}, false, 10) // 10 minute timeout
},
{
name: '!collectAllBlocks',
description: 'Collect all the nearest blocks of a given type until told to stop.',
params: {
'type': { type: 'blockName', description: 'The block type to collect.' }
'type': { type: 'BlockName', description: 'The block type to collect.' }
},
perform: wrapExecution(async (agent, type) => {
let success = await skills.collectBlock(agent.bot, type, 1);
if (!success)
agent.coder.cancelResume();
}, 10, 'collectAllBlocks') // 10 minute timeout
}, true, 3) // 3 minute timeout
},
{
name: '!craftRecipe',
description: 'Craft the given recipe a given number of times.',
params: {
'recipe_name': { type: 'itemName', description: 'The name of the output item to craft.' },
'num': { type: 'int', description: '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.', 'domain': [1, Number.MAX_SAFE_INTEGER] }
'recipe_name': { type: 'ItemName', description: 'The name of the output item to craft.' },
'num': { type: 'int', description: '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.', domain: [1, Number.MAX_SAFE_INTEGER] }
},
perform: wrapExecution(async (agent, recipe_name, num) => {
await skills.craftRecipe(agent.bot, recipe_name, num);
@ -184,7 +239,7 @@ export const actionsList = [
description: 'Smelt the given item the given number of times.',
params: {
'item_name': { type: 'string', description: 'The name of the input item to smelt.' },
'num': { type: 'int', description: 'The number of times to smelt the item.', 'domain': [1, Number.MAX_SAFE_INTEGER] }
'num': { type: 'int', description: 'The number of times to smelt the item.', domain: [1, Number.MAX_SAFE_INTEGER] }
},
perform: async function (agent, item_name, num) {
let response = await wrapExecution(async (agent) => {
@ -226,7 +281,7 @@ export const actionsList = [
{
name: '!activate',
description: 'Activate the nearest object of a given type.',
params: {'type': { type: 'blockName', description: 'The type of object to activate.' }},
params: {'type': { type: 'BlockName', description: 'The type of object to activate.' }},
perform: wrapExecution(async (agent, type) => {
await skills.activateNearestBlock(agent.bot, type);
})
@ -278,7 +333,7 @@ export const actionsList = [
description: 'Set a simple goal for an item or building to automatically work towards. Do not use for complex goals.',
params: {
'name': { type: 'string', description: 'The name of the goal to set. Can be item or building name. If empty will automatically choose a goal.' },
'quantity': { type: 'int', description: 'The quantity of the goal to set. Default is 1.', 'domain': [1, Number.MAX_SAFE_INTEGER] }
'quantity': { type: 'int', description: 'The quantity of the goal to set. Default is 1.', domain: [1, Number.MAX_SAFE_INTEGER] }
},
perform: async function (agent, name=null, quantity=1) {
await agent.npc.setGoal(name, quantity);

View file

@ -117,8 +117,8 @@ function parseCommandMessage(message) {
arg = Number.parseInt(arg); break;
case 'boolean':
arg = parseBoolean(arg); break;
case 'blockName':
case 'itemName':
case 'BlockName':
case 'ItemName':
if (arg.endsWith('plank'))
arg += 's'; // catches common mistakes like "oak_plank" instead of "oak_planks"
case 'string':
@ -146,9 +146,9 @@ function parseCommandMessage(message) {
console.warn(`Command '${commandName}' parameter '${paramNames[i]}' has no domain set. Expect any value [-Infinity, Infinity].`)
suppressNoDomainWarning = true; //Don't spam console. Only give the warning once.
}
} else if(param.type === 'blockName') { //Check that there is a block with this name
} else if(param.type === 'BlockName') { //Check that there is a block with this name
if(getBlockId(arg) == null) return `Invalid block type: ${arg}.`
} else if(param.type === 'itemName') { //Check that there is an item with this name
} else if(param.type === 'ItemName') { //Check that there is an item with this name
if(getItemId(arg) == null) return `Invalid item type: ${arg}.`
}
args[i] = arg;
@ -200,16 +200,33 @@ export async function executeCommand(agent, message) {
else {
console.log('parsed command:', parsed);
const command = getCommand(parsed.commandName);
return await command.perform(agent, ...parsed.args);
const is_action = isAction(command.name);
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 ${numArgs} args, but requires ${numParams(command)} args.`;
else {
if (is_action)
agent.coder.setCurActionName(command.name);
const result = await command.perform(agent, ...parsed.args);
if (is_action)
agent.coder.setCurActionName('');
return result;
}
}
}
export function getCommandDocs() {
const typeTranslations = {
//This was added to keep the prompt the same as before type checks were implemented.
//If the language model is giving invalid inputs changing this might help.
'float': 'number',
'int': 'number',
'blockName': 'string',
'itemName': 'string',
'BlockName': 'string',
'ItemName': 'string',
'boolean': 'bool'
}
let docs = `\n*COMMAND DOCS\n You can use the following commands to perform actions and get information about the world.

View file

@ -32,56 +32,6 @@ async function equipHighestAttack(bot) {
await bot.equip(weapon, 'hand');
}
/**
* Returns the number of ingredients required to use the recipe once.
*
* @param {Recipe} recipe
* @returns {Object<mc.ItemName, number>} an object describing the number of each ingredient.
*/
function ingredientsFromPrismarineRecipe(recipe) {
let requiredIngedients = {};
if (recipe.inShape)
for (const ingredient of recipe.inShape.flat()) {
if(ingredient.id<0) continue; //prismarine-recipe uses id -1 as an empty crafting slot
const ingredientName = mc.getItemName(ingredient.id);
requiredIngedients[ingredientName] ??=0;
requiredIngedients[ingredientName] += ingredient.count;
}
if (recipe.ingredients)
for (const ingredient of recipe.ingredients) {
if(ingredient.id<0) continue;
const ingredientName = mc.getItemName(ingredient.id);
requiredIngedients[ingredientName] ??=0;
requiredIngedients[ingredientName] -= ingredient.count;
//Yes, the `-=` is intended.
//prismarine-recipe uses positive numbers for the shaped ingredients but negative for unshaped.
//Why this is the case is beyond my understanding.
}
return requiredIngedients;
}
/**
* Calculates the number of times an action, such as a crafing recipe, can be completed before running out of resources.
* @template T - doesn't have to be an item. This could be any resource.
* @param {Object.<T, number>} availableItems - The resources available; e.g, `{'cobble_stone': 7, 'stick': 10}`
* @param {Object.<T, number>} requiredItems - The resources required to complete the action once; e.g, `{'cobble_stone': 3, 'stick': 2}`
* @param {boolean} discrete - Is the action discrete?
* @returns {{num: number, limitingResource: (T | null)}} the number of times the action can be completed and the limmiting resource; e.g `{num: 2, limitingResource: 'cobble_stone'}`
*/
function calculateLimitingResource(availableItems, requiredItems, discrete=true) {
let limitingResource = null;
let num = Infinity;
for (const itemType in requiredItems) {
if (availableItems[itemType] < requiredItems[itemType] * num) {
limitingResource = itemType;
num = availableItems[itemType] / requiredItems[itemType];
}
}
if(discrete) num = Math.floor(num);
return {num, limitingResource}
}
export async function craftRecipe(bot, itemName, num=1) {
/**
* Attempt to craft the given item name from a recipe. May craft many items.
@ -141,8 +91,8 @@ export async function craftRecipe(bot, itemName, num=1) {
console.log('crafting...');
//Check that the agent has sufficient items to use the recipe `num` times.
const inventory = world.getInventoryCounts(bot); //Items in the agents inventory
const requiredIngredients = ingredientsFromPrismarineRecipe(recipe); //Items required to use the recipe once.
const craftLimit = calculateLimitingResource(inventory, requiredIngredients);
const requiredIngredients = mc.ingredientsFromPrismarineRecipe(recipe); //Items required to use the recipe once.
const craftLimit = mc.calculateLimitingResource(inventory, requiredIngredients);
await bot.craft(recipe, Math.min(craftLimit.num, num), craftingTable);
if(craftLimit.num<num) log(bot, `Not enough ${craftLimit.limitingResource} to craft ${num}, crafted ${craftLimit.num}. You now have ${world.getInventoryCounts(bot)[itemName]} ${itemName}.`);
@ -260,6 +210,7 @@ export async function smeltItem(bot, itemName, num=1) {
break;
}
}
await bot.closeWindow(furnace);
if (placedFurnace) {
await collectBlock(bot, 'furnace', 1);
@ -441,6 +392,12 @@ export async function collectBlock(bot, blockType, num=1, exclude=null) {
);
}
}
const movements = new pf.Movements(bot);
movements.dontMineUnderFallingBlock = false;
blocks = blocks.filter(
block => movements.safeToBreak(block)
);
if (blocks.length === 0) {
if (collected === 0)
log(bot, `No ${blockType} nearby to collect.`);
@ -707,23 +664,35 @@ export async function placeBlock(bot, blockType, x, y, z, placeOn='bottom', dont
}
}
export async function equip(bot, itemName, bodyPart) {
export async function equip(bot, itemName) {
/**
* Equip the given item to the given body part, like tools or armor.
* Equip the given item to the proper body part, like tools or armor.
* @param {MinecraftBot} bot, reference to the minecraft bot.
* @param {string} itemName, the item or block name to equip.
* @param {string} bodyPart, the body part to equip the item to.
* @returns {Promise<boolean>} true if the item was equipped, false otherwise.
* @example
* await skills.equip(bot, "iron_pickaxe", "hand");
* await skills.equip(bot, "diamond_chestplate", "torso");
* await skills.equip(bot, "iron_pickaxe");
**/
let item = bot.inventory.items().find(item => item.name === itemName);
if (!item) {
log(bot, `You do not have any ${itemName} to equip.`);
return false;
}
await bot.equip(item, bodyPart);
if (itemName.includes('leggings')) {
await bot.equip(item, 'legs');
}
else if (itemName.includes('boots')) {
await bot.equip(item, 'feet');
}
else if (itemName.includes('helmet')) {
await bot.equip(item, 'head');
}
else if (itemName.includes('chestplate')) {
await bot.equip(item, 'torso');
}
else {
await bot.equip(item, 'hand');
}
return true;
}
@ -758,6 +727,94 @@ export async function discard(bot, itemName, num=-1) {
return true;
}
export async function putInChest(bot, itemName, num=-1) {
/**
* Put the given item in the nearest chest.
* @param {MinecraftBot} bot, reference to the minecraft bot.
* @param {string} itemName, the item or block name to put in the chest.
* @param {number} num, the number of items to put in the chest. Defaults to -1, which puts all items.
* @returns {Promise<boolean>} true if the item was put in the chest, false otherwise.
* @example
* await skills.putInChest(bot, "oak_log");
**/
let chest = world.getNearestBlock(bot, 'chest', 32);
if (!chest) {
log(bot, `Could not find a chest nearby.`);
return false;
}
let item = bot.inventory.items().find(item => item.name === itemName);
if (!item) {
log(bot, `You do not have any ${itemName} to put in the chest.`);
return false;
}
let to_put = num === -1 ? item.count : Math.min(num, item.count);
await goToPosition(bot, chest.position.x, chest.position.y, chest.position.z, 2);
const chestContainer = await bot.openContainer(chest);
await chestContainer.deposit(item.type, null, to_put);
await chestContainer.close();
log(bot, `Successfully put ${to_put} ${itemName} in the chest.`);
return true;
}
export async function takeFromChest(bot, itemName, num=-1) {
/**
* Take the given item from the nearest chest.
* @param {MinecraftBot} bot, reference to the minecraft bot.
* @param {string} itemName, the item or block name to take from the chest.
* @param {number} num, the number of items to take from the chest. Defaults to -1, which takes all items.
* @returns {Promise<boolean>} true if the item was taken from the chest, false otherwise.
* @example
* await skills.takeFromChest(bot, "oak_log");
* **/
let chest = world.getNearestBlock(bot, 'chest', 32);
if (!chest) {
log(bot, `Could not find a chest nearby.`);
return false;
}
await goToPosition(bot, chest.position.x, chest.position.y, chest.position.z, 2);
const chestContainer = await bot.openContainer(chest);
let item = chestContainer.containerItems().find(item => item.name === itemName);
if (!item) {
log(bot, `Could not find any ${itemName} in the chest.`);
await chestContainer.close();
return false;
}
let to_take = num === -1 ? item.count : Math.min(num, item.count);
await chestContainer.withdraw(item.type, null, to_take);
await chestContainer.close();
log(bot, `Successfully took ${to_take} ${itemName} from the chest.`);
return true;
}
export async function viewChest(bot) {
/**
* View the contents of the nearest chest.
* @param {MinecraftBot} bot, reference to the minecraft bot.
* @returns {Promise<boolean>} true if the chest was viewed, false otherwise.
* @example
* await skills.viewChest(bot);
* **/
let chest = world.getNearestBlock(bot, 'chest', 32);
if (!chest) {
log(bot, `Could not find a chest nearby.`);
return false;
}
await goToPosition(bot, chest.position.x, chest.position.y, chest.position.z, 2);
const chestContainer = await bot.openContainer(chest);
let items = chestContainer.containerItems();
if (items.length === 0) {
log(bot, `The chest is empty.`);
}
else {
log(bot, `The chest contains:`);
for (let item of items) {
log(bot, `${item.count} ${item.name}`);
}
}
await chestContainer.close();
return true;
}
export async function eat(bot, foodName="") {
/**
* Eat the given item. If no item is given, it will eat the first food item in the bot's inventory.
@ -916,12 +973,33 @@ export async function followPlayer(bot, username, distance=4) {
bot.pathfinder.setGoal(new pf.goals.GoalFollow(player, distance), true);
log(bot, `You are now actively following player ${username}.`);
let last_time = Date.now();
let stuck_time = 0;
let last_pos = bot.entity.position.clone();
while (!bot.interrupt_code) {
await new Promise(resolve => setTimeout(resolve, 500));
const delta = Date.now() - last_time;
// in cheat mode, if the distance is too far, teleport to the player
if (bot.modes.isOn('cheat') && bot.entity.position.distanceTo(player.position) > 100 && player.isOnGround) {
await goToPlayer(bot, username);
}
if (bot.modes.isOn('unstuck')) {
const far_away = bot.entity.position.distanceTo(player.position) > distance + 1;
if (far_away && bot.entity.position.distanceTo(last_pos) <= 2) {
stuck_time += delta;
if (stuck_time > 10000) {
log(bot, `Got stuck, attempting to move away.`);
bot.pathfinder.stop();
await moveAway(bot, 4);
return false;
}
}
else {
stuck_time = 0;
last_pos = bot.entity.position.clone();
}
}
last_time = Date.now();
}
return true;
}

View file

@ -2,10 +2,14 @@ import * as skills from './library/skills.js';
import * as world from './library/world.js';
import * as mc from '../utils/mcdata.js';
import settings from '../../settings.js'
import { handleTranslation } from '../utils/translator.js';
function say(agent, message) {
async function say(agent, message) {
agent.bot.modes.behavior_log += message + '\n';
if (agent.shut_up || !settings.narrate_behavior) return;
agent.bot.chat(message);
let translation = await handleTranslation(message);
agent.bot.chat(translation);
}
// a mode is a function that is called every tick to respond immediately to the world
@ -22,7 +26,7 @@ function say(agent, message) {
const modes = [
{
name: 'self_preservation',
description: 'Respond to drowning, burning, and damage at low health. Interrupts other actions.',
description: 'Respond to drowning, burning, and damage at low health. Interrupts all actions.',
interrupts: ['all'],
on: true,
active: false,
@ -70,9 +74,39 @@ const modes = [
}
}
},
{
name: 'unstuck',
description: 'Attempt to get unstuck when in the same place for a while. Interrupts some actions.',
interrupts: ['collectBlocks', 'goToPlayer', 'collectAllBlocks', 'goToPlace'],
on: true,
active: false,
prev_location: null,
distance: 2,
stuck_time: 0,
last_time: Date.now(),
max_stuck_time: 20,
update: async function (agent) {
if (agent.isIdle()) return;
const bot = agent.bot;
if (this.prev_location && this.prev_location.distanceTo(bot.entity.position) < this.distance) {
this.stuck_time += (Date.now() - this.last_time) / 1000;
}
else {
this.prev_location = bot.entity.position.clone();
this.stuck_time = 0;
}
if (this.stuck_time > this.max_stuck_time) {
say(agent, 'I\'m stuck!');
execute(this, agent, async () => {
await skills.moveAway(bot, 5);
});
}
this.last_time = Date.now();
}
},
{
name: 'cowardice',
description: 'Run away from enemies. Interrupts other actions.',
description: 'Run away from enemies. Interrupts all actions.',
interrupts: ['all'],
on: true,
active: false,
@ -88,7 +122,7 @@ const modes = [
},
{
name: 'self_defense',
description: 'Attack nearby enemies. Interrupts other actions.',
description: 'Attack nearby enemies. Interrupts all actions.',
interrupts: ['all'],
on: true,
active: false,
@ -105,7 +139,7 @@ const modes = [
{
name: 'hunting',
description: 'Hunt nearby animals when idle.',
interrupts: ['defaults'],
interrupts: [],
on: true,
active: false,
update: async function (agent) {
@ -130,7 +164,8 @@ const modes = [
noticed_at: -1,
update: async function (agent) {
let item = world.getNearestEntityWhere(agent.bot, entity => entity.name === 'item', 8);
if (item && item !== this.prev_item && await world.isClearPath(agent.bot, item)) {
let empty_inv_slots = agent.bot.inventory.emptySlotCount();
if (item && item !== this.prev_item && await world.isClearPath(agent.bot, item) && empty_inv_slots > 1) {
if (this.noticed_at === -1) {
this.noticed_at = Date.now();
}
@ -230,6 +265,7 @@ class ModeController {
this.agent = agent;
this.modes_list = modes;
this.modes_map = {};
this.behavior_log = '';
for (let mode of this.modes_list) {
this.modes_map[mode.name] = mode;
}
@ -281,15 +317,20 @@ class ModeController {
this.unPauseAll();
}
for (let mode of this.modes_list) {
let available = mode.interrupts.includes('all') || this.agent.isIdle();
let interruptible = this.agent.coder.interruptible && (mode.interrupts.includes('defaults') || mode.interrupts.includes(this.agent.coder.resume_name));
if (mode.on && !mode.paused && !mode.active && (available || interruptible)) {
let interruptible = mode.interrupts.some(i => i === 'all') || mode.interrupts.some(i => i === this.agent.coder.cur_action_name);
if (mode.on && !mode.paused && !mode.active && (this.agent.isIdle() || interruptible)) {
await mode.update(this.agent);
}
if (mode.active) break;
}
}
flushBehaviorLog() {
const log = this.behavior_log;
this.behavior_log = '';
return log;
}
getJson() {
let res = {};
for (let mode of this.modes_list) {

View file

@ -10,7 +10,7 @@ import { GPT } from '../models/gpt.js';
import { Claude } from '../models/claude.js';
import { ReplicateAPI } from '../models/replicate.js';
import { Local } from '../models/local.js';
import { GroqCloudAPI } from '../models/groq.js';
export class Prompter {
constructor(agent, fp) {
@ -21,16 +21,22 @@ export class Prompter {
let name = this.profile.name;
let chat = this.profile.model;
// try to get "max_tokens" parameter, else null
let max_tokens = null;
if (this.profile.max_tokens)
max_tokens = this.profile.max_tokens;
if (typeof chat === 'string' || chat instanceof String) {
chat = {model: chat};
if (chat.model.includes('gemini'))
chat.api = 'google';
else if (chat.model.includes('gpt'))
else if (chat.model.includes('gpt') || chat.model.includes('o1'))
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 if (chat.model.includes("groq/") || chat.model.includes("groqcloud/"))
chat.api = 'groq';
else
chat.api = 'ollama';
}
@ -47,6 +53,9 @@ export class Prompter {
this.chat_model = new ReplicateAPI(chat.model, chat.url);
else if (chat.api == 'ollama')
this.chat_model = new Local(chat.model, chat.url);
else if (chat.api == 'groq') {
this.chat_model = new GroqCloudAPI(chat.model.replace('groq/', '').replace('groqcloud/', ''), chat.url, max_tokens ? max_tokens : 8192);
}
else
throw new Error('Unknown API:', api);

View file

@ -1,5 +1,6 @@
import OpenAIApi from 'openai';
import { getKey, hasKey } from '../utils/keys.js';
import { strictFormat } from '../utils/text.js';
export class GPT {
constructor(model_name, url) {
@ -18,18 +19,23 @@ export class GPT {
}
async sendRequest(turns, systemMessage, stop_seq='***') {
let messages = [{'role': 'system', 'content': systemMessage}].concat(turns);
const pack = {
model: this.model_name || "gpt-3.5-turbo",
messages,
stop: stop_seq,
};
if (this.model_name.includes('o1')) {
pack.messages = strictFormat(messages);
delete pack.stop;
}
let res = null;
try {
console.log('Awaiting openai api response...')
// console.log('Messages:', messages);
let completion = await this.openai.chat.completions.create({
model: this.model_name || "gpt-3.5-turbo",
messages: messages,
stop: stop_seq,
});
let completion = await this.openai.chat.completions.create(pack);
if (completion.choices[0].finish_reason == 'length')
throw new Error('Context length exceeded');
console.log('Received.')

51
src/models/groq.js Normal file
View file

@ -0,0 +1,51 @@
import Groq from 'groq-sdk'
import { getKey } from '../utils/keys.js';
// Umbrella class for Mixtral, LLama, Gemma...
export class GroqCloudAPI {
constructor(model_name, url, max_tokens=16384) {
this.model_name = model_name;
this.url = url;
this.max_tokens = max_tokens;
// ReplicateAPI theft :3
if (this.url) {
console.warn("Groq Cloud has no implementation for custom URLs. Ignoring provided URL.");
}
this.groq = new Groq({ apiKey: getKey('GROQCLOUD_API_KEY') });
}
async sendRequest(turns, systemMessage, stop_seq=null) {
let messages = [{"role": "system", "content": systemMessage}].concat(turns);
let res = null;
try {
console.log("Awaiting Groq response...");
let completion = await this.groq.chat.completions.create({
"messages": messages,
"model": this.model_name || "mixtral-8x7b-32768",
"temperature": 0.2,
"max_tokens": this.max_tokens, // maximum token limit, differs from model to model
"top_p": 1,
"stream": true,
"stop": stop_seq // "***"
});
let temp_res = "";
for await (const chunk of completion) {
temp_res += chunk.choices[0]?.delta?.content || '';
}
res = temp_res;
}
catch(err) {
console.log(err);
res = "My brain just kinda stopped working. Try again.";
}
return res;
}
async embed(text) {
console.log("There is no support for embeddings in Groq support. However, the following text was provided: " + text);
}
}

View file

@ -247,3 +247,52 @@ export function getBlockTool(blockName) {
export function makeItem(name, amount=1) {
return new Item(getItemId(name), amount);
}
/**
* Returns the number of ingredients required to use the recipe once.
*
* @param {Recipe} recipe
* @returns {Object<mc.ItemName, number>} an object describing the number of each ingredient.
*/
export function ingredientsFromPrismarineRecipe(recipe) {
let requiredIngedients = {};
if (recipe.inShape)
for (const ingredient of recipe.inShape.flat()) {
if(ingredient.id<0) continue; //prismarine-recipe uses id -1 as an empty crafting slot
const ingredientName = mc.getItemName(ingredient.id);
requiredIngedients[ingredientName] ??=0;
requiredIngedients[ingredientName] += ingredient.count;
}
if (recipe.ingredients)
for (const ingredient of recipe.ingredients) {
if(ingredient.id<0) continue;
const ingredientName = mc.getItemName(ingredient.id);
requiredIngedients[ingredientName] ??=0;
requiredIngedients[ingredientName] -= ingredient.count;
//Yes, the `-=` is intended.
//prismarine-recipe uses positive numbers for the shaped ingredients but negative for unshaped.
//Why this is the case is beyond my understanding.
}
return requiredIngedients;
}
/**
* Calculates the number of times an action, such as a crafing recipe, can be completed before running out of resources.
* @template T - doesn't have to be an item. This could be any resource.
* @param {Object.<T, number>} availableItems - The resources available; e.g, `{'cobble_stone': 7, 'stick': 10}`
* @param {Object.<T, number>} requiredItems - The resources required to complete the action once; e.g, `{'cobble_stone': 3, 'stick': 2}`
* @param {boolean} discrete - Is the action discrete?
* @returns {{num: number, limitingResource: (T | null)}} the number of times the action can be completed and the limmiting resource; e.g `{num: 2, limitingResource: 'cobble_stone'}`
*/
export function calculateLimitingResource(availableItems, requiredItems, discrete=true) {
let limitingResource = null;
let num = Infinity;
for (const itemType in requiredItems) {
if (availableItems[itemType] < requiredItems[itemType] * num) {
limitingResource = itemType;
num = availableItems[itemType] / requiredItems[itemType];
}
}
if(discrete) num = Math.floor(num);
return {num, limitingResource}
}

30
src/utils/translator.js Normal file
View file

@ -0,0 +1,30 @@
import translate from 'google-translate-api-x';
import settings from '../../settings.js';
const preferred_lang = settings.language;
export async function handleTranslation(message) {
try {
if (preferred_lang.toLowerCase() === 'en' || preferred_lang.toLowerCase() === 'english') {
return message;
} else {
const lang = String(preferred_lang);
const translation = await translate(message, { to: lang });
return translation.text || message;
}
} catch (error) {
console.error('Error translating message:', error);
return message;
}
}
export async function handleEnglishTranslation(message) {
try {
const translation = await translate(message, { to: 'english' });
return translation.text || message;
} catch (error) {
console.error('Error translating message:', error);
return message;
}
}