mirror of
https://github.com/kolbytn/mindcraft.git
synced 2025-08-28 09:53:06 +02:00
Merge branch 'develop' into pollinations-support
This commit is contained in:
commit
e67bd9ab92
43 changed files with 691 additions and 238 deletions
10
README.md
10
README.md
|
@ -10,9 +10,9 @@ 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.21.1)
|
||||
- [Minecraft Java Edition](https://www.minecraft.net/en-us/store/minecraft-java-bedrock-edition-pc) (up to v1.21.4, 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) |
|
||||
- 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) | [Mercury API](https://platform.inceptionlabs.ai/docs)
|
||||
|
||||
## Install and Run
|
||||
|
||||
|
@ -64,10 +64,14 @@ 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) |
|
||||
| `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`
|
||||
|
||||
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
|
||||
|
@ -180,3 +184,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},
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
|
|
@ -13,5 +13,7 @@
|
|||
"GHLF_API_KEY": "",
|
||||
"HYPERBOLIC_API_KEY": "",
|
||||
"NOVITA_API_KEY": "",
|
||||
"OPENROUTER_API_KEY": ""
|
||||
"OPENROUTER_API_KEY": "",
|
||||
"CEREBRAS_API_KEY": "",
|
||||
"MERCURY_API_KEY":""
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.17.1",
|
||||
"@google/genai": "^1.15.0",
|
||||
"@cerebras/cerebras_cloud_sdk": "^1.46.0",
|
||||
"@huggingface/inference": "^2.8.1",
|
||||
"@mistralai/mistralai": "^1.1.0",
|
||||
"canvas": "^3.1.0",
|
||||
|
|
14
profiles/andy-4-reasoning.json
Normal file
14
profiles/andy-4-reasoning.json
Normal file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"name": "andy-4-thinking",
|
||||
|
||||
"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:"
|
||||
|
||||
}
|
7
profiles/andy-4.json
Normal file
7
profiles/andy-4.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"name": "andy-4",
|
||||
|
||||
"model": "ollama/sweaterdog/andy-4:micro-q8_0",
|
||||
|
||||
"embedding": "ollama"
|
||||
}
|
19
profiles/azure.json
Normal file
19
profiles/azure.json
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "azure",
|
||||
"model": {
|
||||
"api": "azure",
|
||||
"url": "https://<your-resource>.openai.azure.com",
|
||||
"model": "<chat-deployment-name>",
|
||||
"params": {
|
||||
"apiVersion": "2024-08-01-preview"
|
||||
}
|
||||
},
|
||||
"embedding": {
|
||||
"api": "azure",
|
||||
"url": "https://<your-resource>.openai.azure.com",
|
||||
"model": "<embedding-deployment-name>",
|
||||
"params": {
|
||||
"apiVersion": "2024-08-01-preview"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "claude",
|
||||
|
||||
"model": "claude-3-5-sonnet-latest",
|
||||
"model": "claude-sonnet-4-20250514",
|
||||
|
||||
"embedding": "openai"
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
"name": "claude_thinker",
|
||||
|
||||
"model": {
|
||||
"model": "claude-3-7-sonnet-latest",
|
||||
"model": "claude-sonnet-4-20250514",
|
||||
"params": {
|
||||
"thinking": {
|
||||
"type": "enabled",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "gemini",
|
||||
|
||||
"model": "gemini-2.0-flash",
|
||||
"model": "gemini-2.5-flash",
|
||||
|
||||
"cooldown": 5000
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "Grok",
|
||||
|
||||
"model": "grok-beta",
|
||||
"model": "grok-3-mini-latest",
|
||||
|
||||
"embedding": "openai"
|
||||
}
|
9
profiles/mercury.json
Normal file
9
profiles/mercury.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"name": "Mercury",
|
||||
|
||||
"cooldown": 5000,
|
||||
|
||||
"model": "mercury/mercury-coder-small",
|
||||
|
||||
"embedding": "openai"
|
||||
}
|
11
settings.js
11
settings.js
|
@ -1,5 +1,5 @@
|
|||
const settings = {
|
||||
"minecraft_version": "1.21.1", // supports up to 1.21.1
|
||||
"minecraft_version": "auto", // or specific version like "1.21.1"
|
||||
"host": "127.0.0.1", // or "localhost", "your.ip.address.here"
|
||||
"port": 55916,
|
||||
"auth": "offline", // or "microsoft"
|
||||
|
@ -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": "assistant", // survival, assistant, creative, or god_mode
|
||||
"profiles": [
|
||||
"./andy.json",
|
||||
// "./profiles/gpt.json",
|
||||
|
@ -18,6 +18,8 @@ const settings = {
|
|||
// "./profiles/grok.json",
|
||||
// "./profiles/mistral.json",
|
||||
// "./profiles/deepseek.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
|
||||
|
@ -31,7 +33,8 @@ const settings = {
|
|||
// allows all bots to speak through text-to-speech. format: {provider}/{model}/{voice}. if set to "system" it will use system text-to-speech, which works on windows and mac, but on linux you need to `apt install espeak`.
|
||||
// specify speech model inside each profile - so that you can have each bot with different voices
|
||||
|
||||
"language": "en", // translate to/from this language. NOT text-to-speech language. Supports these language names: https://cloud.google.com/translate/docs/languages
|
||||
"chat_ingame": true, // bot responses are shown in minecraft chat
|
||||
"language": "en", // translate to/from this language. Supports these language names: https://cloud.google.com/translate/docs/languages
|
||||
"render_bot_view": false, // show bot's view in browser at localhost:3000, 3001...
|
||||
|
||||
"allow_insecure_coding": false, // allows newAction command and model can write/run code on your computer. enable at own risk
|
||||
|
@ -43,7 +46,7 @@ const settings = {
|
|||
"max_messages": 15, // max number of messages to keep in context
|
||||
"num_examples": 2, // number of examples to give to the model
|
||||
"max_commands": -1, // max number of commands that can be used in consecutive responses. -1 for no limit
|
||||
"verbose_commands": true, // show full command syntax
|
||||
"show_command_syntax": "full", // "full", "shortened", or "none"
|
||||
"narrate_behavior": true, // chat simple automatic actions ('Picking up item!')
|
||||
"chat_bot_messages": true, // publicly chat messages to other bots
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ import { SelfPrompter } from './self_prompter.js';
|
|||
import convoManager from './conversation.js';
|
||||
import { handleTranslation, handleEnglishTranslation } from '../utils/translator.js';
|
||||
import { addBrowserViewer } from './vision/browser_viewer.js';
|
||||
import { serverProxy } from './mindserver_proxy.js';
|
||||
import { serverProxy, sendOutputToServer } from './mindserver_proxy.js';
|
||||
import settings from './settings.js';
|
||||
import { Task } from './tasks/tasks.js';
|
||||
import { say } from './speak.js';
|
||||
|
@ -304,16 +304,23 @@ export class Agent {
|
|||
if (checkInterrupt()) break;
|
||||
this.self_prompter.handleUserPromptedCmd(self_prompt, isAction(command_name));
|
||||
|
||||
if (settings.verbose_commands) {
|
||||
if (settings.show_command_syntax === "full") {
|
||||
this.routeResponse(source, res);
|
||||
}
|
||||
else { // only output command name
|
||||
else if (settings.show_command_syntax === "shortened") {
|
||||
// show only "used !commandname"
|
||||
let pre_message = res.substring(0, res.indexOf(command_name)).trim();
|
||||
let chat_message = `*used ${command_name.substring(1)}*`;
|
||||
if (pre_message.length > 0)
|
||||
chat_message = `${pre_message} ${chat_message}`;
|
||||
this.routeResponse(source, chat_message);
|
||||
}
|
||||
else {
|
||||
// no command at all
|
||||
let pre_message = res.substring(0, res.indexOf(command_name)).trim();
|
||||
if (pre_message.trim().length > 0)
|
||||
this.routeResponse(source, pre_message);
|
||||
}
|
||||
|
||||
let execute_res = await executeCommand(this, res);
|
||||
|
||||
|
@ -379,7 +386,8 @@ export class Agent {
|
|||
if (settings.speak) {
|
||||
say(to_translate, this.prompter.profile.speak_model);
|
||||
}
|
||||
this.bot.chat(message);
|
||||
if (settings.chat_ingame) {this.bot.chat(message);}
|
||||
sendOutputToServer(this.name, message);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -2,7 +2,7 @@ import { io } from 'socket.io-client';
|
|||
import convoManager from './conversation.js';
|
||||
import { setSettings } from './settings.js';
|
||||
|
||||
// agents connection to mindserver
|
||||
// agent's individual connection to the mindserver
|
||||
// always connect to localhost
|
||||
|
||||
class MindServerProxy {
|
||||
|
@ -110,6 +110,12 @@ class MindServerProxy {
|
|||
// Create and export a singleton instance
|
||||
export const serverProxy = new MindServerProxy();
|
||||
|
||||
// for chatting with other bots
|
||||
export function sendBotChatToServer(agentName, json) {
|
||||
serverProxy.getSocket().emit('chat-message', agentName, json);
|
||||
}
|
||||
|
||||
// for sending general output to server for display
|
||||
export function sendOutputToServer(agentName, message) {
|
||||
serverProxy.getSocket().emit('bot-output', agentName, message);
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
151
src/mindcraft/mcserver.js
Normal file
151
src/mindcraft/mcserver.js
Normal file
|
@ -0,0 +1,151 @@
|
|||
import net from 'net';
|
||||
import mc from 'minecraft-protocol';
|
||||
|
||||
/**
|
||||
* Scans the IP address for Minecraft LAN servers and collects their info.
|
||||
* @param {string} ip - The IP address to scan.
|
||||
* @param {number} port - The port to check.
|
||||
* @param {number} timeout - The connection timeout in ms.
|
||||
* @param {boolean} verbose - Whether to print output on connection errors.
|
||||
* @returns {Promise<Array>} - A Promise that resolves to an array of server info objects.
|
||||
*/
|
||||
export async function serverInfo(ip, port, timeout = 1000, verbose = false) {
|
||||
return new Promise((resolve) => {
|
||||
|
||||
let timeoutId = setTimeout(() => {
|
||||
if (verbose)
|
||||
console.error(`Timeout pinging server ${ip}:${port}`);
|
||||
resolve(null); // Resolve as null if no response within timeout
|
||||
}, timeout);
|
||||
|
||||
mc.ping({
|
||||
host: ip,
|
||||
port
|
||||
}, (err, response) => {
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (err) {
|
||||
if (verbose)
|
||||
console.error(`Error pinging server ${ip}:${port}`, err);
|
||||
return resolve(null);
|
||||
}
|
||||
|
||||
// extract version number from modded servers like "Paper 1.21.4"
|
||||
const version = response?.version?.name || '';
|
||||
const match = String(version).match(/\d+\.\d+(?:\.\d+)?/);
|
||||
const numericVersion = match ? match[0] : null;
|
||||
if (numericVersion !== version) {
|
||||
console.log(`Modded server found (${version}), attempting to use ${numericVersion}...`);
|
||||
}
|
||||
|
||||
const serverInfo = {
|
||||
host: ip,
|
||||
port,
|
||||
name: response.description.text || 'No description provided.',
|
||||
ping: response.latency,
|
||||
version: numericVersion
|
||||
};
|
||||
|
||||
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<Array>} - 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);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// This supresses a lot of annoying console output from the mc library
|
||||
// TODO: find a better way to do this, it supresses other useful 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, 200, false);
|
||||
if (server) {
|
||||
servers.push(server);
|
||||
|
||||
if (earlyExit) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Restore console output
|
||||
console.log = originalConsoleLog;
|
||||
|
||||
return servers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the MC server info from the host and port.
|
||||
* @param {string} host - The host to search for.
|
||||
* @param {number} port - The port to search for.
|
||||
* @param {string} version - The version to search for.
|
||||
* @returns {Promise<Object>} - A Promise that resolves to the server info object.
|
||||
*/
|
||||
export async function getServer(host, port, version) {
|
||||
let server = null;
|
||||
let serverString = "";
|
||||
let serverVersion = "";
|
||||
|
||||
// Search for server
|
||||
if (port == -1)
|
||||
{
|
||||
console.log(`No port provided. Searching for LAN server on host ${host}...`);
|
||||
|
||||
await findServers(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(host, port, 1000, true);
|
||||
|
||||
// Server not found
|
||||
if (server == null)
|
||||
throw new Error(`MC server not found. (Host: ${host}, Port: ${port}) Check the host and port in settings.js, and ensure the server is running and open to public or LAN.`);
|
||||
|
||||
serverString = `(Host: ${server.host}, Port: ${server.port}, Version: ${server.version})`;
|
||||
|
||||
if (version === "auto")
|
||||
serverVersion = server.version;
|
||||
else
|
||||
serverVersion = version;
|
||||
// Server version unsupported / mismatch
|
||||
if (mc.supportedVersions.indexOf(serverVersion) === -1)
|
||||
throw new Error(`MC server was found ${serverString}, but version is unsupported. Supported versions are: ${mc.supportedVersions.join(", ")}.`);
|
||||
else if (version !== "auto" && server.version !== version)
|
||||
throw new Error(`MC server was found ${serverString}, but version is incorrect. Expected ${version}, but found ${server.version}. Check the server version in settings.js.`);
|
||||
else
|
||||
console.log(`MC server found. ${serverString}`);
|
||||
|
||||
return server;
|
||||
}
|
|
@ -1,11 +1,11 @@
|
|||
import { createMindServer, registerAgent } from './mindserver.js';
|
||||
import { AgentProcess } from '../process/agent_process.js';
|
||||
import { getServer } from './mcserver.js';
|
||||
|
||||
let mindserver;
|
||||
let connected = false;
|
||||
let agent_processes = {};
|
||||
let agent_count = 0;
|
||||
let host = 'localhost';
|
||||
let port = 8080;
|
||||
|
||||
export async function init(host_public=false, port=8080) {
|
||||
|
@ -28,6 +28,12 @@ export async function createAgent(settings) {
|
|||
registerAgent(settings);
|
||||
let load_memory = settings.load_memory || false;
|
||||
let init_message = settings.init_message || null;
|
||||
|
||||
const server = await getServer(settings.host, settings.port, settings.minecraft_version);
|
||||
settings.host = server.host;
|
||||
settings.port = server.port;
|
||||
settings.minecraft_version = server.version;
|
||||
|
||||
const agentProcess = new AgentProcess(agent_name, port);
|
||||
agentProcess.start(load_memory, init_message, agent_count);
|
||||
agent_count++;
|
||||
|
|
|
@ -170,6 +170,10 @@ export function createMindServer(host_public = false, port = 8080) {
|
|||
console.error('Error: ', error);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('bot-output', (agentName, message) => {
|
||||
io.emit('bot-output', agentName, message);
|
||||
});
|
||||
});
|
||||
|
||||
let host = host_public ? '0.0.0.0' : 'localhost';
|
||||
|
|
|
@ -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 ? `<div class="agent-view-container"><iframe class="agent-viewer" src="http://localhost:${3000 + idx}"></iframe></div>` : '';
|
||||
const lastMessage = agentLastMessage[agent.name] || '';
|
||||
return `
|
||||
<div class="agent">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;width:100%;">
|
||||
<span><span class="status-icon ${agent.in_game ? 'online' : 'offline'}">●</span>${agent.name}</span>
|
||||
<div style="display:flex;align-items:center;">
|
||||
${agent.in_game ? `
|
||||
|
@ -265,6 +282,7 @@
|
|||
`}
|
||||
</div>
|
||||
</div>
|
||||
<div id="lastMessage-${agent.name}" class="last-message">${lastMessage}</div>
|
||||
${viewerHTML}
|
||||
</div>`;
|
||||
}).join('') +
|
||||
|
|
|
@ -94,10 +94,10 @@
|
|||
"description": "Whether to log all prompts to file. Can be very verbose.",
|
||||
"default": false
|
||||
},
|
||||
"verbose_commands": {
|
||||
"type": "boolean",
|
||||
"description": "Whether to show full command syntax in bot responses. If false will use a shortened syntax.",
|
||||
"default": true
|
||||
"show_command_syntax": {
|
||||
"type": "string",
|
||||
"description": "Whether to show \"full\" command syntax, \"shortened\" command syntax, or \"none\"",
|
||||
"default": "full"
|
||||
},
|
||||
"chat_bot_messages": {
|
||||
"type": "boolean",
|
||||
|
|
89
src/models/_model_map.js
Normal file
89
src/models/_model_map.js
Normal file
|
@ -0,0 +1,89 @@
|
|||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath, pathToFileURL } from 'url';
|
||||
|
||||
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};
|
||||
}
|
||||
// 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');
|
||||
}
|
||||
}
|
||||
if (!profile.api) {
|
||||
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
|
||||
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;
|
||||
}
|
32
src/models/azure.js
Normal file
32
src/models/azure.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { AzureOpenAI } from "openai";
|
||||
import { getKey, hasKey } from '../utils/keys.js';
|
||||
import { GPT } from './gpt.js'
|
||||
|
||||
export class AzureGPT extends GPT {
|
||||
static prefix = 'azure';
|
||||
constructor(model_name, url, params) {
|
||||
super(model_name, url)
|
||||
|
||||
this.model_name = model_name;
|
||||
this.params = params || {};
|
||||
|
||||
const config = {};
|
||||
|
||||
if (url)
|
||||
config.endpoint = url;
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
61
src/models/cerebras.js
Normal file
61
src/models/cerebras.js
Normal file
|
@ -0,0 +1,61 @@
|
|||
import CerebrasSDK from '@cerebras/cerebras_cloud_sdk';
|
||||
import { strictFormat } from '../utils/text.js';
|
||||
import { getKey } from '../utils/keys.js';
|
||||
|
||||
export class Cerebras {
|
||||
static prefix = 'cerebras';
|
||||
constructor(model_name, url, params) {
|
||||
this.model_name = model_name;
|
||||
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 || 'gpt-oss-120b',
|
||||
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 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.');
|
||||
}
|
||||
}
|
|
@ -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 || {};
|
||||
|
@ -20,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;
|
||||
|
@ -30,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 || {})
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -6,8 +6,9 @@ import { lamejs } from 'lamejs/lame.all.js';
|
|||
|
||||
|
||||
export class Gemini {
|
||||
constructor(model, url, params) {
|
||||
this.model = model || "gemini-2.5-flash";
|
||||
static prefix = 'google';
|
||||
constructor(model_name, url, params) {
|
||||
this.model_name = model_name;
|
||||
this.params = params;
|
||||
this.safetySettings = [
|
||||
{
|
||||
|
@ -48,7 +49,7 @@ export class Gemini {
|
|||
}
|
||||
|
||||
const result = await this.genAI.models.generateContent({
|
||||
model: this.model,
|
||||
model: this.model_name || "gemini-2.5-flash",
|
||||
contents: contents,
|
||||
safetySettings: this.safetySettings,
|
||||
config: {
|
||||
|
@ -112,7 +113,7 @@ export class Gemini {
|
|||
|
||||
async embed(text) {
|
||||
const result = await this.genAI.models.embedContent({
|
||||
model: 'gemini-embedding-001',
|
||||
model: this.model_name || "gemini-embedding-001",
|
||||
contents: text,
|
||||
})
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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 = 'xai';
|
||||
constructor(model_name, url, params) {
|
||||
this.model_name = model_name;
|
||||
this.url = url;
|
||||
|
@ -19,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 || {})
|
||||
};
|
||||
|
||||
|
@ -42,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.';
|
||||
|
|
|
@ -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) {
|
||||
|
||||
|
@ -49,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 || {})
|
||||
|
@ -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);
|
||||
|
|
|
@ -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/', '');
|
||||
|
|
|
@ -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";
|
||||
|
|
95
src/models/mercury.js
Normal file
95
src/models/mercury.js
Normal file
|
@ -0,0 +1,95 @@
|
|||
import OpenAIApi from 'openai';
|
||||
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');
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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) {
|
||||
|
|
|
@ -4,8 +4,9 @@ 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.model_name = model_name;
|
||||
this.url = url || 'https://api.novita.ai/v3/openai';
|
||||
this.params = params;
|
||||
|
||||
|
@ -25,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 || {})
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { strictFormat } from '../utils/text.js';
|
||||
|
||||
export class Local {
|
||||
export class Ollama {
|
||||
static prefix = 'ollama';
|
||||
constructor(model_name, url, params) {
|
||||
this.model_name = model_name;
|
||||
this.params = params;
|
||||
|
@ -10,11 +11,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-q8_0';
|
||||
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 <think> tags are mismatched.
|
||||
const maxAttempts = 5;
|
||||
let attempt = 0;
|
||||
let finalRes = null;
|
||||
|
@ -24,14 +23,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 +42,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 <think> block.
|
||||
const hasOpenTag = res.includes("<think>");
|
||||
const hasCloseTag = res.includes("</think>");
|
||||
|
||||
// If there's a partial mismatch, retry to get a complete response.
|
||||
if ((hasOpenTag && !hasCloseTag)) {
|
||||
console.warn("Partial <think> block detected. Re-generating...");
|
||||
continue;
|
||||
if (attempt < maxAttempts) continue;
|
||||
}
|
||||
|
||||
// If </think> is present but <think> is not, prepend <think>
|
||||
if (hasCloseTag && !hasOpenTag) {
|
||||
res = '<think>' + res;
|
||||
}
|
||||
// Changed this so if the model reasons, using <think> and </think> but doesn't start the message with <think>, <think> ges prepended to the message so no error occur.
|
||||
|
||||
// If both tags appear, remove them (and everything inside).
|
||||
if (hasOpenTag && hasCloseTag) {
|
||||
res = res.replace(/<think>[\s\S]*?<\/think>/g, '');
|
||||
res = res.replace(/<think>[\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 <think> 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 +94,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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
let embedding_model_profile = null;
|
||||
if (this.profile.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);
|
||||
embedding_model_profile = selectAPI(this.profile.embedding);
|
||||
} catch (e) {
|
||||
embedding_model_profile = null;
|
||||
}
|
||||
}
|
||||
if (embedding_model_profile) {
|
||||
this.embedding_model = createModel(embedding_model_profile);
|
||||
}
|
||||
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.');
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.warn('Warning: Failed to initialize embedding model:', err.message);
|
||||
console.log('Continuing anyway, using word-overlap instead.');
|
||||
this.embedding_model = null;
|
||||
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;
|
||||
}
|
||||
|
@ -404,7 +282,7 @@ export class Prompter {
|
|||
await this._saveLog(prompt, to_summarize, resp, 'memSaving');
|
||||
if (resp?.includes('</think>')) {
|
||||
const [_, afterThink] = resp.split('</think>')
|
||||
resp = afterThink
|
||||
resp = afterThink;
|
||||
}
|
||||
return resp;
|
||||
}
|
||||
|
@ -482,6 +360,4 @@ export class Prompter {
|
|||
logFile = path.join(logDir, logFile);
|
||||
await fs.appendFile(logFile, String(logEntry), 'utf-8');
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
Loading…
Add table
Reference in a new issue