From 7a3a5b3f222b8c7990a07f30c27e0d2214a20425 Mon Sep 17 00:00:00 2001 From: Nimi Date: Tue, 12 Nov 2024 11:20:30 -0600 Subject: [PATCH 01/35] LAN search, better error handling for joining servers --- main.js | 50 +++++++++++++++++++++++++- src/utils/mcserver.js | 84 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 src/utils/mcserver.js diff --git a/main.js b/main.js index 5e8cc97..0c93d2e 100644 --- a/main.js +++ b/main.js @@ -2,6 +2,8 @@ import { AgentProcess } from './src/process/agent-process.js'; import settings from './settings.js'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; +import { serverInfo, findServers } from './src/utils/mcserver.js'; +import mc from 'minecraft-protocol'; function parseArguments() { return yargs(hideBin(process.argv)) @@ -18,12 +20,58 @@ function getProfiles(args) { return args.profiles || settings.profiles; } -function main() { +async function getServer() { + let server = null; + let serverString = ""; + let serverVersion = ""; + + // Search for server + if (settings.port == -1) + { + console.log("No port provided. Searching for LAN server..."); + + await findServers(settings.host, true).then((servers) => { + if (servers.length > 0) + server = servers[0]; + }); + + if (server == null) + throw new Error(`No server found on LAN.`); + } + else + server = await serverInfo(settings.host, settings.port); + + // Server not found + if (server == null) + throw new Error(`Server not found. (Host: ${settings.host}, Port: ${settings.port}) Check the host and port in settings.js.`); + + serverString = `(Host: ${server.host}, Port: ${server.port}, Version: ${server.version})`; + + if (settings.minecraft_version === "auto") + serverVersion = server.version; + else + serverVersion = settings.minecraft_version; + + // Server version unsupported / mismatch + if (mc.supportedVersions.indexOf(serverVersion) === -1) + throw new Error(`A server was found ${serverString}, but version is unsupported. Supported versions are: ${mc.supportedVersions.join(", ")}.`); + else if (settings.minecraft_version !== "auto" && server.version !== settings.minecraft_version) + throw new Error(`A server was found ${serverString}, but version is incorrect. Expected ${settings.minecraft_version}, but found ${server.version}.`); + else + console.log(`Server found. ${serverString}`); + + return server; +} + +async function main() { const args = parseArguments(); const profiles = getProfiles(args); console.log(profiles); const { load_memory, init_message } = settings; + // Get server + const server = await getServer(); + for (let i=0; i} - A Promise that resolves to an array of server info objects. + */ +export async function serverInfo(ip, port, timeout = 100) { + return new Promise((resolve) => { + + setTimeout(() => { + resolve(null); // Resolve as null if no response within timeout + }, timeout); + + mc.ping({ + host: ip, + port + }, (err, response) => { + if (err) return resolve(null); + + const serverInfo = { + host: ip, + port, + name: response.description.text || 'No description provided.', + ping: response.latency, + version: response.version.name + }; + + resolve(serverInfo); + }); + }); +} + +/** + * Scans the IP address for Minecraft LAN servers and collects their info. + * @param {string} ip - The IP address to scan. + * @param {boolean} earlyExit - Whether to exit early after finding a server. + * @param {number} timeout - The connection timeout in ms. + * @returns {Promise} - A Promise that resolves to an array of server info objects. + */ +export async function findServers(ip, earlyExit = false, timeout = 100) { + const servers = []; + const startPort = 49000; + const endPort = 65000; + + const checkPort = (port) => { + return new Promise((resolve) => { + const socket = net.createConnection({ host: ip, port, timeout }, () => { + socket.end(); + resolve(port); // Port is open + }); + + socket.on('error', () => resolve(null)); // Port is closed + socket.on('timeout', () => { + socket.destroy(); + resolve(null); + }); + }); + }; + + // Surpress console output + const originalConsoleLog = console.log; + console.log = () => { }; + + for (let port = startPort; port <= endPort; port++) { + const openPort = await checkPort(port); + if (openPort) { + const server = await serverInfo(ip, port); + if (server) { + servers.push(server); + + if (earlyExit) break; + } + } + } + + // Restore console output + console.log = originalConsoleLog; + + return servers; +} \ No newline at end of file From 418a2470de1bab9e2338e99513db22ef0dbb0f0f Mon Sep 17 00:00:00 2001 From: Nimi Date: Thu, 14 Nov 2024 15:00:53 -0600 Subject: [PATCH 02/35] Server/Minecraft data now agent-level Also refactored mcdata as a class. --- main.js | 50 +-- src/agent/agent.js | 13 +- src/agent/commands/index.js | 6 +- src/agent/commands/queries.js | 2 +- src/agent/library/skills.js | 2 +- src/agent/library/world.js | 2 +- src/agent/modes.js | 2 +- src/agent/npc/build_goal.js | 2 +- src/agent/npc/controller.js | 2 +- src/agent/npc/item_goal.js | 2 +- src/agent/npc/utils.js | 3 +- src/process/agent-process.js | 9 +- src/process/init-agent.js | 14 +- src/utils/mcdata.js | 602 +++++++++++++++++----------------- src/utils/mcserver.js | 44 +++ 15 files changed, 396 insertions(+), 359 deletions(-) diff --git a/main.js b/main.js index 0c93d2e..666ab4b 100644 --- a/main.js +++ b/main.js @@ -2,8 +2,7 @@ import { AgentProcess } from './src/process/agent-process.js'; import settings from './settings.js'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; -import { serverInfo, findServers } from './src/utils/mcserver.js'; -import mc from 'minecraft-protocol'; +import { getServer } from './src/utils/mcserver.js'; function parseArguments() { return yargs(hideBin(process.argv)) @@ -20,49 +19,6 @@ function getProfiles(args) { return args.profiles || settings.profiles; } -async function getServer() { - let server = null; - let serverString = ""; - let serverVersion = ""; - - // Search for server - if (settings.port == -1) - { - console.log("No port provided. Searching for LAN server..."); - - await findServers(settings.host, true).then((servers) => { - if (servers.length > 0) - server = servers[0]; - }); - - if (server == null) - throw new Error(`No server found on LAN.`); - } - else - server = await serverInfo(settings.host, settings.port); - - // Server not found - if (server == null) - throw new Error(`Server not found. (Host: ${settings.host}, Port: ${settings.port}) Check the host and port in settings.js.`); - - serverString = `(Host: ${server.host}, Port: ${server.port}, Version: ${server.version})`; - - if (settings.minecraft_version === "auto") - serverVersion = server.version; - else - serverVersion = settings.minecraft_version; - - // Server version unsupported / mismatch - if (mc.supportedVersions.indexOf(serverVersion) === -1) - throw new Error(`A server was found ${serverString}, but version is unsupported. Supported versions are: ${mc.supportedVersions.join(", ")}.`); - else if (settings.minecraft_version !== "auto" && server.version !== settings.minecraft_version) - throw new Error(`A server was found ${serverString}, but version is incorrect. Expected ${settings.minecraft_version}, but found ${server.version}.`); - else - console.log(`Server found. ${serverString}`); - - return server; -} - async function main() { const args = parseArguments(); const profiles = getProfiles(args); @@ -71,10 +27,10 @@ async function main() { // Get server const server = await getServer(); - + for (let i=0; i { diff --git a/src/agent/library/skills.js b/src/agent/library/skills.js index 2b8d233..6a85303 100644 --- a/src/agent/library/skills.js +++ b/src/agent/library/skills.js @@ -1,4 +1,4 @@ -import * as mc from "../../utils/mcdata.js"; +import { mc } from '../../utils/mcdata.js'; import * as world from "./world.js"; import pf from 'mineflayer-pathfinder'; import Vec3 from 'vec3'; diff --git a/src/agent/library/world.js b/src/agent/library/world.js index 01d54c3..673d799 100644 --- a/src/agent/library/world.js +++ b/src/agent/library/world.js @@ -1,5 +1,5 @@ import pf from 'mineflayer-pathfinder'; -import * as mc from '../../utils/mcdata.js'; +import { mc } from '../../utils/mcdata.js'; export function getNearestFreeSpace(bot, size=1, distance=8) { diff --git a/src/agent/modes.js b/src/agent/modes.js index 2c0b3e0..a33a090 100644 --- a/src/agent/modes.js +++ b/src/agent/modes.js @@ -1,6 +1,6 @@ import * as skills from './library/skills.js'; import * as world from './library/world.js'; -import * as mc from '../utils/mcdata.js'; +import { mc } from '../utils/mcdata.js'; import settings from '../../settings.js' import { handleTranslation } from '../utils/translator.js'; diff --git a/src/agent/npc/build_goal.js b/src/agent/npc/build_goal.js index ebca78f..c529727 100644 --- a/src/agent/npc/build_goal.js +++ b/src/agent/npc/build_goal.js @@ -1,7 +1,7 @@ import { Vec3 } from 'vec3'; import * as skills from '../library/skills.js'; import * as world from '../library/world.js'; -import * as mc from '../../utils/mcdata.js'; +import { mc } from '../../utils/mcdata.js'; import { blockSatisfied, getTypeOfGeneric, rotateXZ } from './utils.js'; diff --git a/src/agent/npc/controller.js b/src/agent/npc/controller.js index 9af3f3e..0dbca62 100644 --- a/src/agent/npc/controller.js +++ b/src/agent/npc/controller.js @@ -5,7 +5,7 @@ import { BuildGoal } from './build_goal.js'; import { itemSatisfied, rotateXZ } from './utils.js'; import * as skills from '../library/skills.js'; import * as world from '../library/world.js'; -import * as mc from '../../utils/mcdata.js'; +import { mc } from '../../utils/mcdata.js'; export class NPCContoller { diff --git a/src/agent/npc/item_goal.js b/src/agent/npc/item_goal.js index 40589ba..7283de8 100644 --- a/src/agent/npc/item_goal.js +++ b/src/agent/npc/item_goal.js @@ -1,6 +1,6 @@ import * as skills from '../library/skills.js'; import * as world from '../library/world.js'; -import * as mc from '../../utils/mcdata.js'; +import { mc } from '../../utils/mcdata.js'; import { itemSatisfied } from './utils.js'; diff --git a/src/agent/npc/utils.js b/src/agent/npc/utils.js index 53d8486..2b3b825 100644 --- a/src/agent/npc/utils.js +++ b/src/agent/npc/utils.js @@ -1,6 +1,5 @@ import * as world from '../library/world.js'; -import * as mc from '../../utils/mcdata.js'; - +import { mc } from '../../utils/mcdata.js'; export function getTypeOfGeneric(bot, block_name) { // Get type of wooden block diff --git a/src/process/agent-process.js b/src/process/agent-process.js index 5135de1..a5f08d1 100644 --- a/src/process/agent-process.js +++ b/src/process/agent-process.js @@ -3,7 +3,7 @@ import { spawn } from 'child_process'; export class AgentProcess { static runningCount = 0; - start(profile, load_memory=false, init_message=null, count_id=0) { + start(profile, load_memory=false, init_message=null, server_host=null, server_port=0, server_version=null, count_id=0) { let args = ['src/process/init-agent.js', this.name]; args.push('-p', profile); args.push('-c', count_id); @@ -12,6 +12,11 @@ export class AgentProcess { if (init_message) args.push('-m', init_message); + // Pass server/version info to agent + args.push('--server_host', server_host); + args.push('--server_port', server_port); + args.push('--server_version', server_version); + const agentProcess = spawn('node', args, { stdio: 'inherit', stderr: 'inherit', @@ -34,7 +39,7 @@ export class AgentProcess { return; } console.log('Restarting agent...'); - this.start(profile, true, 'Agent process restarted.', count_id); + this.start(profile, true, 'Agent process restarted.', server_host, server_port, server_version, count_id); last_restart = Date.now(); } }); diff --git a/src/process/init-agent.js b/src/process/init-agent.js index 829f437..bc554f5 100644 --- a/src/process/init-agent.js +++ b/src/process/init-agent.js @@ -38,6 +38,18 @@ const argv = yargs(args) type: 'number', default: 0, description: 'identifying count for multi-agent scenarios', + }) + .option('server_host', { + type: 'string', + description: 'minecraft server host', + }) + .option('server_port', { + type: 'number', + description: 'minecraft server port', + }) + .option('server_version', { + type: 'string', + description: 'minecraft version' }).argv; // Wrap agent start in async IIFE with proper error handling @@ -45,7 +57,7 @@ const argv = yargs(args) try { console.log('Starting agent with profile:', argv.profile); const agent = new Agent(); - await agent.start(argv.profile, argv.load_memory, argv.init_message, argv.count_id); + await agent.start(argv.profile, argv.load_memory, argv.init_message, argv.server_host, argv.server_port, argv.server_version, argv.count_id); } catch (error) { console.error('Failed to start agent process:', { message: error.message || 'No error message', diff --git a/src/utils/mcdata.js b/src/utils/mcdata.js index 04a535a..2a8d7fa 100644 --- a/src/utils/mcdata.js +++ b/src/utils/mcdata.js @@ -9,317 +9,331 @@ import { plugin as autoEat } from 'mineflayer-auto-eat'; import plugin from 'mineflayer-armor-manager'; const armorManager = plugin; -const mc_version = settings.minecraft_version; -const mcdata = minecraftData(mc_version); -const Item = prismarine_items(mc_version); +class MinecraftData { + constructor() { + this.mcdata = null; + this.Item = null; -/** - * @typedef {string} ItemName - * @typedef {string} BlockName -*/ - -export const WOOD_TYPES = ['oak', 'spruce', 'birch', 'jungle', 'acacia', 'dark_oak']; -export const MATCHING_WOOD_BLOCKS = [ - 'log', - 'planks', - 'sign', - 'boat', - 'fence_gate', - 'door', - 'fence', - 'slab', - 'stairs', - 'button', - 'pressure_plate', - 'trapdoor' -] -export const WOOL_COLORS = [ - 'white', - 'orange', - 'magenta', - 'light_blue', - 'yellow', - 'lime', - 'pink', - 'gray', - 'light_gray', - 'cyan', - 'purple', - 'blue', - 'brown', - 'green', - 'red', - 'black' -] - - -export function initBot(username) { - let bot = createBot({ - username: username, - - host: settings.host, - port: settings.port, - auth: settings.auth, - - version: mc_version, - }); - bot.loadPlugin(pathfinder); - bot.loadPlugin(pvp); - bot.loadPlugin(collectblock); - bot.loadPlugin(autoEat); - bot.loadPlugin(armorManager); // auto equip armor - - return bot; -} - -export function isHuntable(mob) { - if (!mob || !mob.name) return false; - const animals = ['chicken', 'cow', 'llama', 'mooshroom', 'pig', 'rabbit', 'sheep']; - return animals.includes(mob.name.toLowerCase()) && !mob.metadata[16]; // metadata 16 is not baby -} - -export function isHostile(mob) { - if (!mob || !mob.name) return false; - return (mob.type === 'mob' || mob.type === 'hostile') && mob.name !== 'iron_golem' && mob.name !== 'snow_golem'; -} - -export function getItemId(itemName) { - let item = mcdata.itemsByName[itemName]; - if (item) { - return item.id; + this.server_version = null; + this.server_port = null; + this.server_host = null; } - return null; -} -export function getItemName(itemId) { - let item = mcdata.items[itemId] - if (item) { - return item.name; + init(host, port, version) { + this.server_version = version; + this.server_port = port; + this.server_host = host; + + this.mcdata = minecraftData(this.server_version); + this.Item = prismarine_items(this.server_version); } - return null; -} -export function getBlockId(blockName) { - let block = mcdata.blocksByName[blockName]; - if (block) { - return block.id; + initBot(username) { + let bot = createBot({ + username: username, + host: this.server_host, + port: this.server_port, + auth: settings.auth, + version: this.server_version, + }); + bot.loadPlugin(pathfinder); + bot.loadPlugin(pvp); + bot.loadPlugin(collectblock); + bot.loadPlugin(autoEat); + bot.loadPlugin(armorManager); // auto equip armor + return bot; } - return null; -} -export function getBlockName(blockId) { - let block = mcdata.blocks[blockId] - if (block) { - return block.name; + isHuntable(mob) { + if (!mob || !mob.name) return false; + const animals = ['chicken', 'cow', 'llama', 'mooshroom', 'pig', 'rabbit', 'sheep']; + return animals.includes(mob.name.toLowerCase()) && !mob.metadata[16]; // metadata 16 is not baby } - return null; -} -export function getAllItems(ignore) { - if (!ignore) { - ignore = []; + isHostile(mob) { + if (!mob || !mob.name) return false; + return (mob.type === 'mob' || mob.type === 'hostile') && mob.name !== 'iron_golem' && mob.name !== 'snow_golem'; } - let items = [] - for (const itemId in mcdata.items) { - const item = mcdata.items[itemId]; - if (!ignore.includes(item.name)) { - items.push(item); + + getItemId(itemName) { + let item = this.mcdata.itemsByName[itemName]; + if (item) { + return item.id; } - } - return items; -} - -export function getAllItemIds(ignore) { - const items = getAllItems(ignore); - let itemIds = []; - for (const item of items) { - itemIds.push(item.id); - } - return itemIds; -} - -export function getAllBlocks(ignore) { - if (!ignore) { - ignore = []; - } - let blocks = [] - for (const blockId in mcdata.blocks) { - const block = mcdata.blocks[blockId]; - if (!ignore.includes(block.name)) { - blocks.push(block); - } - } - return blocks; -} - -export function getAllBlockIds(ignore) { - const blocks = getAllBlocks(ignore); - let blockIds = []; - for (const block of blocks) { - blockIds.push(block.id); - } - return blockIds; -} - -export function getAllBiomes() { - return mcdata.biomes; -} - -export function getItemCraftingRecipes(itemName) { - let itemId = getItemId(itemName); - if (!mcdata.recipes[itemId]) { return null; } - let recipes = []; - for (let r of mcdata.recipes[itemId]) { - let recipe = {}; - let ingredients = []; - if (r.ingredients) { - ingredients = r.ingredients; - } else if (r.inShape) { - ingredients = r.inShape.flat(); + getItemName(itemId) { + let item = this.mcdata.items[itemId] + if (item) { + return item.name; } - for (let ingredient of ingredients) { - let ingredientName = getItemName(ingredient); - if (ingredientName === null) continue; - if (!recipe[ingredientName]) - recipe[ingredientName] = 0; - recipe[ingredientName]++; - } - recipes.push(recipe); - } - - return recipes; -} - -export function isSmeltable(itemName) { - const misc_smeltables = ['beef', 'chicken', 'cod', 'mutton', 'porkchop', 'rabbit', 'salmon', 'tropical_fish', 'potato', 'kelp', 'sand', 'cobblestone', 'clay_ball']; - return itemName.includes('raw') || itemName.includes('log') || misc_smeltables.includes(itemName); -} - -export function getSmeltingFuel(bot) { - let fuel = bot.inventory.items().find(i => i.name === 'coal' || i.name === 'charcoal') - if (fuel) - return fuel; - fuel = bot.inventory.items().find(i => i.name.includes('log') || i.name.includes('planks')) - if (fuel) - return fuel; - return bot.inventory.items().find(i => i.name === 'coal_block' || i.name === 'lava_bucket'); -} - -export function getFuelSmeltOutput(fuelName) { - if (fuelName === 'coal' || fuelName === 'charcoal') - return 8; - if (fuelName.includes('log') || fuelName.includes('planks')) - return 1.5 - if (fuelName === 'coal_block') - return 80; - if (fuelName === 'lava_bucket') - return 100; - return 0; -} - -export function getItemSmeltingIngredient(itemName) { - return { - baked_potato: 'potato', - steak: 'raw_beef', - cooked_chicken: 'raw_chicken', - cooked_cod: 'raw_cod', - cooked_mutton: 'raw_mutton', - cooked_porkchop: 'raw_porkchop', - cooked_rabbit: 'raw_rabbit', - cooked_salmon: 'raw_salmon', - dried_kelp: 'kelp', - iron_ingot: 'raw_iron', - gold_ingot: 'raw_gold', - copper_ingot: 'raw_copper', - glass: 'sand' - }[itemName]; -} - -export function getItemBlockSources(itemName) { - let itemId = getItemId(itemName); - let sources = []; - for (let block of getAllBlocks()) { - if (block.drops.includes(itemId)) { - sources.push(block.name); - } - } - return sources; -} - -export function getItemAnimalSource(itemName) { - return { - raw_beef: 'cow', - raw_chicken: 'chicken', - raw_cod: 'cod', - raw_mutton: 'sheep', - raw_porkchop: 'pig', - raw_rabbit: 'rabbit', - raw_salmon: 'salmon', - leather: 'cow', - wool: 'sheep' - }[itemName]; -} - -export function getBlockTool(blockName) { - let block = mcdata.blocksByName[blockName]; - if (!block || !block.harvestTools) { return null; } - return getItemName(Object.keys(block.harvestTools)[0]); // Double check first tool is always simplest -} -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} 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 = 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 = 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.} availableItems - The resources available; e.g, `{'cobble_stone': 7, 'stick': 10}` - * @param {Object.} 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]; + getBlockId(blockName) { + let block = this.mcdata.blocksByName[blockName]; + if (block) { + return block.id; } + return null; } - if(discrete) num = Math.floor(num); - return {num, limitingResource} -} \ No newline at end of file + + getBlockName(blockId) { + let block = this.mcdata.blocks[blockId] + if (block) { + return block.name; + } + return null; + } + + getAllItems(ignore) { + if (!ignore) { + ignore = []; + } + let items = [] + for (const itemId in this.mcdata.items) { + const item = this.mcdata.items[itemId]; + if (!ignore.includes(item.name)) { + items.push(item); + } + } + return items; + } + + getAllItemIds(ignore) { + const items = this.getAllItems(ignore); + let itemIds = []; + for (const item of items) { + itemIds.push(item.id); + } + return itemIds; + } + + getAllBlocks(ignore) { + if (!ignore) { + ignore = []; + } + let blocks = [] + for (const blockId in this.mcdata.blocks) { + const block = this.mcdata.blocks[blockId]; + if (!ignore.includes(block.name)) { + blocks.push(block); + } + } + return blocks; + } + + getAllBlockIds(ignore) { + const blocks = this.getAllBlocks(ignore); + let blockIds = []; + for (const block of blocks) { + blockIds.push(block.id); + } + return blockIds; + } + + getAllBiomes() { + return this.mcdata.biomes; + } + + getItemCraftingRecipes(itemName) { + let itemId = this.getItemId(itemName); + if (!this.mcdata.recipes[itemId]) { + return null; + } + + let recipes = []; + for (let r of this.mcdata.recipes[itemId]) { + let recipe = {}; + let ingredients = []; + if (r.ingredients) { + ingredients = r.ingredients; + } else if (r.inShape) { + ingredients = r.inShape.flat(); + } + for (let ingredient of ingredients) { + let ingredientName = this.getItemName(ingredient); + if (ingredientName === null) continue; + if (!recipe[ingredientName]) + recipe[ingredientName] = 0; + recipe[ingredientName]++; + } + recipes.push(recipe); + } + + return recipes; + } + + isSmeltable(itemName) { + const misc_smeltables = ['beef', 'chicken', 'cod', 'mutton', 'porkchop', 'rabbit', 'salmon', 'tropical_fish', 'potato', 'kelp', 'sand', 'cobblestone', 'clay_ball']; + return itemName.includes('raw') || itemName.includes('log') || misc_smeltables.includes(itemName); + } + + getSmeltingFuel(bot) { + let fuel = bot.inventory.items().find(i => i.name === 'coal' || i.name === 'charcoal') + if (fuel) + return fuel; + fuel = bot.inventory.items().find(i => i.name.includes('log') || i.name.includes('planks')) + if (fuel) + return fuel; + return bot.inventory.items().find(i => i.name === 'coal_block' || i.name === 'lava_bucket'); + } + + getFuelSmeltOutput(fuelName) { + if (fuelName === 'coal' || fuelName === 'charcoal') + return 8; + if (fuelName.includes('log') || fuelName.includes('planks')) + return 1.5 + if (fuelName === 'coal_block') + return 80; + if (fuelName === 'lava_bucket') + return 100; + return 0; + } + + getItemSmeltingIngredient(itemName) { + return { + baked_potato: 'potato', + steak: 'raw_beef', + cooked_chicken: 'raw_chicken', + cooked_cod: 'raw_cod', + cooked_mutton: 'raw_mutton', + cooked_porkchop: 'raw_porkchop', + cooked_rabbit: 'raw_rabbit', + cooked_salmon: 'raw_salmon', + dried_kelp: 'kelp', + iron_ingot: 'raw_iron', + gold_ingot: 'raw_gold', + copper_ingot: 'raw_copper', + glass: 'sand' + }[itemName]; + } + + getItemBlockSources(itemName) { + let itemId = this.getItemId(itemName); + let sources = []; + for (let block of this.getAllBlocks()) { + if (block.drops.includes(itemId)) { + sources.push(block.name); + } + } + return sources; + } + + getItemAnimalSource(itemName) { + return { + raw_beef: 'cow', + raw_chicken: 'chicken', + raw_cod: 'cod', + raw_mutton: 'sheep', + raw_porkchop: 'pig', + raw_rabbit: 'rabbit', + raw_salmon: 'salmon', + leather: 'cow', + wool: 'sheep' + }[itemName]; + } + + getBlockTool(blockName) { + let block = this.mcdata.blocksByName[blockName]; + if (!block || !block.harvestTools) { + return null; + } + return this.getItemName(Object.keys(block.harvestTools)[0]); // Double check first tool is always simplest + } + + makeItem(name, amount=1) { + return new this.Item(this.getItemId(name), amount); + } + + /** + * Returns the number of ingredients required to use the recipe once. + * + * @param {Recipe} recipe + * @returns {Object} an object describing the number of each ingredient. + */ + 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 = this.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 = this.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.} availableItems - The resources available; e.g, `{'cobble_stone': 7, 'stick': 10}` + * @param {Object.} 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'}` + */ + 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} + } + + /** + * @typedef {string} ItemName + * @typedef {string} BlockName + */ + + WOOD_TYPES = ['oak', 'spruce', 'birch', 'jungle', 'acacia', 'dark_oak']; + MATCHING_WOOD_BLOCKS = [ + 'log', + 'planks', + 'sign', + 'boat', + 'fence_gate', + 'door', + 'fence', + 'slab', + 'stairs', + 'button', + 'pressure_plate', + 'trapdoor' + ] + WOOL_COLORS = [ + 'white', + 'orange', + 'magenta', + 'light_blue', + 'yellow', + 'lime', + 'pink', + 'gray', + 'light_gray', + 'cyan', + 'purple', + 'blue', + 'brown', + 'green', + 'red', + 'black' + ] +} + +export const mc = new MinecraftData(); \ No newline at end of file diff --git a/src/utils/mcserver.js b/src/utils/mcserver.js index 3e9d29e..e5a5f05 100644 --- a/src/utils/mcserver.js +++ b/src/utils/mcserver.js @@ -1,3 +1,4 @@ +import settings from '../../settings.js'; import net from 'net'; import mc from 'minecraft-protocol'; @@ -81,4 +82,47 @@ export async function findServers(ip, earlyExit = false, timeout = 100) { console.log = originalConsoleLog; return servers; +} + +export async function getServer() { + let server = null; + let serverString = ""; + let serverVersion = ""; + + // Search for server + if (settings.port == -1) + { + console.log(`No port provided. Searching for LAN server on host ${settings.host}...`); + + await findServers(settings.host, true).then((servers) => { + if (servers.length > 0) + server = servers[0]; + }); + + if (server == null) + throw new Error(`No server found on LAN.`); + } + else + server = await serverInfo(settings.host, settings.port); + + // Server not found + if (server == null) + throw new Error(`Server not found. (Host: ${settings.host}, Port: ${settings.port}) Check the host and port in settings.js.`); + + serverString = `(Host: ${server.host}, Port: ${server.port}, Version: ${server.version})`; + + if (settings.minecraft_version === "auto") + serverVersion = server.version; + else + serverVersion = settings.minecraft_version; + + // Server version unsupported / mismatch + if (mc.supportedVersions.indexOf(serverVersion) === -1) + throw new Error(`A server was found ${serverString}, but version is unsupported. Supported versions are: ${mc.supportedVersions.join(", ")}.`); + else if (settings.minecraft_version !== "auto" && server.version !== settings.minecraft_version) + throw new Error(`A server was found ${serverString}, but version is incorrect. Expected ${settings.minecraft_version}, but found ${server.version}.`); + else + console.log(`Server found. ${serverString}`); + + return server; } \ No newline at end of file From 18300a8f13905527886aeca15561630f56626dca Mon Sep 17 00:00:00 2001 From: Zgrill2 Date: Thu, 6 Feb 2025 00:20:52 -0500 Subject: [PATCH 03/35] Add support for GPT models in Azure AI Studio --- profiles/azure.json | 15 +++++++++++++++ src/models/azure.js | 23 +++++++++++++++++++++++ src/models/prompter.js | 5 +++++ 3 files changed, 43 insertions(+) create mode 100644 profiles/azure.json create mode 100644 src/models/azure.js diff --git a/profiles/azure.json b/profiles/azure.json new file mode 100644 index 0000000..fbd382d --- /dev/null +++ b/profiles/azure.json @@ -0,0 +1,15 @@ +{ + "name": "azure", + "model": { + "api": "azure", + "url": "", + "model": "gpt-4o", + "api_version": "2024-08-01-preview" + }, + "embedding": { + "api": "azure", + "url": "", + "model": "text-embedding-ada-002", + "api_version": "2024-08-01-preview" + } +} \ No newline at end of file diff --git a/src/models/azure.js b/src/models/azure.js new file mode 100644 index 0000000..d8e2f4a --- /dev/null +++ b/src/models/azure.js @@ -0,0 +1,23 @@ +import { AzureOpenAI } from "openai"; +import { getKey } from '../utils/keys.js'; +import { GPT } from './gpt.js' + +export class AzureGPT extends GPT { + constructor(model_name, url, api_version, params) { + super(model_name, url) + + this.model_name = model_name; + this.params = params; + + let config = {} + + if (url) + config.endpoint = url; + + config.apiKey = getKey('OPENAI_API_KEY'); + config.deployment = model_name; // This must be what you named the deployment in Azure, not the model version itself + config.apiVersion = api_version; // This is required for Azure + + this.openai = new AzureOpenAI(config) + } +} \ No newline at end of file diff --git a/src/models/prompter.js b/src/models/prompter.js index 5295653..2260ead 100644 --- a/src/models/prompter.js +++ b/src/models/prompter.js @@ -19,6 +19,7 @@ import { HuggingFace } from './huggingface.js'; import { Qwen } from "./qwen.js"; import { Grok } from "./grok.js"; import { DeepSeek } from './deepseek.js'; +import { AzureGPT } from './azure.js'; export class Prompter { constructor(agent, fp) { @@ -72,6 +73,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 === 'azure') + this.embedding_model = new AzureGPT(embedding.model, embedding.url, embedding.api_version); else if (embedding.api === 'replicate') this.embedding_model = new ReplicateAPI(embedding.model, embedding.url); else if (embedding.api === 'ollama') @@ -139,6 +142,8 @@ export class Prompter { model = new Gemini(profile.model, profile.url, profile.params); else if (profile.api === 'openai') model = new GPT(profile.model, profile.url, profile.params); + else if (profile.api === 'azure') + model = new AzureGPT(profile.model, profile.url, profile.api_version, profile.params); else if (profile.api === 'anthropic') model = new Claude(profile.model, profile.url, profile.params); else if (profile.api === 'replicate') From d1bb34446f9d0683a1fea3d52b2beb00633e6028 Mon Sep 17 00:00:00 2001 From: Qu Yi Date: Sun, 20 Apr 2025 11:04:57 +0800 Subject: [PATCH 04/35] New Model Support: Mercury (diffusion large language models) --- README.md | 3 ++- keys.example.json | 3 ++- profiles/mercury.json | 15 +++++++++++++++ src/models/prompter.js | 7 ++++++- 4 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 profiles/mercury.json diff --git a/README.md b/README.md index 8c4a4e8..ebd251c 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Do not connect this bot to public servers with coding enabled. This project allo - [Minecraft Java Edition](https://www.minecraft.net/en-us/store/minecraft-java-bedrock-edition-pc) (up to v1.21.1, recommend v1.20.4) - [Node.js Installed](https://nodejs.org/) (at least v14) -- One of these: [OpenAI API Key](https://openai.com/blog/openai-api) | [Gemini API Key](https://aistudio.google.com/app/apikey) | [Anthropic API Key](https://docs.anthropic.com/claude/docs/getting-access-to-claude) | [Replicate API Key](https://replicate.com/) | [Hugging Face API Key](https://huggingface.co/) | [Groq API Key](https://console.groq.com/keys) | [Ollama Installed](https://ollama.com/download). | [Mistral API Key](https://docs.mistral.ai/getting-started/models/models_overview/) | [Qwen API Key [Intl.]](https://www.alibabacloud.com/help/en/model-studio/developer-reference/get-api-key)/[[cn]](https://help.aliyun.com/zh/model-studio/getting-started/first-api-call-to-qwen?) | [Novita AI API Key](https://novita.ai/settings?utm_source=github_mindcraft&utm_medium=github_readme&utm_campaign=link#key-management) | +- One of these: [OpenAI API Key](https://openai.com/blog/openai-api) | [Gemini API Key](https://aistudio.google.com/app/apikey) | [Anthropic API Key](https://docs.anthropic.com/claude/docs/getting-access-to-claude) | [Replicate API Key](https://replicate.com/) | [Hugging Face API Key](https://huggingface.co/) | [Groq API Key](https://console.groq.com/keys) | [Ollama Installed](https://ollama.com/download). | [Mistral API Key](https://docs.mistral.ai/getting-started/models/models_overview/) | [Qwen API Key [Intl.]](https://www.alibabacloud.com/help/en/model-studio/developer-reference/get-api-key)/[[cn]](https://help.aliyun.com/zh/model-studio/getting-started/first-api-call-to-qwen?) | [Novita AI API Key](https://novita.ai/settings?utm_source=github_mindcraft&utm_medium=github_readme&utm_campaign=link#key-management) | [Mercury API](https://platform.inceptionlabs.ai/docs) ## Install and Run @@ -53,6 +53,7 @@ You can configure the agent's name, model, and prompts in their profile like `an | `openrouter` | `OPENROUTER_API_KEY` | `openrouter/anthropic/claude-3.5-sonnet` | [docs](https://openrouter.ai/models) | | `glhf.chat` | `GHLF_API_KEY` | `glhf/hf:meta-llama/Llama-3.1-405B-Instruct` | [docs](https://glhf.chat/user-settings/api) | | `hyperbolic` | `HYPERBOLIC_API_KEY` | `hyperbolic/deepseek-ai/DeepSeek-V3` | [docs](https://docs.hyperbolic.xyz/docs/getting-started) | +| `mercury(EA)` | `MERCURY_API_KEY` | `mercury-coder-small` | [docs](https://www.inceptionlabs.ai/) | If you use Ollama, to install the models used by default (generation and embedding), execute the following terminal command: `ollama pull llama3.1 && ollama pull nomic-embed-text` diff --git a/keys.example.json b/keys.example.json index 99286c5..eb08814 100644 --- a/keys.example.json +++ b/keys.example.json @@ -13,5 +13,6 @@ "GHLF_API_KEY": "", "HYPERBOLIC_API_KEY": "", "NOVITA_API_KEY": "", - "OPENROUTER_API_KEY": "" + "OPENROUTER_API_KEY": "", + "MERCURY_API_KEY":"" } diff --git a/profiles/mercury.json b/profiles/mercury.json new file mode 100644 index 0000000..cac6d49 --- /dev/null +++ b/profiles/mercury.json @@ -0,0 +1,15 @@ +{ + "name": "Mercury", + + "cooldown": 5000, + + "model": { + "api": "mercury", + "url": "https://api.inceptionlabs.ai/v1", + "model": "mercury-coder-small" + }, + + "embedding": "openai", + + "description":"Official Website Introduction:The world’s first diffusion large language models" +} \ No newline at end of file diff --git a/src/models/prompter.js b/src/models/prompter.js index 46f7760..e988fc1 100644 --- a/src/models/prompter.js +++ b/src/models/prompter.js @@ -21,6 +21,7 @@ import { DeepSeek } from './deepseek.js'; import { Hyperbolic } from './hyperbolic.js'; import { GLHF } from './glhf.js'; import { OpenRouter } from './openrouter.js'; +import { Mercury} from "./mercury.js"; export class Prompter { constructor(agent, fp) { @@ -159,8 +160,10 @@ export class Prompter { profile.api = 'xai'; else if (profile.model.includes('deepseek')) profile.api = 'deepseek'; - else if (profile.model.includes('mistral')) + else if (profile.model.includes('mistral')) profile.api = 'mistral'; + else if (profile.model.includes('mercury')) + profile.api = 'mercury'; else throw new Error('Unknown model:', profile.model); } @@ -198,6 +201,8 @@ export class Prompter { model = new DeepSeek(profile.model, profile.url, profile.params); else if (profile.api === 'openrouter') model = new OpenRouter(profile.model.replace('openrouter/', ''), profile.url, profile.params); + else if (profile.api === 'mercury') + model = new Mercury(profile.model, profile.url, profile.params); else throw new Error('Unknown API:', profile.api); return model; From a100486896f5a69849148aa0a52994fbd3e2bc9c Mon Sep 17 00:00:00 2001 From: Qu Yi Date: Sun, 20 Apr 2025 11:06:31 +0800 Subject: [PATCH 05/35] Add `mercury.js` --- src/models/mercury.js | 92 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 src/models/mercury.js diff --git a/src/models/mercury.js b/src/models/mercury.js new file mode 100644 index 0000000..485cc8d --- /dev/null +++ b/src/models/mercury.js @@ -0,0 +1,92 @@ +import OpenAIApi from 'openai'; +import { getKey, hasKey } from '../utils/keys.js'; +import { strictFormat } from '../utils/text.js'; + +export class Mercury { + constructor(model_name, url, params) { + this.model_name = model_name; + this.params = params; + let config = {}; + if (url) + config.baseURL = url; + + config.apiKey = getKey('MERCURY_API_KEY'); + + this.openai = new OpenAIApi(config); + } + + async sendRequest(turns, systemMessage, stop_seq='***') { + if (typeof stop_seq === 'string') { + stop_seq = [stop_seq]; + } else if (!Array.isArray(stop_seq)) { // 处理其他非字符串非数组的无效输入 + stop_seq = []; // 或抛出错误,或其他处理方式 + } + let messages = [{'role': 'system', 'content': systemMessage}].concat(turns); + messages = strictFormat(messages); + const pack = { + model: this.model_name || "mercury-coder-small", + messages, + stop: stop_seq, + ...(this.params || {}) + }; + + + let res = null; + + try { + console.log('Awaiting mercury api response from model', this.model_name) + // console.log('Messages:', messages); + 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.') + res = completion.choices[0].message.content; + } + catch (err) { + if ((err.message == 'Context length exceeded' || err.code == 'context_length_exceeded') && turns.length > 1) { + console.log('Context length exceeded, trying again with shorter context.'); + return await this.sendRequest(turns.slice(1), systemMessage, stop_seq); + } else if (err.message.includes('image_url')) { + console.log(err); + res = 'Vision is only supported by certain models.'; + } else { + console.log(err); + res = 'My brain disconnected, try again.'; + } + } + return res; + } + + async sendVisionRequest(messages, systemMessage, imageBuffer) { + const imageMessages = [...messages]; + imageMessages.push({ + role: "user", + content: [ + { type: "text", text: systemMessage }, + { + type: "image_url", + image_url: { + url: `data:image/jpeg;base64,${imageBuffer.toString('base64')}` + } + } + ] + }); + + return this.sendRequest(imageMessages, systemMessage); + } + + async embed(text) { + if (text.length > 8191) + text = text.slice(0, 8191); + const embedding = await this.openai.embeddings.create({ + model: this.model_name || "text-embedding-3-small", + input: text, + encoding_format: "float", + }); + return embedding.data[0].embedding; + } + +} + + + From 23095ce0ed4121e6f1fa84619202922fa132b204 Mon Sep 17 00:00:00 2001 From: Qu Yi Date: Sun, 20 Apr 2025 11:41:04 +0800 Subject: [PATCH 06/35] Update `mercury.js` --- src/models/mercury.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/models/mercury.js b/src/models/mercury.js index 485cc8d..d4c4c3e 100644 --- a/src/models/mercury.js +++ b/src/models/mercury.js @@ -18,8 +18,8 @@ export class Mercury { async sendRequest(turns, systemMessage, stop_seq='***') { if (typeof stop_seq === 'string') { stop_seq = [stop_seq]; - } else if (!Array.isArray(stop_seq)) { // 处理其他非字符串非数组的无效输入 - stop_seq = []; // 或抛出错误,或其他处理方式 + } else if (!Array.isArray(stop_seq)) { + stop_seq = []; } let messages = [{'role': 'system', 'content': systemMessage}].concat(turns); messages = strictFormat(messages); From b1932cf93e7b02f0190fc3455dcb0dda48791aca Mon Sep 17 00:00:00 2001 From: Qu Yi Date: Fri, 2 May 2025 09:50:48 +0800 Subject: [PATCH 07/35] feat:add new model support `mercury`(diffusion large language models) --- settings.js | 1 + 1 file changed, 1 insertion(+) diff --git a/settings.js b/settings.js index b782097..55cbf4d 100644 --- a/settings.js +++ b/settings.js @@ -21,6 +21,7 @@ const settings = { // "./profiles/grok.json", // "./profiles/mistral.json", // "./profiles/deepseek.json", + //"./profiles/mercury.json", // using more than 1 profile requires you to /msg each bot indivually // individual profiles override values from the base profile From bba08979a87ff6cec3980a828f3ac13d941fdac8 Mon Sep 17 00:00:00 2001 From: Kevin Taylor Date: Sun, 11 May 2025 23:22:14 -0700 Subject: [PATCH 08/35] Added provider support for Cerebras --- keys.example.json | 3 ++- package.json | 1 + src/models/cerebras.js | 43 ++++++++++++++++++++++++++++++++++++++++++ src/models/prompter.js | 5 +++++ 4 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 src/models/cerebras.js diff --git a/keys.example.json b/keys.example.json index 99286c5..52204ae 100644 --- a/keys.example.json +++ b/keys.example.json @@ -13,5 +13,6 @@ "GHLF_API_KEY": "", "HYPERBOLIC_API_KEY": "", "NOVITA_API_KEY": "", - "OPENROUTER_API_KEY": "" + "OPENROUTER_API_KEY": "", + "CEREBRAS_API_KEY": "" } diff --git a/package.json b/package.json index bb3fd90..a37cf43 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "type": "module", "dependencies": { "@anthropic-ai/sdk": "^0.17.1", + "@cerebras/cerebras_cloud_sdk": "^1.0.0", "@google/generative-ai": "^0.2.1", "@huggingface/inference": "^2.8.1", "@mistralai/mistralai": "^1.1.0", diff --git a/src/models/cerebras.js b/src/models/cerebras.js new file mode 100644 index 0000000..21f1eee --- /dev/null +++ b/src/models/cerebras.js @@ -0,0 +1,43 @@ +import CerebrasSDK from '@cerebras/cerebras_cloud_sdk'; +import { strictFormat } from '../utils/text.js'; +import { getKey } from '../utils/keys.js'; + +export class Cerebras { + constructor(model_name, url, params) { + // Strip the prefix + this.model_name = model_name.replace('cerebras/', ''); + this.url = url; + this.params = params; + + // Initialize client with API key + this.client = new CerebrasSDK({ apiKey: getKey('CEREBRAS_API_KEY') }); + } + + async sendRequest(turns, systemMessage, stop_seq = '***') { + // Format messages array + const messages = strictFormat(turns); + messages.unshift({ role: 'system', content: systemMessage }); + + const pack = { + model: this.model_name || 'llama-4-scout-17b-16e-instruct', + messages, + stream: false, + ...(this.params || {}), + }; + + let res; + try { + const completion = await this.client.chat.completions.create(pack); + // OpenAI-compatible shape + res = completion.choices?.[0]?.message?.content || ''; + } catch (err) { + console.error('Cerebras API error:', err); + res = 'My brain disconnected, try again.'; + } + return res; + } + + async embed(text) { + throw new Error('Embeddings are not supported by Cerebras.'); + } +} diff --git a/src/models/prompter.js b/src/models/prompter.js index e05f5a8..22f23f7 100644 --- a/src/models/prompter.js +++ b/src/models/prompter.js @@ -22,6 +22,7 @@ import { Hyperbolic } from './hyperbolic.js'; import { GLHF } from './glhf.js'; import { OpenRouter } from './openrouter.js'; import { VLLM } from './vllm.js'; +import { Cerebras } from './cerebras.js'; import { promises as fs } from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; @@ -170,6 +171,8 @@ export class Prompter { profile.api = 'deepseek'; else if (profile.model.includes('mistral')) profile.api = 'mistral'; + else if (profile.model.startsWith('cerebras/')) + profile.api = 'cerebras'; else throw new Error('Unknown model:', profile.model); } @@ -209,6 +212,8 @@ export class Prompter { model = new OpenRouter(profile.model.replace('openrouter/', ''), profile.url, profile.params); else if (profile.api === 'vllm') model = new VLLM(profile.model.replace('vllm/', ''), profile.url, profile.params); + else if (profile.api === 'cerebras') + model = new Cerebras(profile.model.replace('cerebras/', ''), profile.url, profile.params); else throw new Error('Unknown API:', profile.api); return model; From c70b05a05621bec66fb8e6902f62898ed737e851 Mon Sep 17 00:00:00 2001 From: Kevin Taylor Date: Mon, 12 May 2025 10:52:24 -0700 Subject: [PATCH 09/35] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f291c2f..b51ea4d 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Do not connect this bot to public servers with coding enabled. This project allo - [Minecraft Java Edition](https://www.minecraft.net/en-us/store/minecraft-java-bedrock-edition-pc) (up to v1.21.1, recommend v1.20.4) - [Node.js Installed](https://nodejs.org/) (at least v14) -- One of these: [OpenAI API Key](https://openai.com/blog/openai-api) | [Gemini API Key](https://aistudio.google.com/app/apikey) | [Anthropic API Key](https://docs.anthropic.com/claude/docs/getting-access-to-claude) | [Replicate API Key](https://replicate.com/) | [Hugging Face API Key](https://huggingface.co/) | [Groq API Key](https://console.groq.com/keys) | [Ollama Installed](https://ollama.com/download). | [Mistral API Key](https://docs.mistral.ai/getting-started/models/models_overview/) | [Qwen API Key [Intl.]](https://www.alibabacloud.com/help/en/model-studio/developer-reference/get-api-key)/[[cn]](https://help.aliyun.com/zh/model-studio/getting-started/first-api-call-to-qwen?) | [Novita AI API Key](https://novita.ai/settings?utm_source=github_mindcraft&utm_medium=github_readme&utm_campaign=link#key-management) | +- One of these: [OpenAI API Key](https://openai.com/blog/openai-api) | [Gemini API Key](https://aistudio.google.com/app/apikey) | [Anthropic API Key](https://docs.anthropic.com/claude/docs/getting-access-to-claude) | [Replicate API Key](https://replicate.com/) | [Hugging Face API Key](https://huggingface.co/) | [Groq API Key](https://console.groq.com/keys) | [Ollama Installed](https://ollama.com/download). | [Mistral API Key](https://docs.mistral.ai/getting-started/models/models_overview/) | [Qwen API Key [Intl.]](https://www.alibabacloud.com/help/en/model-studio/developer-reference/get-api-key)/[[cn]](https://help.aliyun.com/zh/model-studio/getting-started/first-api-call-to-qwen?) | [Novita AI API Key](https://novita.ai/settings?utm_source=github_mindcraft&utm_medium=github_readme&utm_campaign=link#key-management) | [Cerebras API Key](https://cloud.cerebras.ai) ## Install and Run @@ -64,6 +64,7 @@ You can configure the agent's name, model, and prompts in their profile like `an | `glhf.chat` | `GHLF_API_KEY` | `glhf/hf:meta-llama/Llama-3.1-405B-Instruct` | [docs](https://glhf.chat/user-settings/api) | | `hyperbolic` | `HYPERBOLIC_API_KEY` | `hyperbolic/deepseek-ai/DeepSeek-V3` | [docs](https://docs.hyperbolic.xyz/docs/getting-started) | | `vllm` | n/a | `vllm/llama3` | n/a | +| `cerebras` | `CEREBRAS_API_KEY` | `cerebras/llama-3.3-70b` | [docs](https://inference-docs.cerebras.ai/introduction) | If you use Ollama, to install the models used by default (generation and embedding), execute the following terminal command: `ollama pull llama3.1 && ollama pull nomic-embed-text` From ff430ee0473533c7a88b28ccbc3e8f8c11faf65e Mon Sep 17 00:00:00 2001 From: Solenopsisbot <101841422+Solenopsisbot@users.noreply.github.com> Date: Tue, 13 May 2025 17:11:54 +1000 Subject: [PATCH 10/35] Update settings.js --- settings.js | 1 + 1 file changed, 1 insertion(+) diff --git a/settings.js b/settings.js index b782097..4ca090c 100644 --- a/settings.js +++ b/settings.js @@ -29,6 +29,7 @@ const settings = { "init_message": "Respond with hello world and your name", // sends to all on spawn "only_chat_with": [], // users that the bots listen to and send general messages to. if empty it will chat publicly "speak": false, // allows all bots to speak through system text-to-speech. works on windows, mac, on linux you need to `apt install espeak` + "chat_response": true, // enables or disables bots sending their responses to minecraft chat "language": "en", // translate to/from this language. Supports these language names: https://cloud.google.com/translate/docs/languages "show_bot_views": false, // show bot's view in browser at localhost:3000, 3001... From a6a5330075c3803d368a74f8a0dbf412e216e507 Mon Sep 17 00:00:00 2001 From: Solenopsisbot <101841422+Solenopsisbot@users.noreply.github.com> Date: Tue, 13 May 2025 17:13:14 +1000 Subject: [PATCH 11/35] Update mind_server.js --- src/server/mind_server.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/server/mind_server.js b/src/server/mind_server.js index eed71d7..1c79a2e 100644 --- a/src/server/mind_server.js +++ b/src/server/mind_server.js @@ -128,6 +128,9 @@ export function createMindServer(port = 8080) { console.error('Error: ', error); } }); + socket.on('response-message', (agentName, message) => { + io.emit('response-message', {agentName, message}); + }); }); server.listen(port, 'localhost', () => { From 0b8aeb8464920f35d7819b2727e69902cbb1b1b8 Mon Sep 17 00:00:00 2001 From: Solenopsisbot <101841422+Solenopsisbot@users.noreply.github.com> Date: Tue, 13 May 2025 17:13:58 +1000 Subject: [PATCH 12/35] Update agent_proxy.js --- src/agent/agent_proxy.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/agent/agent_proxy.js b/src/agent/agent_proxy.js index b0333b1..ba16f85 100644 --- a/src/agent/agent_proxy.js +++ b/src/agent/agent_proxy.js @@ -71,3 +71,7 @@ export const serverProxy = new AgentServerProxy(); export function sendBotChatToServer(agentName, json) { serverProxy.getSocket().emit('chat-message', agentName, json); } + +export function sendResponseToServer(agentName, message) { + serverProxy.getSocket().emit('response-message', agentName, message); +} From bc781b7db877bd7250deaebe238fec8941afd41a Mon Sep 17 00:00:00 2001 From: Solenopsisbot <101841422+Solenopsisbot@users.noreply.github.com> Date: Tue, 13 May 2025 17:16:03 +1000 Subject: [PATCH 13/35] Update agent.js --- src/agent/agent.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/agent/agent.js b/src/agent/agent.js index 7106783..93864b7 100644 --- a/src/agent/agent.js +++ b/src/agent/agent.js @@ -388,7 +388,8 @@ export class Agent { if (settings.speak) { say(to_translate); } - this.bot.chat(message); + if (settings.chat_response) {this.bot.chat(message);} + sendResponseToServer(this.name, message); } } From 0cd4dcd4205ec5d52c2b264144b222c1f05ae0fb Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Mon, 18 Aug 2025 23:21:57 -0500 Subject: [PATCH 14/35] lil fixes to smelting/pathfinding --- settings.js | 2 +- src/agent/action_manager.js | 2 +- src/agent/commands/actions.js | 7 +------ src/agent/modes.js | 2 +- 4 files changed, 4 insertions(+), 9 deletions(-) diff --git a/settings.js b/settings.js index 855fea7..c0f7705 100644 --- a/settings.js +++ b/settings.js @@ -7,7 +7,7 @@ const settings = { // the mindserver manages all agents and hosts the UI "mindserver_port": 8080, - "base_profile": "survival", // survival, creative, assistant, or god_mode + "base_profile": "survival", // survival, assistant, creative, or god_mode "profiles": [ "./andy.json", // "./profiles/gpt.json", diff --git a/src/agent/action_manager.js b/src/agent/action_manager.js index 470f507..9b9d0d2 100644 --- a/src/agent/action_manager.js +++ b/src/agent/action_manager.js @@ -69,7 +69,7 @@ export class ActionManager { else { this.recent_action_counter = 0; } - if (this.recent_action_counter > 2) { + if (this.recent_action_counter > 3) { console.warn('Fast action loop detected, cancelling resume.'); this.cancelResume(); // likely cause of repetition } diff --git a/src/agent/commands/actions.js b/src/agent/commands/actions.js index e321764..4e6acbb 100644 --- a/src/agent/commands/actions.js +++ b/src/agent/commands/actions.js @@ -278,12 +278,7 @@ export const actionsList = [ 'num': { type: 'int', description: 'The number of times to smelt the item.', domain: [1, Number.MAX_SAFE_INTEGER] } }, perform: runAsAction(async (agent, item_name, num) => { - let success = await skills.smeltItem(agent.bot, item_name, num); - if (success) { - setTimeout(() => { - agent.cleanKill('Safely restarting to update inventory.'); - }, 500); - } + await skills.smeltItem(agent.bot, item_name, num); }) }, { diff --git a/src/agent/modes.js b/src/agent/modes.js index cde53fc..ce196af 100644 --- a/src/agent/modes.js +++ b/src/agent/modes.js @@ -156,7 +156,7 @@ const modes_list = [ { name: 'hunting', description: 'Hunt nearby animals when idle.', - interrupts: [], + interrupts: ['action:followPlayer'], on: true, active: false, update: async function (agent) { From 2a38d310fcd26a49976da6c0d2bfddaa97ff2a3b Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Wed, 20 Aug 2025 13:08:59 -0500 Subject: [PATCH 15/35] refactor models for better modularity, use sweaterdog ollama/local --- src/models/_model_map.js | 88 +++++++++++++++ src/models/gemini.js | 6 +- src/models/{local.js => ollama.js} | 69 ++++++------ src/models/prompter.js | 166 ++++------------------------- 4 files changed, 150 insertions(+), 179 deletions(-) create mode 100644 src/models/_model_map.js rename src/models/{local.js => ollama.js} (60%) diff --git a/src/models/_model_map.js b/src/models/_model_map.js new file mode 100644 index 0000000..10fa893 --- /dev/null +++ b/src/models/_model_map.js @@ -0,0 +1,88 @@ +import { Gemini } from './gemini.js'; +import { GPT } from './gpt.js'; +import { Claude } from './claude.js'; +import { Mistral } from './mistral.js'; +import { ReplicateAPI } from './replicate.js'; +import { Ollama } from './ollama.js'; +import { Novita } from './novita.js'; +import { GroqCloudAPI } from './groq.js'; +import { HuggingFace } from './huggingface.js'; +import { Qwen } from "./qwen.js"; +import { Grok } from "./grok.js"; +import { DeepSeek } from './deepseek.js'; +import { Hyperbolic } from './hyperbolic.js'; +import { GLHF } from './glhf.js'; +import { OpenRouter } from './openrouter.js'; +import { VLLM } from './vllm.js'; + +// Add new models here. +// It maps api prefixes to model classes, eg 'openai/gpt-4o' -> GPT +const apiMap = { + 'openai': GPT, + 'google': Gemini, + 'anthropic': Claude, + 'replicate': ReplicateAPI, + 'ollama': Ollama, + 'mistral': Mistral, + 'groq': GroqCloudAPI, + 'huggingface': HuggingFace, + 'novita': Novita, + 'qwen': Qwen, + 'grok': Grok, + 'deepseek': DeepSeek, + 'hyperbolic': Hyperbolic, + 'glhf': GLHF, + 'openrouter': OpenRouter, + 'vllm': VLLM, +} + +export function selectAPI(profile) { + if (typeof profile === 'string' || profile instanceof String) { + profile = {model: profile}; + } + const api = Object.keys(apiMap).find(key => profile.model.startsWith(key)); + if (api) { + profile.api = api; + } + else { + // backwards compatibility with local->ollama + if (profile.model.includes('local')) { + profile.api = 'ollama'; + profile.model = profile.model.replace('local/', ''); + } + // check for some common models that do not require prefixes + else if (profile.model.includes('gpt') || profile.model.includes('o1')|| profile.model.includes('o3')) + profile.api = 'openai'; + else if (profile.model.includes('claude')) + profile.api = 'anthropic'; + else if (profile.model.includes('gemini')) + profile.api = "google"; + else if (profile.model.includes('grok')) + profile.api = 'grok'; + else if (profile.model.includes('mistral')) + profile.api = 'mistral'; + else if (profile.model.includes('deepseek')) + profile.api = 'deepseek'; + else if (profile.model.includes('qwen')) + profile.api = 'qwen'; + } + if (!profile.api) { + throw new Error('Unknown model:', profile.model); + } + let model_name = profile.model.replace(profile.api + '/', ''); // remove prefix + profile.model = model_name === "" ? null : model_name; // if model is empty, set to null + return profile; +} + +export function createModel(profile) { + if (!!apiMap[profile.model]) { + // if the model value is an api (instead of a specific model name) + // then set model to null so it uses the default model for that api + profile.model = null; + } + if (!apiMap[profile.api]) { + throw new Error('Unknown api:', profile.api); + } + const model = new apiMap[profile.api](profile.model, profile.url, profile.params); + return model; +} \ No newline at end of file diff --git a/src/models/gemini.js b/src/models/gemini.js index 4d24c93..4e3af14 100644 --- a/src/models/gemini.js +++ b/src/models/gemini.js @@ -142,15 +142,15 @@ export class Gemini { } async embed(text) { - let model; + let model = this.model_name || "text-embedding-004"; if (this.url) { model = this.genAI.getGenerativeModel( - { model: "text-embedding-004" }, + { model }, { baseUrl: this.url } ); } else { model = this.genAI.getGenerativeModel( - { model: "text-embedding-004" } + { model } ); } diff --git a/src/models/local.js b/src/models/ollama.js similarity index 60% rename from src/models/local.js rename to src/models/ollama.js index e51bcf8..064d2ad 100644 --- a/src/models/local.js +++ b/src/models/ollama.js @@ -1,6 +1,6 @@ import { strictFormat } from '../utils/text.js'; -export class Local { +export class Ollama { constructor(model_name, url, params) { this.model_name = model_name; this.params = params; @@ -10,11 +10,9 @@ export class Local { } async sendRequest(turns, systemMessage) { - let model = this.model_name || 'llama3.1'; // Updated to llama3.1, as it is more performant than llama3 + let model = this.model_name || 'sweaterdog/andy-4:micro-q5_k_m'; let messages = strictFormat(turns); messages.unshift({ role: 'system', content: systemMessage }); - - // We'll attempt up to 5 times for models with deepseek-r1-esk reasoning if the tags are mismatched. const maxAttempts = 5; let attempt = 0; let finalRes = null; @@ -24,14 +22,14 @@ export class Local { console.log(`Awaiting local response... (model: ${model}, attempt: ${attempt})`); let res = null; try { - res = await this.send(this.chat_endpoint, { + let apiResponse = await this.send(this.chat_endpoint, { model: model, messages: messages, stream: false, ...(this.params || {}) }); - if (res) { - res = res['message']['content']; + if (apiResponse) { + res = apiResponse['message']['content']; } else { res = 'No response data.'; } @@ -43,36 +41,27 @@ export class Local { console.log(err); res = 'My brain disconnected, try again.'; } - } - // If the model name includes "deepseek-r1" or "Andy-3.5-reasoning", then handle the block. - const hasOpenTag = res.includes(""); - const hasCloseTag = res.includes(""); - - // If there's a partial mismatch, retry to get a complete response. - if ((hasOpenTag && !hasCloseTag)) { - console.warn("Partial block detected. Re-generating..."); - continue; - } - - // If is present but is not, prepend - if (hasCloseTag && !hasOpenTag) { - res = '' + res; - } - // Changed this so if the model reasons, using and but doesn't start the message with , ges prepended to the message so no error occur. - - // If both tags appear, remove them (and everything inside). - if (hasOpenTag && hasCloseTag) { - res = res.replace(/[\s\S]*?<\/think>/g, ''); - } + const hasOpenTag = res.includes(""); + const hasCloseTag = res.includes(""); + if ((hasOpenTag && !hasCloseTag)) { + console.warn("Partial block detected. Re-generating..."); + if (attempt < maxAttempts) continue; + } + if (hasCloseTag && !hasOpenTag) { + res = '' + res; + } + if (hasOpenTag && hasCloseTag) { + res = res.replace(/[\s\S]*?<\/think>/g, '').trim(); + } finalRes = res; - break; // Exit the loop if we got a valid response. + break; } if (finalRes == null) { - console.warn("Could not get a valid block or normal response after max attempts."); + console.warn("Could not get a valid response after max attempts."); finalRes = 'I thought too hard, sorry, try again.'; } return finalRes; @@ -104,4 +93,22 @@ export class Local { } return data; } -} + + async sendVisionRequest(messages, systemMessage, imageBuffer) { + const imageMessages = [...messages]; + imageMessages.push({ + role: "user", + content: [ + { type: "text", text: systemMessage }, + { + type: "image_url", + image_url: { + url: `data:image/jpeg;base64,${imageBuffer.toString('base64')}` + } + } + ] + }); + + return this.sendRequest(imageMessages, systemMessage); + } +} \ No newline at end of file diff --git a/src/models/prompter.js b/src/models/prompter.js index 89d5fe9..a8c4db7 100644 --- a/src/models/prompter.js +++ b/src/models/prompter.js @@ -5,26 +5,10 @@ import { SkillLibrary } from "../agent/library/skill_library.js"; import { stringifyTurns } from '../utils/text.js'; import { getCommand } from '../agent/commands/index.js'; import settings from '../agent/settings.js'; - -import { Gemini } from './gemini.js'; -import { GPT } from './gpt.js'; -import { Claude } from './claude.js'; -import { Mistral } from './mistral.js'; -import { ReplicateAPI } from './replicate.js'; -import { Local } from './local.js'; -import { Novita } from './novita.js'; -import { GroqCloudAPI } from './groq.js'; -import { HuggingFace } from './huggingface.js'; -import { Qwen } from "./qwen.js"; -import { Grok } from "./grok.js"; -import { DeepSeek } from './deepseek.js'; -import { Hyperbolic } from './hyperbolic.js'; -import { GLHF } from './glhf.js'; -import { OpenRouter } from './openrouter.js'; -import { VLLM } from './vllm.js'; import { promises as fs } from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; +import { selectAPI, createModel } from './_model_map.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -66,70 +50,46 @@ export class Prompter { this.last_prompt_time = 0; this.awaiting_coding = false; - // try to get "max_tokens" parameter, else null + // for backwards compatibility, move max_tokens to params let max_tokens = null; if (this.profile.max_tokens) max_tokens = this.profile.max_tokens; - let chat_model_profile = this._selectAPI(this.profile.model); - this.chat_model = this._createModel(chat_model_profile); + let chat_model_profile = selectAPI(this.profile.model); + this.chat_model = createModel(chat_model_profile); if (this.profile.code_model) { - let code_model_profile = this._selectAPI(this.profile.code_model); - this.code_model = this._createModel(code_model_profile); + let code_model_profile = selectAPI(this.profile.code_model); + this.code_model = createModel(code_model_profile); } else { this.code_model = this.chat_model; } if (this.profile.vision_model) { - let vision_model_profile = this._selectAPI(this.profile.vision_model); - this.vision_model = this._createModel(vision_model_profile); + let vision_model_profile = selectAPI(this.profile.vision_model); + this.vision_model = createModel(vision_model_profile); } else { this.vision_model = this.chat_model; } - let embedding = this.profile.embedding; - if (embedding === undefined) { - if (chat_model_profile.api !== 'ollama') - embedding = {api: chat_model_profile.api}; - else - embedding = {api: 'none'}; - } - else if (typeof embedding === 'string' || embedding instanceof String) - embedding = {api: embedding}; - - console.log('Using embedding settings:', embedding); - - try { - if (embedding.api === 'google') - 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 if (embedding.api === 'qwen') - this.embedding_model = new Qwen(embedding.model, embedding.url); - else if (embedding.api === 'mistral') - this.embedding_model = new Mistral(embedding.model, embedding.url); - else if (embedding.api === 'huggingface') - this.embedding_model = new HuggingFace(embedding.model, embedding.url); - else if (embedding.api === 'novita') - this.embedding_model = new Novita(embedding.model, embedding.url); - else { - this.embedding_model = null; - let embedding_name = embedding ? embedding.api : '[NOT SPECIFIED]' - console.warn('Unsupported embedding: ' + embedding_name + '. Using word-overlap instead, expect reduced performance. Recommend using a supported embedding model. See Readme.'); + + let embedding_model_profile = null; + if (this.profile.embedding) { + try { + embedding_model_profile = selectAPI(this.profile.embedding); + } catch (e) { + embedding_model_profile = null; } } - catch (err) { - console.warn('Warning: Failed to initialize embedding model:', err.message); - console.log('Continuing anyway, using word-overlap instead.'); - this.embedding_model = null; + if (embedding_model_profile) { + this.embedding_model = createModel(embedding_model_profile); } + else { + this.embedding_model = createModel({api: chat_model_profile.api}); + } + this.skill_libary = new SkillLibrary(agent, this.embedding_model); mkdirSync(`./bots/${name}`, { recursive: true }); writeFileSync(`./bots/${name}/last_profile.json`, JSON.stringify(this.profile, null, 4), (err) => { @@ -140,88 +100,6 @@ export class Prompter { }); } - _selectAPI(profile) { - if (typeof profile === 'string' || profile instanceof String) { - profile = {model: profile}; - } - if (!profile.api) { - if (profile.model.includes('openrouter/')) - profile.api = 'openrouter'; // must do first because shares names with other models - else if (profile.model.includes('ollama/')) - profile.api = 'ollama'; // also must do early because shares names with other models - else if (profile.model.includes('gemini')) - profile.api = 'google'; - else if (profile.model.includes('vllm/')) - profile.api = 'vllm'; - else if (profile.model.includes('gpt') || profile.model.includes('o1')|| profile.model.includes('o3')) - profile.api = 'openai'; - else if (profile.model.includes('claude')) - profile.api = 'anthropic'; - else if (profile.model.includes('huggingface/')) - profile.api = "huggingface"; - else if (profile.model.includes('replicate/')) - profile.api = 'replicate'; - else if (profile.model.includes('mistralai/') || profile.model.includes("mistral/")) - model_profile.api = 'mistral'; - else if (profile.model.includes("groq/") || profile.model.includes("groqcloud/")) - profile.api = 'groq'; - else if (profile.model.includes("glhf/")) - profile.api = 'glhf'; - else if (profile.model.includes("hyperbolic/")) - profile.api = 'hyperbolic'; - else if (profile.model.includes('novita/')) - profile.api = 'novita'; - else if (profile.model.includes('qwen')) - profile.api = 'qwen'; - else if (profile.model.includes('grok')) - profile.api = 'xai'; - else if (profile.model.includes('deepseek')) - profile.api = 'deepseek'; - else if (profile.model.includes('mistral')) - profile.api = 'mistral'; - else - throw new Error('Unknown model:', profile.model); - } - return profile; - } - _createModel(profile) { - let model = null; - if (profile.api === 'google') - model = new Gemini(profile.model, profile.url, profile.params); - else if (profile.api === 'openai') - model = new GPT(profile.model, profile.url, profile.params); - else if (profile.api === 'anthropic') - model = new Claude(profile.model, profile.url, profile.params); - else if (profile.api === 'replicate') - model = new ReplicateAPI(profile.model.replace('replicate/', ''), profile.url, profile.params); - else if (profile.api === 'ollama') - model = new Local(profile.model.replace('ollama/', ''), profile.url, profile.params); - else if (profile.api === 'mistral') - model = new Mistral(profile.model, profile.url, profile.params); - else if (profile.api === 'groq') - model = new GroqCloudAPI(profile.model.replace('groq/', '').replace('groqcloud/', ''), profile.url, profile.params); - else if (profile.api === 'huggingface') - model = new HuggingFace(profile.model, profile.url, profile.params); - else if (profile.api === 'glhf') - model = new GLHF(profile.model.replace('glhf/', ''), profile.url, profile.params); - else if (profile.api === 'hyperbolic') - model = new Hyperbolic(profile.model.replace('hyperbolic/', ''), profile.url, profile.params); - else if (profile.api === 'novita') - model = new Novita(profile.model.replace('novita/', ''), profile.url, profile.params); - else if (profile.api === 'qwen') - model = new Qwen(profile.model, profile.url, profile.params); - else if (profile.api === 'xai') - model = new Grok(profile.model, profile.url, profile.params); - else if (profile.api === 'deepseek') - model = new DeepSeek(profile.model, profile.url, profile.params); - else if (profile.api === 'openrouter') - model = new OpenRouter(profile.model.replace('openrouter/', ''), profile.url, profile.params); - else if (profile.api === 'vllm') - model = new VLLM(profile.model.replace('vllm/', ''), profile.url, profile.params); - else - throw new Error('Unknown API:', profile.api); - return model; - } getName() { return this.profile.name; } @@ -482,6 +360,4 @@ export class Prompter { logFile = path.join(logDir, logFile); await fs.appendFile(logFile, String(logEntry), 'utf-8'); } - - } From 14eff85120063697df43d43b614dc0c52e81a717 Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Wed, 20 Aug 2025 18:04:00 -0500 Subject: [PATCH 16/35] dynamically load models --- src/models/_model_map.js | 123 +++++++++++++++++++------------------- src/models/claude.js | 1 + src/models/deepseek.js | 1 + src/models/gemini.js | 1 + src/models/glhf.js | 1 + src/models/gpt.js | 11 ++-- src/models/grok.js | 1 + src/models/groq.js | 2 +- src/models/huggingface.js | 1 + src/models/hyperbolic.js | 1 + src/models/mistral.js | 1 + src/models/novita.js | 1 + src/models/ollama.js | 1 + src/models/openrouter.js | 1 + src/models/qwen.js | 1 + src/models/replicate.js | 1 + src/models/vllm.js | 6 +- 17 files changed, 85 insertions(+), 70 deletions(-) diff --git a/src/models/_model_map.js b/src/models/_model_map.js index 10fa893..be43893 100644 --- a/src/models/_model_map.js +++ b/src/models/_model_map.js @@ -1,73 +1,74 @@ -import { Gemini } from './gemini.js'; -import { GPT } from './gpt.js'; -import { Claude } from './claude.js'; -import { Mistral } from './mistral.js'; -import { ReplicateAPI } from './replicate.js'; -import { Ollama } from './ollama.js'; -import { Novita } from './novita.js'; -import { GroqCloudAPI } from './groq.js'; -import { HuggingFace } from './huggingface.js'; -import { Qwen } from "./qwen.js"; -import { Grok } from "./grok.js"; -import { DeepSeek } from './deepseek.js'; -import { Hyperbolic } from './hyperbolic.js'; -import { GLHF } from './glhf.js'; -import { OpenRouter } from './openrouter.js'; -import { VLLM } from './vllm.js'; +import { promises as fs } from 'fs'; +import path from 'path'; +import { fileURLToPath, pathToFileURL } from 'url'; -// Add new models here. -// It maps api prefixes to model classes, eg 'openai/gpt-4o' -> GPT -const apiMap = { - 'openai': GPT, - 'google': Gemini, - 'anthropic': Claude, - 'replicate': ReplicateAPI, - 'ollama': Ollama, - 'mistral': Mistral, - 'groq': GroqCloudAPI, - 'huggingface': HuggingFace, - 'novita': Novita, - 'qwen': Qwen, - 'grok': Grok, - 'deepseek': DeepSeek, - 'hyperbolic': Hyperbolic, - 'glhf': GLHF, - 'openrouter': OpenRouter, - 'vllm': VLLM, -} +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Dynamically discover model classes in this directory. +// Each model class must export a static `prefix` string. +const apiMap = await (async () => { + const map = {}; + const files = (await fs.readdir(__dirname)) + .filter(f => f.endsWith('.js') && f !== '_model_map.js' && f !== 'prompter.js'); + for (const file of files) { + try { + const moduleUrl = pathToFileURL(path.join(__dirname, file)).href; + const mod = await import(moduleUrl); + for (const exported of Object.values(mod)) { + if (typeof exported === 'function' && Object.prototype.hasOwnProperty.call(exported, 'prefix')) { + const prefix = exported.prefix; + if (typeof prefix === 'string' && prefix.length > 0) { + map[prefix] = exported; + } + } + } + } catch (e) { + console.warn('Failed to load model module:', file, e?.message || e); + } + } + return map; +})(); export function selectAPI(profile) { if (typeof profile === 'string' || profile instanceof String) { profile = {model: profile}; } - const api = Object.keys(apiMap).find(key => profile.model.startsWith(key)); - if (api) { - profile.api = api; - } - else { - // backwards compatibility with local->ollama - if (profile.model.includes('local')) { - profile.api = 'ollama'; - profile.model = profile.model.replace('local/', ''); + // backwards compatibility with local->ollama + if (profile.api?.includes('local') || profile.model?.includes('local')) { + profile.api = 'ollama'; + if (profile.model) { + profile.model = profile.model.replace('local', 'ollama'); } - // check for some common models that do not require prefixes - else if (profile.model.includes('gpt') || profile.model.includes('o1')|| profile.model.includes('o3')) - profile.api = 'openai'; - else if (profile.model.includes('claude')) - profile.api = 'anthropic'; - else if (profile.model.includes('gemini')) - profile.api = "google"; - else if (profile.model.includes('grok')) - profile.api = 'grok'; - else if (profile.model.includes('mistral')) - profile.api = 'mistral'; - else if (profile.model.includes('deepseek')) - profile.api = 'deepseek'; - else if (profile.model.includes('qwen')) - profile.api = 'qwen'; } if (!profile.api) { - throw new Error('Unknown model:', profile.model); + const api = Object.keys(apiMap).find(key => profile.model?.startsWith(key)); + if (api) { + profile.api = api; + } + else { + // check for some common models that do not require prefixes + if (profile.model.includes('gpt') || profile.model.includes('o1')|| profile.model.includes('o3')) + profile.api = 'openai'; + else if (profile.model.includes('claude')) + profile.api = 'anthropic'; + else if (profile.model.includes('gemini')) + profile.api = "google"; + else if (profile.model.includes('grok')) + profile.api = 'grok'; + else if (profile.model.includes('mistral')) + profile.api = 'mistral'; + else if (profile.model.includes('deepseek')) + profile.api = 'deepseek'; + else if (profile.model.includes('qwen')) + profile.api = 'qwen'; + } + if (!profile.api) { + throw new Error('Unknown model:', profile.model); + } + } + if (!apiMap[profile.api]) { + throw new Error('Unknown api:', profile.api); } let model_name = profile.model.replace(profile.api + '/', ''); // remove prefix profile.model = model_name === "" ? null : model_name; // if model is empty, set to null diff --git a/src/models/claude.js b/src/models/claude.js index d6e48bc..c42d2e6 100644 --- a/src/models/claude.js +++ b/src/models/claude.js @@ -3,6 +3,7 @@ import { strictFormat } from '../utils/text.js'; import { getKey } from '../utils/keys.js'; export class Claude { + static prefix = 'anthropic'; constructor(model_name, url, params) { this.model_name = model_name; this.params = params || {}; diff --git a/src/models/deepseek.js b/src/models/deepseek.js index da98ba2..5596fa8 100644 --- a/src/models/deepseek.js +++ b/src/models/deepseek.js @@ -3,6 +3,7 @@ import { getKey, hasKey } from '../utils/keys.js'; import { strictFormat } from '../utils/text.js'; export class DeepSeek { + static prefix = 'deepseek'; constructor(model_name, url, params) { this.model_name = model_name; this.params = params; diff --git a/src/models/gemini.js b/src/models/gemini.js index 4e3af14..75a20e0 100644 --- a/src/models/gemini.js +++ b/src/models/gemini.js @@ -3,6 +3,7 @@ import { toSinglePrompt, strictFormat } from '../utils/text.js'; import { getKey } from '../utils/keys.js'; export class Gemini { + static prefix = 'google'; constructor(model_name, url, params) { this.model_name = model_name; this.params = params; diff --git a/src/models/glhf.js b/src/models/glhf.js index d41b843..b237c8d 100644 --- a/src/models/glhf.js +++ b/src/models/glhf.js @@ -2,6 +2,7 @@ import OpenAIApi from 'openai'; import { getKey } from '../utils/keys.js'; export class GLHF { + static prefix = 'glhf'; constructor(model_name, url) { this.model_name = model_name; const apiKey = getKey('GHLF_API_KEY'); diff --git a/src/models/gpt.js b/src/models/gpt.js index e8e5c5c..ea7d600 100644 --- a/src/models/gpt.js +++ b/src/models/gpt.js @@ -3,6 +3,7 @@ import { getKey, hasKey } from '../utils/keys.js'; import { strictFormat } from '../utils/text.js'; export class GPT { + static prefix = 'openai'; constructor(model_name, url, params) { this.model_name = model_name; this.params = params; @@ -22,20 +23,21 @@ export class GPT { async sendRequest(turns, systemMessage, stop_seq='***') { let messages = [{'role': 'system', 'content': systemMessage}].concat(turns); messages = strictFormat(messages); + let model = this.model_name || "gpt-4o-mini"; const pack = { - model: this.model_name || "gpt-3.5-turbo", + model: model, messages, stop: stop_seq, ...(this.params || {}) }; - if (this.model_name.includes('o1') || this.model_name.includes('o3') || this.model_name.includes('5')) { + if (model.includes('o1') || model.includes('o3') || model.includes('5')) { delete pack.stop; } let res = null; try { - console.log('Awaiting openai api response from model', this.model_name) + console.log('Awaiting openai api response from model', model) // console.log('Messages:', messages); let completion = await this.openai.chat.completions.create(pack); if (completion.choices[0].finish_reason == 'length') @@ -88,6 +90,3 @@ export class GPT { } } - - - diff --git a/src/models/grok.js b/src/models/grok.js index 2878a10..0753f10 100644 --- a/src/models/grok.js +++ b/src/models/grok.js @@ -3,6 +3,7 @@ import { getKey } from '../utils/keys.js'; // xAI doesn't supply a SDK for their models, but fully supports OpenAI and Anthropic SDKs export class Grok { + static prefix = 'grok'; constructor(model_name, url, params) { this.model_name = model_name; this.url = url; diff --git a/src/models/groq.js b/src/models/groq.js index e4e8f3b..9da88c7 100644 --- a/src/models/groq.js +++ b/src/models/groq.js @@ -6,6 +6,7 @@ import { getKey } from '../utils/keys.js'; // Umbrella class for everything under the sun... That GroqCloud provides, that is. export class GroqCloudAPI { + static prefix = 'groq'; constructor(model_name, url, params) { @@ -63,7 +64,6 @@ export class GroqCloudAPI { if (err.message.includes("content must be a string")) { res = "Vision is only supported by certain models."; } else { - console.log(this.model_name); res = "My brain disconnected, try again."; } console.log(err); diff --git a/src/models/huggingface.js b/src/models/huggingface.js index 80c36e8..91fbdfd 100644 --- a/src/models/huggingface.js +++ b/src/models/huggingface.js @@ -3,6 +3,7 @@ import { getKey } from '../utils/keys.js'; import { HfInference } from "@huggingface/inference"; export class HuggingFace { + static prefix = 'huggingface'; constructor(model_name, url, params) { // Remove 'huggingface/' prefix if present this.model_name = model_name.replace('huggingface/', ''); diff --git a/src/models/hyperbolic.js b/src/models/hyperbolic.js index a2ccc48..f483b69 100644 --- a/src/models/hyperbolic.js +++ b/src/models/hyperbolic.js @@ -1,6 +1,7 @@ import { getKey } from '../utils/keys.js'; export class Hyperbolic { + static prefix = 'hyperbolic'; constructor(modelName, apiUrl) { this.modelName = modelName || "deepseek-ai/DeepSeek-V3"; this.apiUrl = apiUrl || "https://api.hyperbolic.xyz/v1/chat/completions"; diff --git a/src/models/mistral.js b/src/models/mistral.js index 72448f1..536b386 100644 --- a/src/models/mistral.js +++ b/src/models/mistral.js @@ -3,6 +3,7 @@ import { getKey } from '../utils/keys.js'; import { strictFormat } from '../utils/text.js'; export class Mistral { + static prefix = 'mistral'; #client; constructor(model_name, url, params) { diff --git a/src/models/novita.js b/src/models/novita.js index 8f2dd08..380fa4c 100644 --- a/src/models/novita.js +++ b/src/models/novita.js @@ -4,6 +4,7 @@ import { strictFormat } from '../utils/text.js'; // llama, mistral export class Novita { + static prefix = 'novita'; constructor(model_name, url, params) { this.model_name = model_name.replace('novita/', ''); this.url = url || 'https://api.novita.ai/v3/openai'; diff --git a/src/models/ollama.js b/src/models/ollama.js index 064d2ad..37d8557 100644 --- a/src/models/ollama.js +++ b/src/models/ollama.js @@ -1,6 +1,7 @@ import { strictFormat } from '../utils/text.js'; export class Ollama { + static prefix = 'ollama'; constructor(model_name, url, params) { this.model_name = model_name; this.params = params; diff --git a/src/models/openrouter.js b/src/models/openrouter.js index 5cbc090..ca0782b 100644 --- a/src/models/openrouter.js +++ b/src/models/openrouter.js @@ -3,6 +3,7 @@ import { getKey, hasKey } from '../utils/keys.js'; import { strictFormat } from '../utils/text.js'; export class OpenRouter { + static prefix = 'openrouter'; constructor(model_name, url) { this.model_name = model_name; diff --git a/src/models/qwen.js b/src/models/qwen.js index 4dfacfe..a768b5b 100644 --- a/src/models/qwen.js +++ b/src/models/qwen.js @@ -3,6 +3,7 @@ import { getKey, hasKey } from '../utils/keys.js'; import { strictFormat } from '../utils/text.js'; export class Qwen { + static prefix = 'qwen'; constructor(model_name, url, params) { this.model_name = model_name; this.params = params; diff --git a/src/models/replicate.js b/src/models/replicate.js index c8c3ba3..aa296c5 100644 --- a/src/models/replicate.js +++ b/src/models/replicate.js @@ -4,6 +4,7 @@ import { getKey } from '../utils/keys.js'; // llama, mistral export class ReplicateAPI { + static prefix = 'replicate'; constructor(model_name, url, params) { this.model_name = model_name; this.url = url; diff --git a/src/models/vllm.js b/src/models/vllm.js index e9116ef..d821983 100644 --- a/src/models/vllm.js +++ b/src/models/vllm.js @@ -6,6 +6,7 @@ import { getKey, hasKey } from '../utils/keys.js'; import { strictFormat } from '../utils/text.js'; export class VLLM { + static prefix = 'vllm'; constructor(model_name, url) { this.model_name = model_name; @@ -23,13 +24,14 @@ export class VLLM { async sendRequest(turns, systemMessage, stop_seq = '***') { let messages = [{ 'role': 'system', 'content': systemMessage }].concat(turns); + let model = this.model_name || "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B"; - if (this.model_name.includes('deepseek') || this.model_name.includes('qwen')) { + if (model.includes('deepseek') || model.includes('qwen')) { messages = strictFormat(messages); } const pack = { - model: this.model_name || "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B", + model: model, messages, stop: stop_seq, }; From 17cabc9df8eb90c90f9b2c6244940b6eeb6239a3 Mon Sep 17 00:00:00 2001 From: Sweaterdog Date: Fri, 22 Aug 2025 12:48:18 -0700 Subject: [PATCH 17/35] Update default model version in sendRequest method --- src/models/ollama.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/models/ollama.js b/src/models/ollama.js index 37d8557..49a80e0 100644 --- a/src/models/ollama.js +++ b/src/models/ollama.js @@ -11,7 +11,7 @@ export class Ollama { } async sendRequest(turns, systemMessage) { - let model = this.model_name || 'sweaterdog/andy-4:micro-q5_k_m'; + let model = this.model_name || 'sweaterdog/andy-4:micro-q8_0'; let messages = strictFormat(turns); messages.unshift({ role: 'system', content: systemMessage }); const maxAttempts = 5; @@ -112,4 +112,4 @@ export class Ollama { return this.sendRequest(imageMessages, systemMessage); } -} \ No newline at end of file +} From 66409185dfbd30d8460f927c11a31dc70d373671 Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Sat, 23 Aug 2025 11:45:14 -0500 Subject: [PATCH 18/35] revert change, should still restart after smelt --- src/agent/commands/actions.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/agent/commands/actions.js b/src/agent/commands/actions.js index 4e6acbb..e321764 100644 --- a/src/agent/commands/actions.js +++ b/src/agent/commands/actions.js @@ -278,7 +278,12 @@ export const actionsList = [ 'num': { type: 'int', description: 'The number of times to smelt the item.', domain: [1, Number.MAX_SAFE_INTEGER] } }, perform: runAsAction(async (agent, item_name, num) => { - await skills.smeltItem(agent.bot, item_name, num); + let success = await skills.smeltItem(agent.bot, item_name, num); + if (success) { + setTimeout(() => { + agent.cleanKill('Safely restarting to update inventory.'); + }, 500); + } }) }, { From cb263c1b02426158e8a2230514ae7f8060e6a727 Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Sat, 23 Aug 2025 11:47:07 -0500 Subject: [PATCH 19/35] clear furnace after smelt, better following logic --- src/agent/library/skills.js | 47 +++++++++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/src/agent/library/skills.js b/src/agent/library/skills.js index 68cd4d0..4dbb6b5 100644 --- a/src/agent/library/skills.js +++ b/src/agent/library/skills.js @@ -228,28 +228,33 @@ export async function smeltItem(bot, itemName, num=1) { await furnace.putInput(mc.getItemId(itemName), null, num); // wait for the items to smelt let total = 0; - let collected_last = true; let smelted_item = null; await new Promise(resolve => setTimeout(resolve, 200)); + let last_collected = Date.now(); while (total < num) { - await new Promise(resolve => setTimeout(resolve, 10000)); - console.log('checking...'); - let collected = false; + await new Promise(resolve => setTimeout(resolve, 1000)); if (furnace.outputItem()) { smelted_item = await furnace.takeOutput(); if (smelted_item) { total += smelted_item.count; - collected = true; + last_collected = Date.now(); } } - if (!collected && !collected_last) { - break; // if nothing was collected this time or last time + if (Date.now() - last_collected > 11000) { + break; // if nothing has been collected in 11 seconds, stop } - collected_last = collected; if (bot.interrupt_code) { break; } } + // take all remaining in input/fuel slots + if (furnace.inputItem()) { + await furnace.takeInput(); + } + if (furnace.fuelItem()) { + await furnace.takeFuel(); + } + await bot.closeWindow(furnace); if (placedFurnace) { @@ -1040,7 +1045,7 @@ export async function goToGoal(bot, goal) { log(bot, `Found destructive path.`); } else { - log(bot, `Could not find a path to goal, attempting to navigate anyway using destructive movements.`); + log(bot, `Path not found, but attempting to navigate anyway using destructive movements.`); } const doorCheckInterval = startDoorInterval(bot); @@ -1288,11 +1293,29 @@ export async function followPlayer(bot, username, distance=4) { while (!bot.interrupt_code) { await new Promise(resolve => setTimeout(resolve, 500)); // 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) { + const distance_from_player = bot.entity.position.distanceTo(player.position); + + const teleport_distance = 100; + const ignore_modes_distance = 30; + const nearby_distance = distance + 2; + + if (distance_from_player > teleport_distance && bot.modes.isOn('cheat')) { + // teleport with cheat mode await goToPlayer(bot, username); } - const is_nearby = bot.entity.position.distanceTo(player.position) <= distance + 2; - if (is_nearby) { + else if (distance_from_player > ignore_modes_distance) { + // these modes slow down the bot, and we want to catch up + bot.modes.pause('item_collecting'); + bot.modes.pause('hunting'); + bot.modes.pause('torch_placing'); + } + else if (distance_from_player <= ignore_modes_distance) { + bot.modes.unpause('item_collecting'); + bot.modes.unpause('hunting'); + bot.modes.unpause('torch_placing'); + } + + if (distance_from_player <= nearby_distance) { clearInterval(doorCheckInterval); doorCheckInterval = null; bot.modes.pause('unstuck'); From 246aa450bc7784e60bb9f0398955f15723aa51cf Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Sat, 23 Aug 2025 12:02:33 -0500 Subject: [PATCH 20/35] fix novita --- src/models/novita.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/novita.js b/src/models/novita.js index 380fa4c..46140f0 100644 --- a/src/models/novita.js +++ b/src/models/novita.js @@ -6,7 +6,7 @@ import { strictFormat } from '../utils/text.js'; export class Novita { static prefix = 'novita'; constructor(model_name, url, params) { - this.model_name = model_name.replace('novita/', ''); + this.model_name = model_name; this.url = url || 'https://api.novita.ai/v3/openai'; this.params = params; From 9b23a2177e4d47cd3cb9d6f8ca9903b5195367b3 Mon Sep 17 00:00:00 2001 From: Isadora White <54700097+icwhite@users.noreply.github.com> Date: Sat, 23 Aug 2025 13:40:00 -0400 Subject: [PATCH 21/35] test --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 07ce15a..4d2fe08 100644 --- a/README.md +++ b/README.md @@ -174,3 +174,5 @@ Some of the node modules that we depend on have bugs in them. To add a patch, ch url = {https://arxiv.org/abs/2504.17950}, } ``` + + From 4bf0deaa1b2526040e9900e12d16a2fae61e3121 Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Sat, 23 Aug 2025 14:29:24 -0500 Subject: [PATCH 22/35] added vision/prefix to cerebras --- package.json | 2 +- src/models/cerebras.js | 24 +++++++++++++++++++++--- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index dabb8fe..106a75c 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "type": "module", "dependencies": { "@anthropic-ai/sdk": "^0.17.1", - "@cerebras/cerebras_cloud_sdk": "^1.0.0", + "@cerebras/cerebras_cloud_sdk": "^1.46.0", "@google/generative-ai": "^0.2.1", "@huggingface/inference": "^2.8.1", "@mistralai/mistralai": "^1.1.0", diff --git a/src/models/cerebras.js b/src/models/cerebras.js index 21f1eee..be902a6 100644 --- a/src/models/cerebras.js +++ b/src/models/cerebras.js @@ -3,9 +3,9 @@ import { strictFormat } from '../utils/text.js'; import { getKey } from '../utils/keys.js'; export class Cerebras { + static prefix = 'cerebras'; constructor(model_name, url, params) { - // Strip the prefix - this.model_name = model_name.replace('cerebras/', ''); + this.model_name = model_name; this.url = url; this.params = params; @@ -19,7 +19,7 @@ export class Cerebras { messages.unshift({ role: 'system', content: systemMessage }); const pack = { - model: this.model_name || 'llama-4-scout-17b-16e-instruct', + model: this.model_name || 'gpt-oss-120b', messages, stream: false, ...(this.params || {}), @@ -37,6 +37,24 @@ export class Cerebras { return res; } + async sendVisionRequest(messages, systemMessage, imageBuffer) { + const imageMessages = [...messages]; + imageMessages.push({ + role: "user", + content: [ + { type: "text", text: systemMessage }, + { + type: "image_url", + image_url: { + url: `data:image/jpeg;base64,${imageBuffer.toString('base64')}` + } + } + ] + }); + + return this.sendRequest(imageMessages, systemMessage); + } + async embed(text) { throw new Error('Embeddings are not supported by Cerebras.'); } From 6c8382874da4a628dc5d6271d3e3f522eef8c851 Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Sat, 23 Aug 2025 14:33:26 -0500 Subject: [PATCH 23/35] fix grok, updated many default models --- profiles/claude.json | 2 +- profiles/claude_thinker.json | 2 +- profiles/gemini.json | 2 +- profiles/grok.json | 2 +- src/models/gemini.js | 2 +- src/models/grok.js | 9 ++++----- src/models/groq.js | 2 +- src/models/novita.js | 2 +- 8 files changed, 11 insertions(+), 12 deletions(-) diff --git a/profiles/claude.json b/profiles/claude.json index d4ce4cc..27f610d 100644 --- a/profiles/claude.json +++ b/profiles/claude.json @@ -1,7 +1,7 @@ { "name": "claude", - "model": "claude-3-5-sonnet-latest", + "model": "claude-4-sonnet-latest", "embedding": "openai" } \ No newline at end of file diff --git a/profiles/claude_thinker.json b/profiles/claude_thinker.json index fdddb59..64e3fb4 100644 --- a/profiles/claude_thinker.json +++ b/profiles/claude_thinker.json @@ -2,7 +2,7 @@ "name": "claude_thinker", "model": { - "model": "claude-3-7-sonnet-latest", + "model": "claude-4-sonnet-latest", "params": { "thinking": { "type": "enabled", diff --git a/profiles/gemini.json b/profiles/gemini.json index 8a91387..0e388c7 100644 --- a/profiles/gemini.json +++ b/profiles/gemini.json @@ -1,7 +1,7 @@ { "name": "gemini", - "model": "gemini-2.0-flash", + "model": "gemini-2.5-flash", "cooldown": 5000 } diff --git a/profiles/grok.json b/profiles/grok.json index eeb3a38..eda1aaa 100644 --- a/profiles/grok.json +++ b/profiles/grok.json @@ -1,7 +1,7 @@ { "name": "Grok", - "model": "grok-beta", + "model": "grok-3-mini-latest", "embedding": "openai" } \ No newline at end of file diff --git a/src/models/gemini.js b/src/models/gemini.js index 75a20e0..ba24072 100644 --- a/src/models/gemini.js +++ b/src/models/gemini.js @@ -37,7 +37,7 @@ export class Gemini { async sendRequest(turns, systemMessage) { let model; const modelConfig = { - model: this.model_name || "gemini-1.5-flash", + model: this.model_name || "gemini-2.5-flash", // systemInstruction does not work bc google is trash }; if (this.url) { diff --git a/src/models/grok.js b/src/models/grok.js index 0753f10..40c63ce 100644 --- a/src/models/grok.js +++ b/src/models/grok.js @@ -3,7 +3,7 @@ import { getKey } from '../utils/keys.js'; // xAI doesn't supply a SDK for their models, but fully supports OpenAI and Anthropic SDKs export class Grok { - static prefix = 'grok'; + static prefix = 'xai'; constructor(model_name, url, params) { this.model_name = model_name; this.url = url; @@ -20,13 +20,12 @@ export class Grok { this.openai = new OpenAIApi(config); } - async sendRequest(turns, systemMessage, stop_seq='***') { + async sendRequest(turns, systemMessage) { let messages = [{'role': 'system', 'content': systemMessage}].concat(turns); const pack = { - model: this.model_name || "grok-beta", + model: this.model_name || "grok-3-mini-latest", messages, - stop: [stop_seq], ...(this.params || {}) }; @@ -43,7 +42,7 @@ export class Grok { catch (err) { if ((err.message == 'Context length exceeded' || err.code == 'context_length_exceeded') && turns.length > 1) { console.log('Context length exceeded, trying again with shorter context.'); - return await this.sendRequest(turns.slice(1), systemMessage, stop_seq); + return await this.sendRequest(turns.slice(1), systemMessage); } else if (err.message.includes('The model expects a single `text` element per message.')) { console.log(err); res = 'Vision is only supported by certain models.'; diff --git a/src/models/groq.js b/src/models/groq.js index 9da88c7..85a913e 100644 --- a/src/models/groq.js +++ b/src/models/groq.js @@ -50,7 +50,7 @@ export class GroqCloudAPI { let completion = await this.groq.chat.completions.create({ "messages": messages, - "model": this.model_name || "llama-3.3-70b-versatile", + "model": this.model_name || "qwen/qwen3-32b", "stream": false, "stop": stop_seq, ...(this.params || {}) diff --git a/src/models/novita.js b/src/models/novita.js index 46140f0..18e1fc4 100644 --- a/src/models/novita.js +++ b/src/models/novita.js @@ -26,7 +26,7 @@ export class Novita { messages = strictFormat(messages); const pack = { - model: this.model_name || "meta-llama/llama-3.1-70b-instruct", + model: this.model_name || "meta-llama/llama-4-scout-17b-16e-instruct", messages, stop: [stop_seq], ...(this.params || {}) From 131d04b8874b87662e5024627c7313b00786f030 Mon Sep 17 00:00:00 2001 From: Max Robinson Date: Sat, 23 Aug 2025 14:36:15 -0500 Subject: [PATCH 24/35] removed duplicate requirements --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index 66ae616..fa2b798 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,6 @@ Do not connect this bot to public servers with coding enabled. This project allo ## Requirements -- [Minecraft Java Edition](https://www.minecraft.net/en-us/store/minecraft-java-bedrock-edition-pc) (up to v1.21.1, recommend v1.20.4) -- [Node.js Installed](https://nodejs.org/) (at least v14) -- One of these: [OpenAI API Key](https://openai.com/blog/openai-api) | [Gemini API Key](https://aistudio.google.com/app/apikey) | [Anthropic API Key](https://docs.anthropic.com/claude/docs/getting-access-to-claude) | [Replicate API Key](https://replicate.com/) | [Hugging Face API Key](https://huggingface.co/) | [Groq API Key](https://console.groq.com/keys) | [Ollama Installed](https://ollama.com/download). | [Mistral API Key](https://docs.mistral.ai/getting-started/models/models_overview/) | [Qwen API Key [Intl.]](https://www.alibabacloud.com/help/en/model-studio/developer-reference/get-api-key)/[[cn]](https://help.aliyun.com/zh/model-studio/getting-started/first-api-call-to-qwen?) | [Novita AI API Key](https://novita.ai/settings?utm_source=github_mindcraft&utm_medium=github_readme&utm_campaign=link#key-management) | [Cerebras API Key](https://cloud.cerebras.ai) - [Minecraft Java Edition](https://www.minecraft.net/en-us/store/minecraft-java-bedrock-edition-pc) (up to v1.21.1, recommend v1.21.1) - [Node.js Installed](https://nodejs.org/) (at least v18) - One of these: [OpenAI API Key](https://openai.com/blog/openai-api) | [Gemini API Key](https://aistudio.google.com/app/apikey) | [Anthropic API Key](https://docs.anthropic.com/claude/docs/getting-access-to-claude) | [Replicate API Key](https://replicate.com/) | [Hugging Face API Key](https://huggingface.co/) | [Groq API Key](https://console.groq.com/keys) | [Ollama Installed](https://ollama.com/download). | [Mistral API Key](https://docs.mistral.ai/getting-started/models/models_overview/) | [Qwen API Key [Intl.]](https://www.alibabacloud.com/help/en/model-studio/developer-reference/get-api-key)/[[cn]](https://help.aliyun.com/zh/model-studio/getting-started/first-api-call-to-qwen?) | [Novita AI API Key](https://novita.ai/settings?utm_source=github_mindcraft&utm_medium=github_readme&utm_campaign=link#key-management) | [Cerebras API Key](https://cloud.cerebras.ai) From f41f03cdb324dfd67930e7f67325ee70cce5191d Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Sat, 23 Aug 2025 15:40:16 -0500 Subject: [PATCH 25/35] clean up mercury --- README.md | 2 +- profiles/mercury.json | 10 ++-------- src/models/mercury.js | 3 +++ 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index b1ff8cb..191b21a 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ You can configure the agent's name, model, and prompts in their profile like `an | `hyperbolic` | `HYPERBOLIC_API_KEY` | `hyperbolic/deepseek-ai/DeepSeek-V3` | [docs](https://docs.hyperbolic.xyz/docs/getting-started) | | `vllm` | n/a | `vllm/llama3` | n/a | | `cerebras` | `CEREBRAS_API_KEY` | `cerebras/llama-3.3-70b` | [docs](https://inference-docs.cerebras.ai/introduction) | -| `mercury(EA)` | `MERCURY_API_KEY` | `mercury-coder-small` | [docs](https://www.inceptionlabs.ai/) | +| `mercury` | `MERCURY_API_KEY` | `mercury-coder-small` | [docs](https://www.inceptionlabs.ai/) | If you use Ollama, to install the models used by default (generation and embedding), execute the following terminal command: `ollama pull llama3.1 && ollama pull nomic-embed-text` diff --git a/profiles/mercury.json b/profiles/mercury.json index cac6d49..482b601 100644 --- a/profiles/mercury.json +++ b/profiles/mercury.json @@ -3,13 +3,7 @@ "cooldown": 5000, - "model": { - "api": "mercury", - "url": "https://api.inceptionlabs.ai/v1", - "model": "mercury-coder-small" - }, + "model": "mercury/mercury-coder-small", - "embedding": "openai", - - "description":"Official Website Introduction:The world’s first diffusion large language models" + "embedding": "openai" } \ No newline at end of file diff --git a/src/models/mercury.js b/src/models/mercury.js index d4c4c3e..74cd64e 100644 --- a/src/models/mercury.js +++ b/src/models/mercury.js @@ -3,12 +3,15 @@ import { getKey, hasKey } from '../utils/keys.js'; import { strictFormat } from '../utils/text.js'; export class Mercury { + static prefix = 'mercury'; constructor(model_name, url, params) { this.model_name = model_name; this.params = params; let config = {}; if (url) config.baseURL = url; + else + config.baseURL = "https://api.inceptionlabs.ai/v1"; config.apiKey = getKey('MERCURY_API_KEY'); From 1588a8770bd7290d2a9ea9cc6b997239f5ea85bf Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Sat, 23 Aug 2025 16:00:59 -0500 Subject: [PATCH 26/35] clean up azure --- profiles/azure.json | 18 +++++++++++------- src/models/azure.js | 23 ++++++++++++++++------- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/profiles/azure.json b/profiles/azure.json index fbd382d..29b1122 100644 --- a/profiles/azure.json +++ b/profiles/azure.json @@ -2,14 +2,18 @@ "name": "azure", "model": { "api": "azure", - "url": "", - "model": "gpt-4o", - "api_version": "2024-08-01-preview" + "url": "https://.openai.azure.com", + "model": "", + "params": { + "apiVersion": "2024-08-01-preview" + } }, "embedding": { "api": "azure", - "url": "", - "model": "text-embedding-ada-002", - "api_version": "2024-08-01-preview" + "url": "https://.openai.azure.com", + "model": "", + "params": { + "apiVersion": "2024-08-01-preview" + } } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/models/azure.js b/src/models/azure.js index d8e2f4a..b6be3e0 100644 --- a/src/models/azure.js +++ b/src/models/azure.js @@ -1,22 +1,31 @@ import { AzureOpenAI } from "openai"; -import { getKey } from '../utils/keys.js'; +import { getKey, hasKey } from '../utils/keys.js'; import { GPT } from './gpt.js' export class AzureGPT extends GPT { - constructor(model_name, url, api_version, params) { + static prefix = 'azure'; + constructor(model_name, url, params) { super(model_name, url) this.model_name = model_name; - this.params = params; + this.params = params || {}; - let config = {} + const config = {}; if (url) config.endpoint = url; - config.apiKey = getKey('OPENAI_API_KEY'); - config.deployment = model_name; // This must be what you named the deployment in Azure, not the model version itself - config.apiVersion = api_version; // This is required for Azure + config.apiKey = hasKey('AZURE_OPENAI_API_KEY') ? getKey('AZURE_OPENAI_API_KEY') : getKey('OPENAI_API_KEY'); + + config.deployment = model_name; + + if (this.params.apiVersion) { + config.apiVersion = this.params.apiVersion; + delete this.params.apiVersion; // remove from params for later use in requests + } + else { + throw new Error('apiVersion is required in params for azure!'); + } this.openai = new AzureOpenAI(config) } From 5f3a7f98c4aa1a994bbf29180a9aca244b3fac21 Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Sat, 23 Aug 2025 16:13:04 -0500 Subject: [PATCH 27/35] fix default claude models --- profiles/claude.json | 2 +- profiles/claude_thinker.json | 2 +- src/models/claude.js | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/profiles/claude.json b/profiles/claude.json index 27f610d..b1a324d 100644 --- a/profiles/claude.json +++ b/profiles/claude.json @@ -1,7 +1,7 @@ { "name": "claude", - "model": "claude-4-sonnet-latest", + "model": "claude-sonnet-4-20250514", "embedding": "openai" } \ No newline at end of file diff --git a/profiles/claude_thinker.json b/profiles/claude_thinker.json index 64e3fb4..49df53f 100644 --- a/profiles/claude_thinker.json +++ b/profiles/claude_thinker.json @@ -2,7 +2,7 @@ "name": "claude_thinker", "model": { - "model": "claude-4-sonnet-latest", + "model": "claude-sonnet-4-20250514", "params": { "thinking": { "type": "enabled", diff --git a/src/models/claude.js b/src/models/claude.js index c42d2e6..271c6b2 100644 --- a/src/models/claude.js +++ b/src/models/claude.js @@ -21,7 +21,7 @@ export class Claude { const messages = strictFormat(turns); let res = null; try { - console.log('Awaiting anthropic api response...') + console.log(`Awaiting anthropic response from ${this.model_name}...`) if (!this.params.max_tokens) { if (this.params.thinking?.budget_tokens) { this.params.max_tokens = this.params.thinking.budget_tokens + 1000; @@ -31,7 +31,7 @@ export class Claude { } } const resp = await this.anthropic.messages.create({ - model: this.model_name || "claude-3-sonnet-20240229", + model: this.model_name || "claude-sonnet-4-20250514", system: systemMessage, messages: messages, ...(this.params || {}) From 0e990af01fa9cc39dcf96c9e23f26c969620754b Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Sat, 23 Aug 2025 16:14:30 -0500 Subject: [PATCH 28/35] add azure instructions --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 191b21a..11a2894 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,8 @@ You can configure the agent's name, model, and prompts in their profile like `an If you use Ollama, to install the models used by default (generation and embedding), execute the following terminal command: `ollama pull llama3.1 && ollama pull nomic-embed-text` +To use Azure, you can reuse the `OPENAI_API_KEY` environment variable. You can get the key from the Azure portal. See [azure.json](profiles/azure.json) for an example. + ### 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 too and play with it. To connect, change these lines in `settings.js`: ```javascript From 64cd142d5169b006b79172bf17c2de24d28f640e Mon Sep 17 00:00:00 2001 From: Sweaterdog Date: Sun, 24 Aug 2025 11:55:40 -0700 Subject: [PATCH 29/35] Add Andy-4 (thinking) Minecraft bot configuration --- profiles/andy-4-reasoning.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 profiles/andy-4-reasoning.json diff --git a/profiles/andy-4-reasoning.json b/profiles/andy-4-reasoning.json new file mode 100644 index 0000000..faa3b0b --- /dev/null +++ b/profiles/andy-4-reasoning.json @@ -0,0 +1,14 @@ +{ + "name": "Andy-4", + + "model": "ollama/sweaterdog/andy-4:micro-q8_0", + + "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.\n$SELF_PROMPT 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. Think in high amounts before responding. 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)'. Respond only as $NAME, never output '(FROM OTHER BOT)' or pretend to be someone else. If you have nothing to say or do, respond with an just a tab '\t'. This is extremely important to me, take a deep breath and have fun :)\nSummarized memory:'$MEMORY'\n$STATS\n$INVENTORY\n$COMMAND_DOCS\n$EXAMPLES\nReason before responding. Conversation Begin:", + + "coding": "You are an intelligent mineflayer bot $NAME that plays minecraft by writing javascript codeblocks. Given the conversation, 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 receive it's output. If an error occurs, write another codeblock and try to fix the problem. Be maximally efficient, creative, and correct. Be mindful of previous actions. Do not use commands !likeThis, only use codeblocks. The code is asynchronous and MUST USE AWAIT for all async function calls, and must contain at least one await. You have `Vec3`, `skills`, and `world` imported, and the mineflayer `bot` is given. Do not import other libraries. Think deeply before responding. Do not use setTimeout or setInterval. Do not speak conversationally, only use codeblocks. Do any planning in comments. This is extremely important to me, think step-by-step, take a deep breath and good luck! \n$SELF_PROMPT\nSummarized memory:'$MEMORY'\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 and your old memory in your next response. Prioritize preserving important facts, things you've learned, useful tips, and long term reminders. Do Not record stats, inventory, or docs! Only save transient information from your chat history. You're limited to 500 characters, so be extremely brief, think about what you will summarize before responding, minimize words, and provide your summarization in Chinese. Compress useful information. \nOld Memory: '$MEMORY'\nRecent conversation: \n$TO_SUMMARIZE\nSummarize your old memory and recent conversation into a new memory, and respond only with the unwrapped memory text: ", + + "bot_responder": "You are a minecraft bot named $NAME that is currently in conversation with another AI bot. Both of you can take actions with the !command syntax, and actions take time to complete. You are currently busy with the following action: '$ACTION' but have received a new message. Decide whether to 'respond' immediately or 'ignore' it and wait for your current action to finish. Be conservative and only respond when necessary, like when you need to change/stop your action, or convey necessary information. Example 1: You:Building a house! !newAction('Build a house.').\nOther Bot: 'Come here!'\nYour decision: ignore\nExample 2: You:Collecting dirt !collectBlocks('dirt',10).\nOther Bot: 'No, collect some wood instead.'\nYour decision: respond\nExample 3: You:Coming to you now. !goToPlayer('billy',3).\nOther Bot: 'What biome are you in?'\nYour decision: respond\nActual Conversation: $TO_SUMMARIZE\nDecide by outputting ONLY 'respond' or 'ignore', nothing else. Your decision:" + +} From b29b90c0d669a1885551fb2bc6da3615a4d6b14b Mon Sep 17 00:00:00 2001 From: Sweaterdog Date: Sun, 24 Aug 2025 11:57:34 -0700 Subject: [PATCH 30/35] Rename Andy-4 to Andy-4-thinking in JSON --- profiles/andy-4-reasoning.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/profiles/andy-4-reasoning.json b/profiles/andy-4-reasoning.json index faa3b0b..9a83fbd 100644 --- a/profiles/andy-4-reasoning.json +++ b/profiles/andy-4-reasoning.json @@ -1,5 +1,5 @@ { - "name": "Andy-4", + "name": "Andy-4-thinking", "model": "ollama/sweaterdog/andy-4:micro-q8_0", From c973d79c838133b0e026f3b80396ab61bc7deb6f Mon Sep 17 00:00:00 2001 From: Sweaterdog Date: Sun, 24 Aug 2025 11:58:32 -0700 Subject: [PATCH 31/35] Add new profile for Andy-4 --- profiles/andy-4.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 profiles/andy-4.json diff --git a/profiles/andy-4.json b/profiles/andy-4.json new file mode 100644 index 0000000..64ed347 --- /dev/null +++ b/profiles/andy-4.json @@ -0,0 +1,7 @@ +{ + "name": "andy-4", + + "model": "ollama/sweaterdog/andy-4:micro-q8_0", + + "embedding": "ollama" +} From 36c247713ea272427f6fb5f49dfa7c3606d79a0b Mon Sep 17 00:00:00 2001 From: Sweaterdog Date: Sun, 24 Aug 2025 11:58:45 -0700 Subject: [PATCH 32/35] Change name field to lowercase in JSON --- profiles/andy-4-reasoning.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/profiles/andy-4-reasoning.json b/profiles/andy-4-reasoning.json index 9a83fbd..b4fadd3 100644 --- a/profiles/andy-4-reasoning.json +++ b/profiles/andy-4-reasoning.json @@ -1,5 +1,5 @@ { - "name": "Andy-4-thinking", + "name": "andy-4-thinking", "model": "ollama/sweaterdog/andy-4:micro-q8_0", From e9366a162643bffd875e11190afdbcf118f47119 Mon Sep 17 00:00:00 2001 From: Sweaterdog Date: Sun, 24 Aug 2025 12:04:13 -0700 Subject: [PATCH 33/35] fix spacing mercury and add andy-4 profile options Also added `Supports up to 75 messages!` next to the Andy-4 profile tag so people know it can be more than just 15 messages. --- settings.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/settings.js b/settings.js index d9f0037..2aacf91 100644 --- a/settings.js +++ b/settings.js @@ -18,7 +18,8 @@ const settings = { // "./profiles/grok.json", // "./profiles/mistral.json", // "./profiles/deepseek.json", - //"./profiles/mercury.json", + // "./profiles/mercury.json", + // "./profiles/andy-4.json", // Supports up to 75 messages! // using more than 1 profile requires you to /msg each bot indivually // individual profiles override values from the base profile From e3aed60f6d7d5075bd3f8181206fcd3b002682f7 Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Mon, 25 Aug 2025 15:29:10 -0500 Subject: [PATCH 34/35] add last bot output to ui --- src/mindcraft/public/index.html | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/mindcraft/public/index.html b/src/mindcraft/public/index.html index fd9f10b..d9690a2 100644 --- a/src/mindcraft/public/index.html +++ b/src/mindcraft/public/index.html @@ -25,8 +25,8 @@ background: #363636; border-radius: 4px; display: flex; - justify-content: space-between; - align-items: center; + flex-direction: column; + align-items: flex-start; } .restart-btn, .start-btn, .stop-btn { color: white; @@ -102,6 +102,13 @@ border: none; margin-left: 10px; } + .last-message { + font-style: italic; + color: #aaa; + margin-top: 5px; + white-space: pre-wrap; + word-break: break-word; + } .start-btn:disabled { opacity: 0.4; cursor: not-allowed; @@ -135,6 +142,7 @@ let settingsSpec = {}; let profileData = null; const agentSettings = {}; + const agentLastMessage = {}; fetch('/settings_spec.json') .then(r => r.json()) @@ -229,6 +237,14 @@ }); }); + socket.on('bot-output', (agentName, message) => { + agentLastMessage[agentName] = message; + const messageDiv = document.getElementById(`lastMessage-${agentName}`); + if (messageDiv) { + messageDiv.textContent = message; + } + }); + function fetchAgentSettings(name) { return new Promise((resolve) => { if (agentSettings[name]) { resolve(agentSettings[name]); return; } @@ -250,9 +266,10 @@ const cfg = agentSettings[agent.name] || {}; const showViewer = cfg.render_bot_view === true; const viewerHTML = showViewer ? `
` : ''; + const lastMessage = agentLastMessage[agent.name] || ''; return `
-
+
${agent.name}
${agent.in_game ? ` @@ -265,6 +282,7 @@ `}
+
${lastMessage}
${viewerHTML}
`; }).join('') + From b33953ab01b23f883f63963e7aa0ed781b4b7f25 Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Mon, 25 Aug 2025 15:45:54 -0500 Subject: [PATCH 35/35] fix "none" command syntax --- src/agent/agent.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/agent/agent.js b/src/agent/agent.js index 69aefe2..4c4072b 100644 --- a/src/agent/agent.js +++ b/src/agent/agent.js @@ -318,9 +318,8 @@ export class Agent { else { // no command at all let pre_message = res.substring(0, res.indexOf(command_name)).trim(); - res = pre_message; - if (res.trim().length > 0) - this.routeResponse(source, res); + if (pre_message.trim().length > 0) + this.routeResponse(source, pre_message); } let execute_res = await executeCommand(this, res);