Merge pull request #364 from kolbytn/multi-agent-controls

Multi agent controls
This commit is contained in:
Max Robinson 2024-12-08 21:34:14 -06:00 committed by GitHub
commit 13bd9a7bba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 714 additions and 305 deletions

14
main.js
View file

@ -1,8 +1,10 @@
import { AgentProcess } from './src/process/agent-process.js';
import { AgentProcess } from './src/process/agent_process.js';
import settings from './settings.js';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import { createMindServer } from './src/server/mind_server.js';
import { mainProxy } from './src/process/main_proxy.js';
import { readFileSync } from 'fs';
function parseArguments() {
return yargs(hideBin(process.argv))
@ -23,15 +25,19 @@ async function main() {
if (settings.host_mindserver) {
const mindServer = createMindServer();
}
mainProxy.connect();
const args = parseArguments();
const profiles = getProfiles(args);
console.log(profiles);
const { load_memory, init_message } = settings;
for (let i=0; i<profiles.length; i++) {
const agent = new AgentProcess();
agent.start(profiles[i], load_memory, init_message, i);
const agent_process = new AgentProcess();
const profile = readFileSync(profiles[i], 'utf8');
const agent_json = JSON.parse(profile);
mainProxy.registerAgent(agent_json.name, agent_process);
agent_process.start(profiles[i], load_memory, init_message, i);
await new Promise(resolve => setTimeout(resolve, 1000));
}
}

View file

@ -1,7 +1,7 @@
{
"cooldown": 3000,
"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. 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. 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\nConversation Begin:",
"conversing": "You are a playful Minecraft bot named $NAME that can converse with players, see, move, mine, build, and interact with the world by using commands.\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. 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\nConversation Begin:",
"coding": "You are an intelligent mineflayer bot $NAME that plays minecraft by writing javascript codeblocks. Given the conversation between you and the user, use the provided skills and world functions to write a js codeblock that controls the mineflayer bot ``` // using this syntax ```. The code will be executed and you will recieve it's output. If you are satisfied with the response, respond without a codeblock in a conversational way. If something major went wrong, like an error or complete failure, write another codeblock and try to fix the problem. Minor mistakes are acceptable. Be maximally efficient, creative, and clear. Do not use commands !likeThis, only use codeblocks. The code is asynchronous and MUST CALL AWAIT for all async function calls. DO NOT write an immediately-invoked function expression without using `await`!! DO NOT WRITE LIKE THIS: ```(async () => {console.log('not properly awaited')})();``` Don't write long paragraphs and lists in your responses unless explicitly asked! Only summarize the code you write with a sentence or two when done. This is extremely important to me, 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:",
@ -35,6 +35,13 @@
{"role": "assistant", "content": "Hey John, not much. Just saying hi. Bye! !endConversation('john_goodman')"}
],
[
{"role": "system", "content": "work together with the other bot"},
{"role": "assistant", "content": "!startConversation(\"terrance\", \"Hey gpt! Let's work together on this.\"))"},
{"role": "user", "content": "terrance: (FROM OTHER BOT)Sounds good, what should we do first?"},
{"role": "assistant", "content": "I'll start by collecting some resources. !collectBlocks('stone', 10)"}
],
[
{"role": "system", "content": "trade something with zorro_34"},
{"role": "assistant", "content": "!startConversation(\"zorro_34\", \"Hey let's trade. What do you have?\")"},
@ -115,12 +122,22 @@
{"role": "system", "content": "Agent action stopped."},
{"role": "assistant", "content": "I've stopped! What next?"}
],
[
{"role": "system", "content": "Code output: Action failed."},
{"role": "assistant", "content": "Apologies, I made an error. Let me try again. !newAction(\"Try again to complete the task.\")"}
],
[
{"role": "system", "content": "Code output: Could not find any oak_log in 20 blocks."},
{"role": "assistant", "content": "I'll have to search a wider area. !searchForBlock(\"oak_log\", 100)"}
],
[
{"role": "system", "content": "Code output: Placed oak_planks at (-154, 96, -228)."},
{"role": "assistant", "content": "I've placed an oak plank for the house. Wait, I should use newAction to build complicated structures! !newAction(\"Build a house with oak planks\")"}
],
[
{"role": "user", "content": "trixy88: craft some sticks"},
{"role": "assistant", "content": "!craftRecipe(\"stick\", 4)"},
@ -149,7 +166,7 @@
{"role": "user", "content": "brug: Remember that your base is here."},
{"role": "assistant", "content": "Sure, I'll save this location as my base. !rememberHere(\"base\")"},
{"role": "user", "content": "brug: Go to your base."},
{"role": "assistant", "content": "On my way! !goToPlace(\"base\")"}
{"role": "assistant", "content": "On my way! !goToRememberedPlace(\"base\")"}
],
[

View file

@ -3,18 +3,5 @@
"model": "claude-3-5-sonnet-latest",
"embedding": "openai",
"modes": {
"self_preservation": false,
"unstuck": false,
"cowardice": false,
"self_defense": false,
"hunting": false,
"item_collecting": false,
"torch_placing": false,
"elbow_room": false,
"idle_staring": true,
"cheat": true
}
"embedding": "openai"
}

View file

@ -23,6 +23,7 @@ export default
],
"load_memory": false, // load memory from previous session
"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
"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...
@ -36,4 +37,3 @@ export default
"narrate_behavior": true, // chat simple automatic actions ('Picking up item!')
"chat_bot_messages": true, // publicly chat messages to other bots
}

View file

@ -108,10 +108,16 @@ export class ActionManager {
this.currentActionFn = null;
clearTimeout(TIMEOUT);
this.cancelResume();
console.error("Code execution triggered catch: " + err);
console.error("Code execution triggered catch:", err);
// Log the full stack trace
console.error(err.stack);
await this.stop();
let message = this._getBotOutputSummary() + '!!Code threw exception!! Error: ' + err;
let message = this._getBotOutputSummary() +
'!!Code threw exception!!\n' +
'Error: ' + err + '\n' +
'Stack trace:\n' + err.stack;
let interrupted = this.agent.bot.interrupt_code;
this.agent.clearBotLogs();
if (!interrupted && !this.agent.coder.generating) {

View file

@ -8,11 +8,11 @@ import { ActionManager } from './action_manager.js';
import { NPCContoller } from './npc/controller.js';
import { MemoryBank } from './memory_bank.js';
import { SelfPrompter } from './self_prompter.js';
import { isOtherAgent, initConversationManager, sendToBot, endAllChats, responseScheduledFor} from './conversation.js';
import convoManager from './conversation.js';
import { handleTranslation, handleEnglishTranslation } from '../utils/translator.js';
import { addViewer } from './viewer.js';
import settings from '../../settings.js';
import { serverProxy } from './server_proxy.js';
import { serverProxy } from './agent_proxy.js';
export class Agent {
async start(profile_fp, load_mem=false, init_message=null, count_id=0) {
@ -21,9 +21,6 @@ export class Agent {
if (!profile_fp) {
throw new Error('No profile filepath provided');
}
// Connect to MindServer via proxy
serverProxy.connect();
console.log('Starting agent initialization with profile:', profile_fp);
@ -43,11 +40,11 @@ export class Agent {
this.memory_bank = new MemoryBank();
console.log('Initializing self prompter...');
this.self_prompter = new SelfPrompter(this);
initConversationManager(this);
convoManager.initAgent(this);
console.log('Initializing examples...');
await this.prompter.initExamples();
serverProxy.registerAgent(this.name);
serverProxy.connect(this);
console.log(this.name, 'logging into minecraft...');
this.bot = initBot(this.name);
@ -61,6 +58,8 @@ export class Agent {
this.bot.on('login', () => {
console.log(this.name, 'logged in!');
serverProxy.login();
// Set skin for profile, requires Fabric Tailor. (https://modrinth.com/mod/fabrictailor)
if (this.prompter.profile.skin)
@ -113,6 +112,7 @@ export class Agent {
const respondFunc = async (username, message) => {
if (username === this.name) return;
if (settings.only_chat_with.length > 0 && !settings.only_chat_with.includes(username)) return;
try {
if (ignore_messages.some((m) => message.startsWith(m))) return;
@ -120,8 +120,7 @@ export class Agent {
console.log(this.name, 'received message from', username, ':', message);
if (isOtherAgent(username)) {
//recieveFromBot(username, message);
if (convoManager.isOtherAgent(username)) {
console.warn('recieved whisper from other bot??')
}
else {
@ -150,16 +149,21 @@ export class Agent {
this.history.add('system', prompt);
await this.self_prompter.start(prompt);
}
else if (save_data?.last_sender) {
if (save_data?.last_sender) {
this.last_sender = save_data.last_sender;
await this.handleMessage('system', `You have restarted and this message is auto-generated. Continue the conversation with ${this.last_sender}`);
if (convoManager.otherAgentInGame(this.last_sender)) {
const msg_package = {
message: `You have restarted and this message is auto-generated. Continue the conversation with me.`,
start: true
};
convoManager.recieveFromBot(this.last_sender, msg_package);
}
}
else if (init_message) {
await this.handleMessage('system', init_message, 2);
}
else {
const translation = await handleTranslation("Hello world! I am "+this.name);
this.bot.chat(translation);
this.openChat("Hello world! I am "+this.name);
}
}
@ -180,7 +184,7 @@ export class Agent {
if (this.self_prompter.on) {
this.self_prompter.stop(false);
}
endAllChats();
convoManager.endAllConversations();
}
async handleMessage(source, message, max_responses=null) {
@ -193,21 +197,21 @@ export class Agent {
if (max_responses === null) {
max_responses = settings.max_commands === -1 ? Infinity : settings.max_commands;
}
if (max_responses === -1){
if (max_responses === -1) {
max_responses = Infinity;
}
const self_prompt = source === 'system' || source === this.name;
const from_other_bot = isOtherAgent(source);
const from_other_bot = convoManager.isOtherAgent(source);
if (!self_prompt && !from_other_bot) { // from user, check for forced commands
const user_command_name = containsCommand(message);
if (user_command_name) {
if (!commandExists(user_command_name)) {
this.bot.chat(`Command '${user_command_name}' does not exist.`);
this.routeResponse(source, `Command '${user_command_name}' does not exist.`);
return false;
}
this.bot.chat(`*${source} used ${user_command_name.substring(1)}*`);
this.routeResponse(source, `*${source} used ${user_command_name.substring(1)}*`);
if (user_command_name === '!newAction') {
// all user-initiated commands are ignored by the bot except for this one
// add the preceding message to the history to give context for newAction
@ -220,15 +224,15 @@ export class Agent {
}
}
if (!self_prompt)
if (from_other_bot)
this.last_sender = source;
// Now translate the message
message = await handleEnglishTranslation(message);
console.log('received message from', source, ':', message);
const checkInterrupt = () => this.self_prompter.shouldInterrupt(self_prompt) || this.shut_up || responseScheduledFor(source);
const checkInterrupt = () => this.self_prompter.shouldInterrupt(self_prompt) || this.shut_up || convoManager.responseScheduledFor(source);
let behavior_log = this.bot.modes.flushBehaviorLog();
if (behavior_log.trim().length > 0) {
const MAX_LOG = 500;
@ -243,7 +247,6 @@ export class Agent {
await this.history.add(source, message);
this.history.save();
if (!self_prompt && this.self_prompter.on) // message is from user during self-prompting
max_responses = 1; // force only respond to this message, then let self-prompting take over
for (let i=0; i<max_responses; i++) {
@ -251,10 +254,16 @@ export class Agent {
let history = this.history.getHistory();
let res = await this.prompter.promptConvo(history);
console.log(`${this.name} full response to ${source}: ""${res}""`);
if (res.trim().length === 0) {
console.warn('no response')
break; // empty response ends loop
}
let command_name = containsCommand(res);
if (command_name) { // contains query or command
console.log(`Full response: ""${res}""`)
res = truncCommandMessage(res); // everything after the command is ignored
this.history.add(this.name, res);
@ -268,7 +277,7 @@ export class Agent {
this.self_prompter.handleUserPromptedCmd(self_prompt, isAction(command_name));
if (settings.verbose_commands) {
this.routeResponse(source, res, res.indexOf(command_name));
this.routeResponse(source, res);
}
else { // only output command name
let pre_message = res.substring(0, res.indexOf(command_name)).trim();
@ -291,7 +300,6 @@ export class Agent {
else { // conversation response
this.history.add(this.name, res);
this.routeResponse(source, res);
console.log('Purely conversational response:', res);
break;
}
@ -301,22 +309,31 @@ export class Agent {
return used_command;
}
async routeResponse(to_player, message, translate_up_to=-1) {
async routeResponse(to_player, message) {
let self_prompt = to_player === 'system' || to_player === this.name;
if (self_prompt && this.last_sender && !this.self_prompter.on) {
if (self_prompt && this.last_sender) {
// this is for when the agent is prompted by system while still in conversation
// so it can respond to events like death but be routed back to the last sender
to_player = this.last_sender;
}
if (isOtherAgent(to_player)) {
sendToBot(to_player, message);
return;
if (convoManager.isOtherAgent(to_player) && convoManager.inConversation(to_player)) {
// if we're in an ongoing conversation with the other bot, send the response to it
convoManager.sendToBot(to_player, message);
}
else {
// otherwise, use open chat
this.openChat(message);
// note that to_player could be another bot, but if we get here the conversation has ended
}
}
async openChat(message) {
let to_translate = message;
let remaining = '';
if (translate_up_to != -1) {
let command_name = containsCommand(message);
let translate_up_to = command_name ? message.indexOf(command_name) : -1;
if (translate_up_to != -1) { // don't translate the command
to_translate = to_translate.substring(0, translate_up_to);
remaining = message.substring(translate_up_to);
}
@ -324,10 +341,14 @@ export class Agent {
// newlines are interpreted as separate chats, which triggers spam filters. replace them with spaces
message = message.replaceAll('\n', ' ');
if (self_prompt)
if (settings.only_chat_with.length > 0) {
for (let username of settings.only_chat_with) {
this.bot.whisper(username, message);
}
}
else {
this.bot.chat(message);
else
this.bot.whisper(to_player, message);
}
}
startEvents() {
@ -421,7 +442,6 @@ export class Agent {
cleanKill(msg='Killing agent process...') {
this.history.add('system', msg);
this.bot.chat('Restarting.')
this.history.save();
process.exit(1);
}

View file

@ -1,21 +1,23 @@
import { io } from 'socket.io-client';
import { recieveFromBot, updateAgents } from './conversation.js';
import convoManager from './conversation.js';
import settings from '../../settings.js';
class ServerProxy {
class AgentServerProxy {
constructor() {
if (ServerProxy.instance) {
return ServerProxy.instance;
if (AgentServerProxy.instance) {
return AgentServerProxy.instance;
}
this.socket = null;
this.connected = false;
ServerProxy.instance = this;
AgentServerProxy.instance = this;
}
connect() {
connect(agent) {
if (this.connected) return;
this.agent = agent;
this.socket = io(`http://${settings.mindserver_host}:${settings.mindserver_port}`);
this.connected = true;
@ -29,20 +31,21 @@ class ServerProxy {
});
this.socket.on('chat-message', (agentName, json) => {
recieveFromBot(agentName, json);
convoManager.recieveFromBot(agentName, json);
});
this.socket.on('agents-update', (agents) => {
updateAgents(agents);
convoManager.updateAgents(agents);
});
this.socket.on('restart-agent', (agentName) => {
console.log(`Restarting agent: ${agentName}`);
this.agent.cleanKill();
});
}
registerAgent(agentName) {
if (!this.connected) {
console.warn('Cannot register agent: not connected to MindServer');
return;
}
this.socket.emit('register-agent', agentName);
login() {
this.socket.emit('login-agent', this.agent.name);
}
getSocket() {
@ -51,7 +54,7 @@ class ServerProxy {
}
// Create and export a singleton instance
export const serverProxy = new ServerProxy();
export const serverProxy = new AgentServerProxy();
export function sendBotChatToServer(agentName, json) {
serverProxy.getSocket().emit('chat-message', agentName, json);

View file

@ -1,6 +1,6 @@
import * as skills from '../library/skills.js';
import settings from '../../../settings.js';
import { startConversation, endConversation, inConversation, scheduleSelfPrompter, cancelSelfPrompter } from '../conversation.js';
import convoManager from '../conversation.js';
function runAsAction (actionFn, resume = false, timeout = -1) {
let actionLabel = null; // Will be set on first use
@ -56,7 +56,7 @@ export const actionsList = [
name: '!stfu',
description: 'Stop all chatting and self prompting, but continue current action.',
perform: async function (agent) {
agent.bot.chat('Shutting up.');
agent.openChat('Shutting up.');
agent.shutUp();
return;
}
@ -99,15 +99,38 @@ export const actionsList = [
}, true)
},
{
name: '!goToBlock',
description: 'Go to the nearest block of a given type.',
name: '!goToCoordinates',
description: 'Go to the given x, y, z location.',
params: {
'x': {type: 'float', description: 'The x coordinate.', domain: [-Infinity, Infinity]},
'y': {type: 'float', description: 'The y coordinate.', domain: [-64, 320]},
'z': {type: 'float', description: 'The z coordinate.', domain: [-Infinity, Infinity]},
'closeness': {type: 'float', description: 'How close to get to the location.', domain: [0, Infinity]}
},
perform: runAsAction(async (agent, x, y, z, closeness) => {
await skills.goToPosition(agent.bot, x, y, z, closeness);
})
},
{
name: '!searchForBlock',
description: 'Find and go to the nearest block of a given type in a given range.',
params: {
'type': { type: 'BlockName', description: 'The block type to go to.' },
'closeness': { type: 'float', description: 'How close to get to the block.', domain: [0, Infinity] },
'search_range': { type: 'float', description: 'The range to search for the block.', domain: [0, 512] }
'search_range': { type: 'float', description: 'The range to search for the block.', domain: [32, 512] }
},
perform: runAsAction(async (agent, type, closeness, range) => {
await skills.goToNearestBlock(agent.bot, type, closeness, range);
perform: runAsAction(async (agent, block_type, range) => {
await skills.goToNearestBlock(agent.bot, block_type, 4, range);
})
},
{
name: '!searchForEntity',
description: 'Find and go to the nearest entity of a given type in a given range.',
params: {
'type': { type: 'string', description: 'The type of entity to go to.' },
'search_range': { type: 'float', description: 'The range to search for the entity.', domain: [32, 512] }
},
perform: runAsAction(async (agent, entity_type, range) => {
await skills.goToNearestEntity(agent.bot, entity_type, 4, range);
})
},
{
@ -129,7 +152,7 @@ export const actionsList = [
}
},
{
name: '!goToPlace',
name: '!goToRememberedPlace',
description: 'Go to a saved location.',
params: {'name': { type: 'string', description: 'The name of the location to go to.' }},
perform: runAsAction(async (agent, name) => {
@ -150,11 +173,7 @@ export const actionsList = [
'num': { type: 'int', description: 'The number of items to give.', domain: [1, Number.MAX_SAFE_INTEGER] }
},
perform: runAsAction(async (agent, player_name, item_name, num) => {
const modes = agent.bot.modes;
modes.pause('item_collecting');
await skills.giveToPlayer(agent.bot, item_name, player_name, num);
await new Promise(resolve => setTimeout(resolve, 3000));
modes.unpause('item_collecting');
})
},
{
@ -162,8 +181,7 @@ export const actionsList = [
description: 'Eat/drink the given item.',
params: {'item_name': { type: 'ItemName', description: 'The name of the item to consume.' }},
perform: runAsAction(async (agent, item_name) => {
await agent.bot.consume(item_name);
skills.log(agent.bot, `Consumed ${item_name}.`);
await skills.consume(agent.bot, item_name);
})
},
{
@ -342,14 +360,12 @@ export const actionsList = [
'selfPrompt': { type: 'string', description: 'The goal prompt.' },
},
perform: async function (agent, prompt) {
if (inConversation()) {
// if conversing with another bot, dont start self-prompting yet
// wait until conversation ends
if (convoManager.inConversation()) {
agent.self_prompter.setPrompt(prompt);
scheduleSelfPrompter();
convoManager.scheduleSelfPrompter();
}
else {
agent.self_prompter.start(prompt); // don't await, don't return
agent.self_prompter.start(prompt);
}
}
},
@ -358,19 +374,25 @@ export const actionsList = [
description: 'Call when you have accomplished your goal. It will stop self-prompting and the current action. ',
perform: async function (agent) {
agent.self_prompter.stop();
cancelSelfPrompter();
convoManager.cancelSelfPrompter();
return 'Self-prompting stopped.';
}
},
{
name: '!startConversation',
description: 'Send a message to a specific player to initiate conversation.',
description: 'Start a conversation with a player. Use for bots only.',
params: {
'player_name': { type: 'string', description: 'The name of the player to send the message to.' },
'message': { type: 'string', description: 'The message to send.' },
},
perform: async function (agent, player_name, message) {
startConversation(player_name, message);
if (convoManager.inConversation() && !convoManager.inConversation(player_name))
return 'You are already in conversation with other bot.';
if (!convoManager.isOtherAgent(player_name))
return player_name + ' is not a bot, cannot start conversation.';
if (convoManager.inConversation(player_name))
agent.history.add('system', 'You are already in conversation with ' + player_name + ' Don\'t use this command to talk to them.');
convoManager.startConversation(player_name, message);
}
},
{
@ -380,7 +402,10 @@ export const actionsList = [
'player_name': { type: 'string', description: 'The name of the player to end the conversation with.' }
},
perform: async function (agent, player_name) {
endConversation(player_name);
if (!convoManager.inConversation(player_name))
return `Not in conversation with ${player_name}.`;
convoManager.endConversation(player_name);
return `Converstaion with ${player_name} ended.`;
}
}
// { // commented for now, causes confusion with goal command

View file

@ -14,8 +14,8 @@ export function getCommand(name) {
return commandMap[name];
}
const commandRegex = /!(\w+)(?:\(((?:\d+|true|false|"[^"]*")(?:\s*,\s*(?:\d+|true|false|"[^"]*"))*)\))?/
const argRegex = /\d+|true|false|"[^"]*"/g;
const commandRegex = /!(\w+)(?:\(((?:-?\d+(?:\.\d+)?|true|false|"[^"]*")(?:\s*,\s*(?:-?\d+(?:\.\d+)?|true|false|"[^"]*"))*)\))?/
const argRegex = /-?\d+(?:\.\d+)?|true|false|"[^"]*"/g;
export function containsCommand(message) {
const commandMatch = message.match(commandRegex);
@ -109,12 +109,6 @@ export function parseCommandMessage(message) {
if ((arg.startsWith('"') && arg.endsWith('"')) || (arg.startsWith("'") && arg.endsWith("'"))) {
arg = arg.substring(1, arg.length-1);
}
if (arg.includes('=')) {
// this sanitizes syntaxes like "x=2" and ignores the param name
let split = arg.split('=');
args[i] = split[1];
}
//Convert to the correct type
switch(param.type) {

View file

@ -1,6 +1,6 @@
import * as world from '../library/world.js';
import * as mc from '../../utils/mcdata.js';
import { isOtherAgent } from '../conversation.js';
import convoManager from '../conversation.js';
const pad = (str) => {
return '\n' + str + '\n';
@ -48,15 +48,11 @@ export const queryList = [
let players = world.getNearbyPlayerNames(bot);
let bots = [];
for (const player of players) {
if (isOtherAgent(player))
bots.push(player);
}
players = players.filter(p => !isOtherAgent(p));
let bots = convoManager.getInGameAgents().filter(b => b !== agent.name);
players = players.filter(p => !bots.includes(p));
res += '\n- Nearby Human Players: ' + players.join(', ');
res += '\n- Nearby Bot Players: ' + bots.join(', ');
res += '\n- Nearby Human Players: ' + (players.length > 0 ? players.join(', ') : 'None.');
res += '\n- Nearby Bot Players: ' + (bots.length > 0 ? bots.join(', ') : 'None.');
res += '\n' + agent.bot.modes.getMiniDocs() + '\n';
return pad(res);
@ -137,12 +133,8 @@ export const queryList = [
let bot = agent.bot;
let res = 'NEARBY_ENTITIES';
let players = world.getNearbyPlayerNames(bot);
let bots = [];
for (const player of players) {
if (isOtherAgent(player))
bots.push(player);
}
players = players.filter(p => !isOtherAgent(p));
let bots = convoManager.getInGameAgents().filter(b => b !== agent.name);
players = players.filter(p => !bots.includes(p));
for (const player of players) {
res += `\n- Human player: ${player}`;

View file

@ -1,55 +1,14 @@
import settings from '../../settings.js';
import { readFileSync } from 'fs';
import { containsCommand } from './commands/index.js';
import { sendBotChatToServer } from './server_proxy.js';
import { sendBotChatToServer } from './agent_proxy.js';
let agent;
let agent_names = settings.profiles.map((p) => JSON.parse(readFileSync(p, 'utf8')).name);
let agents_in_game = [];
let self_prompter_paused = false;
export function isOtherAgent(name) {
return agent_names.some((n) => n === name);
}
export function updateAgents(names) {
agent_names = names;
}
export function initConversationManager(a) {
agent = a;
}
export function inConversation() {
return Object.values(convos).some(c => c.active);
}
export function endConversation(sender) {
if (convos[sender]) {
convos[sender].end();
if (self_prompter_paused && !inConversation()) {
_resumeSelfPrompter();
}
}
}
export function endAllChats() {
for (const sender in convos) {
convos[sender].end();
}
if (self_prompter_paused) {
_resumeSelfPrompter();
}
}
export function scheduleSelfPrompter() {
self_prompter_paused = true;
}
export function cancelSelfPrompter() {
self_prompter_paused = false;
}
class Conversation {
constructor(name) {
this.name = name;
@ -70,91 +29,235 @@ class Conversation {
end() {
this.active = false;
this.ignore_until_start = true;
this.inMessageTimer = null;
const full_message = _compileInMessages(this);
if (full_message.message.trim().length > 0)
agent.history.add(this.name, full_message.message);
// add the full queued messages to history, but don't respond
if (agent.last_sender === this.name)
agent.last_sender = null;
}
queue(message) {
this.in_queue.push(message);
}
}
const convos = {};
function _getConvo(name) {
if (!convos[name])
convos[name] = new Conversation(name);
return convos[name];
}
export async function startConversation(send_to, message) {
const convo = _getConvo(send_to);
convo.reset();
if (agent.self_prompter.on) {
await agent.self_prompter.stop();
self_prompter_paused = true;
}
convo.active = true;
sendToBot(send_to, message, true);
}
export function sendToBot(send_to, message, start=false) {
if (settings.chat_bot_messages)
agent.bot.chat(`(To ${send_to}) ${message}`);
if (!isOtherAgent(send_to)) {
agent.bot.whisper(send_to, message);
return;
}
const convo = _getConvo(send_to);
if (convo.ignore_until_start)
return;
convo.active = true;
const end = message.includes('!endConversation');
const json = {
'message': message,
start,
end,
};
// agent.bot.whisper(send_to, JSON.stringify(json));
sendBotChatToServer(send_to, JSON.stringify(json));
}
export async function recieveFromBot(sender, json) {
const convo = _getConvo(sender);
// check if any convo is active besides the sender
if (Object.values(convos).some(c => c.active && c.name !== sender)) {
sendToBot(sender, 'I am currently busy. Try again later. !endConversation("' + sender + '")');
return;
const WAIT_TIME_START = 30000;
class ConversationManager {
constructor() {
this.convos = {};
this.activeConversation = null;
this.awaiting_response = false;
this.connection_timeout = null;
this.wait_time_limit = WAIT_TIME_START;
}
console.log(`decoding **${json}**`);
const recieved = JSON.parse(json);
if (recieved.start) {
initAgent(a) {
agent = a;
}
_getConvo(name) {
if (!this.convos[name])
this.convos[name] = new Conversation(name);
return this.convos[name];
}
_startMonitor() {
clearInterval(this.connection_monitor);
let wait_time = 0;
let last_time = Date.now();
this.connection_monitor = setInterval(() => {
if (!this.activeConversation) {
this._stopMonitor();
return; // will clean itself up
}
let delta = Date.now() - last_time;
last_time = Date.now();
let convo_partner = this.activeConversation.name;
if (this.awaiting_response && agent.isIdle()) {
wait_time += delta;
if (wait_time > this.wait_time_limit) {
agent.handleMessage('system', `${convo_partner} hasn't responded in ${this.wait_time_limit/1000} seconds, respond with a message to them or your own action.`);
wait_time = 0;
this.wait_time_limit*=2;
}
}
else if (!this.awaiting_response){
this.wait_time_limit = WAIT_TIME_START;
wait_time = 0;
}
if (!this.otherAgentInGame(convo_partner) && !this.connection_timeout) {
this.connection_timeout = setTimeout(() => {
if (this.otherAgentInGame(convo_partner)){
this._clearMonitorTimeouts();
return;
}
if (!self_prompter_paused) {
this.endConversation(convo_partner);
agent.handleMessage('system', `${convo_partner} disconnected, conversation has ended.`);
}
else {
this.endConversation(convo_partner);
}
}, 10000);
}
}, 1000);
}
_stopMonitor() {
clearInterval(this.connection_monitor);
this.connection_monitor = null;
this._clearMonitorTimeouts();
}
_clearMonitorTimeouts() {
this.awaiting_response = false;
clearTimeout(this.connection_timeout);
this.connection_timeout = null;
}
async startConversation(send_to, message) {
const convo = this._getConvo(send_to);
convo.reset();
if (agent.self_prompter.on) {
await agent.self_prompter.stop();
self_prompter_paused = true;
}
if (convo.active)
return;
convo.active = true;
this.activeConversation = convo;
this._startMonitor();
this.sendToBot(send_to, message, true);
}
if (convo.ignore_until_start)
return;
convo.queue(recieved);
startConversationFromOtherBot(name) {
const convo = this._getConvo(name);
convo.active = true;
this.activeConversation = convo;
this._startMonitor();
}
sendToBot(send_to, message, start=false) {
if (!this.isOtherAgent(send_to)) {
agent.bot.whisper(send_to, message);
return;
}
const convo = this._getConvo(send_to);
if (settings.chat_bot_messages && !start)
agent.openChat(`(To ${send_to}) ${message}`);
if (convo.ignore_until_start)
return;
convo.active = true;
const end = message.includes('!endConversation');
const json = {
'message': message,
start,
end,
};
this.awaiting_response = true;
sendBotChatToServer(send_to, json);
}
async recieveFromBot(sender, recieved) {
const convo = this._getConvo(sender);
// check if any convo is active besides the sender
if (Object.values(this.convos).some(c => c.active && c.name !== sender)) {
this.sendToBot(sender, `I'm talking to someone else, try again later. !endConversation("${sender}")`);
return;
}
// responding to conversation takes priority over self prompting
if (agent.self_prompter.on){
await agent.self_prompter.stopLoop();
if (recieved.start) {
convo.reset();
this.startConversationFromOtherBot(sender);
}
if (convo.ignore_until_start)
return;
this._clearMonitorTimeouts();
convo.queue(recieved);
// responding to conversation takes priority over self prompting
if (agent.self_prompter.on){
await agent.self_prompter.stopLoop();
self_prompter_paused = true;
}
_scheduleProcessInMessage(sender, recieved, convo);
}
responseScheduledFor(sender) {
if (!this.isOtherAgent(sender) || !this.inConversation(sender))
return false;
const convo = this._getConvo(sender);
return !!convo.inMessageTimer;
}
isOtherAgent(name) {
return agent_names.some((n) => n === name);
}
otherAgentInGame(name) {
return agents_in_game.some((n) => n === name);
}
updateAgents(agents) {
agent_names = agents.map(a => a.name);
agents_in_game = agents.filter(a => a.in_game).map(a => a.name);
}
getInGameAgents() {
return agents_in_game;
}
inConversation(other_agent=null) {
if (other_agent)
return this.convos[other_agent]?.active;
return Object.values(this.convos).some(c => c.active);
}
endConversation(sender) {
if (this.convos[sender]) {
this.convos[sender].end();
this._stopMonitor();
this.activeConversation = null;
if (self_prompter_paused && !this.inConversation()) {
_resumeSelfPrompter();
}
}
}
endAllConversations() {
for (const sender in this.convos) {
this.convos[sender].end();
}
if (self_prompter_paused) {
_resumeSelfPrompter();
}
}
scheduleSelfPrompter() {
self_prompter_paused = true;
}
_scheduleProcessInMessage(sender, recieved, convo);
}
// returns true if the other bot has a scheduled response
export function responseScheduledFor(sender) {
if (!isOtherAgent(sender))
return false;
const convo = _getConvo(sender);
return !!convo.inMessageTimer;
cancelSelfPrompter() {
self_prompter_paused = false;
}
}
const convoManager = new ConversationManager();
export default convoManager;
/*
This function controls conversation flow by deciding when the bot responds.
@ -205,7 +308,11 @@ async function _scheduleProcessInMessage(sender, recieved, convo) {
}
function _processInMessageQueue(name) {
const convo = _getConvo(name);
const convo = convoManager._getConvo(name);
_handleFullInMessage(name, _compileInMessages(convo));
}
function _compileInMessages(convo) {
let pack = {};
let full_message = '';
while (convo.in_queue.length > 0) {
@ -213,19 +320,22 @@ function _processInMessageQueue(name) {
full_message += pack.message;
}
pack.message = full_message;
_handleFullInMessage(name, pack);
return pack;
}
function _handleFullInMessage(sender, recieved) {
console.log(`responding to **${JSON.stringify(recieved)}**`);
console.log(`${agent.name} responding to "${recieved.message}" from ${sender}`);
const convo = _getConvo(sender);
const convo = convoManager._getConvo(sender);
convo.active = true;
const message = _tagMessage(recieved.message);
if (recieved.end)
convo.end();
if (recieved.start)
let message = _tagMessage(recieved.message);
if (recieved.end) {
convoManager.endConversation(sender);
sender = 'system'; // bot will respond to system instead of the other bot
message = `Conversation with ${sender} ended with message: "${message}"`;
}
else if (recieved.start)
agent.shut_up = false;
convo.inMessageTimer = null;
agent.handleMessage(sender, message);

View file

@ -4,10 +4,8 @@ import pf from 'mineflayer-pathfinder';
import Vec3 from 'vec3';
export function log(bot, message, chat=false) {
export function log(bot, message) {
bot.output += message + '\n';
if (chat)
bot.chat(message);
}
async function autoLight(bot) {
@ -312,8 +310,6 @@ export async function attackEntity(bot, entity, kill=true) {
**/
let pos = entity.position;
console.log(bot.entity.position.distanceTo(pos))
await equipHighestAttack(bot)
if (!kill) {
@ -585,7 +581,9 @@ export async function placeBlock(bot, blockType, x, y, z, placeOn='bottom', dont
if (blockType === 'ladder' || blockType === 'repeater' || blockType === 'comparator') {
blockType += `[facing=${face}]`;
}
if (blockType.includes('stairs')) {
blockType += `[facing=${face}]`;
}
let msg = '/setblock ' + Math.floor(x) + ' ' + Math.floor(y) + ' ' + Math.floor(z) + ' ' + blockType;
bot.chat(msg);
if (blockType.includes('door'))
@ -758,7 +756,7 @@ export async function discard(bot, itemName, num=-1) {
log(bot, `You do not have any ${itemName} to discard.`);
return false;
}
log(bot, `Successfully discarded ${discarded} ${itemName}.`);
log(bot, `Discarded ${discarded} ${itemName}.`);
return true;
}
@ -850,23 +848,19 @@ export async function viewChest(bot) {
return true;
}
export async function eat(bot, foodName="") {
export async function consume(bot, itemName="") {
/**
* Eat the given item. If no item is given, it will eat the first food item in the bot's inventory.
* Eat/drink the given item.
* @param {MinecraftBot} bot, reference to the minecraft bot.
* @param {string} item, the item to eat.
* @param {string} itemName, the item to eat/drink.
* @returns {Promise<boolean>} true if the item was eaten, false otherwise.
* @example
* await skills.eat(bot, "apple");
**/
let item, name;
if (foodName) {
item = bot.inventory.items().find(item => item.name === foodName);
name = foodName;
}
else {
item = bot.inventory.items().find(item => item.foodRecovery > 0);
name = "food";
if (itemName) {
item = bot.inventory.items().find(item => item.name === itemName);
name = itemName;
}
if (!item) {
log(bot, `You do not have any ${name} to eat.`);
@ -874,7 +868,7 @@ export async function eat(bot, foodName="") {
}
await bot.equip(item, 'hand');
await bot.consume();
log(bot, `Successfully ate ${item.name}.`);
log(bot, `Consumed ${item.name}.`);
return true;
}
@ -895,13 +889,41 @@ export async function giveToPlayer(bot, itemType, username, num=1) {
log(bot, `Could not find ${username}.`);
return false;
}
await goToPlayer(bot, username);
await goToPlayer(bot, username, 3);
// if we are 2 below the player
log(bot, bot.entity.position.y, player.position.y);
if (bot.entity.position.y < player.position.y - 1) {
await goToPlayer(bot, username, 1);
}
// if we are too close, make some distance
if (bot.entity.position.distanceTo(player.position) < 2) {
let goal = new pf.goals.GoalNear(player.position.x, player.position.y, player.position.z, 2);
let inverted_goal = new pf.goals.GoalInvert(goal);
bot.pathfinder.setMovements(new pf.Movements(bot));
await bot.pathfinder.goto(inverted_goal);
}
await bot.lookAt(player.position);
if (await discard(bot, itemType, num)) {
log(bot, `${num} ${itemType} has been given to ${username}.`);
await new Promise(resolve => setTimeout(resolve, 2000));
return true;
let given = false;
bot.once('playerCollect', (collector, collected) => {
console.log(collected.name);
if (collector.username === username) {
log(bot, `${username} recieved ${itemType}.`);
given = true;
}
});
let start = Date.now();
while (!given && !bot.interrupt_code) {
await new Promise(resolve => setTimeout(resolve, 500));
if (given) {
return true;
}
if (Date.now() - start > 3000) {
break;
}
}
}
log(bot, `Failed to give ${itemType} to ${username}, it was never received.`);
return false;
}
@ -961,6 +983,26 @@ export async function goToNearestBlock(bot, blockType, min_distance=2, range=64
}
export async function goToNearestEntity(bot, entityType, min_distance=2, range=64) {
/**
* Navigate to the nearest entity of the given type.
* @param {MinecraftBot} bot, reference to the minecraft bot.
* @param {string} entityType, the type of entity to navigate to.
* @param {number} min_distance, the distance to keep from the entity. Defaults to 2.
* @param {number} range, the range to look for the entity. Defaults to 64.
* @returns {Promise<boolean>} true if the entity was reached, false otherwise.
**/
let entity = world.getNearestEntityWhere(bot, entity => entity.name === entityType, range);
if (!entity) {
log(bot, `Could not find any ${entityType} in ${range} blocks.`);
return false;
}
let distance = bot.entity.position.distanceTo(entity.position);
log(bot, `Found ${entityType} ${distance} blocks away.`);
await goToPosition(bot, entity.position.x, entity.position.y, entity.position.z, min_distance);
return true;
}
export async function goToPlayer(bot, username, distance=3) {
/**
* Navigate to the given player.

View file

@ -238,7 +238,7 @@ export function getNearbyPlayerNames(bot) {
* @example
* let players = world.getNearbyPlayerNames(bot);
**/
let players = getNearbyPlayers(bot, 16);
let players = getNearbyPlayers(bot, 64);
let found = [];
for (let i = 0; i < players.length; i++) {
if (!found.includes(players[i].username) && players[i].username != bot.username) {

View file

@ -2,14 +2,12 @@ import * as skills from './library/skills.js';
import * as world from './library/world.js';
import * as mc from '../utils/mcdata.js';
import settings from '../../settings.js'
import { handleTranslation } from '../utils/translator.js';
import convoManager from './conversation.js';
async function say(agent, message) {
agent.bot.modes.behavior_log += message + '\n';
if (agent.shut_up || !settings.narrate_behavior) return;
let translation = await handleTranslation(message);
agent.bot.chat(translation);
agent.openChat(message);
}
// a mode is a function that is called every tick to respond immediately to the world
@ -297,7 +295,7 @@ async function execute(mode, agent, func, timeout=-1) {
if (should_reprompt) {
// auto prompt to respond to the interruption
let role = agent.last_sender ? agent.last_sender : 'system';
let role = convoManager.inConversation() ? agent.last_sender : 'system';
let logs = agent.bot.modes.flushBehaviorLog();
agent.handleMessage(role, `(AUTO MESSAGE)Your previous action '${interrupted_action}' was interrupted by ${mode.name}.
Your behavior log: ${logs}\nRespond accordingly.`);

View file

@ -34,6 +34,7 @@ export class Prompter {
let chat = this.profile.model;
this.cooldown = this.profile.cooldown ? this.profile.cooldown : 0;
this.last_prompt_time = 0;
this.awaiting_coding = false;
// try to get "max_tokens" parameter, else null
let max_tokens = null;
@ -225,6 +226,8 @@ export class Prompter {
}
async promptConvo(messages) {
this.most_recent_msg_time = Date.now();
let current_msg_time = this.most_recent_msg_time;
for (let i = 0; i < 3; i++) { // try 3 times to avoid hallucinations
await this.checkCooldown();
let prompt = this.profile.conversing;
@ -236,16 +239,27 @@ export class Prompter {
console.warn('LLM hallucinated message as another bot. Trying again...');
continue;
}
if (current_msg_time !== this.most_recent_msg_time) {
console.warn(this.agent.name + ' recieved new message while generating, discarding old response.');
return '';
}
return generation;
}
return "*no response*";
return '';
}
async promptCoding(messages) {
if (this.awaiting_coding) {
console.warn('Already awaiting coding response, returning no response.');
return '```//no response```';
}
this.awaiting_coding = true;
await this.checkCooldown();
let prompt = this.profile.coding;
prompt = await this.replaceStrings(prompt, messages, this.coding_examples);
return await this.chat_model.sendRequest(messages, prompt);
let resp = await this.chat_model.sendRequest(messages, prompt);
this.awaiting_coding = false;
return resp;
}
async promptMemSaving(to_summarize) {

View file

@ -45,7 +45,7 @@ export class SelfPrompter {
no_command_count++;
if (no_command_count >= MAX_NO_COMMAND) {
let out = `Agent did not use command in the last ${MAX_NO_COMMAND} auto-prompts. Stopping auto-prompting.`;
this.agent.bot.chat(out);
this.agent.openChat(out);
console.warn(out);
this.on = false;
break;

View file

@ -45,7 +45,8 @@ export class Grok {
res = 'My brain disconnected, try again.';
}
}
return res;
// sometimes outputs special token <|separator|>, just replace it
return res.replace(/<\|separator\|>/g, '*no response*');
}
async embed(text) {

View file

@ -1,10 +1,13 @@
import { spawn } from 'child_process';
import { mainProxy } from './main_proxy.js';
export class AgentProcess {
static runningCount = 0;
start(profile, load_memory=false, init_message=null, count_id=0) {
let args = ['src/process/init-agent.js', this.name];
this.profile = profile;
this.count_id = count_id;
this.running = true;
let args = ['src/process/init_agent.js', this.name];
args.push('-p', profile);
args.push('-c', count_id);
if (load_memory)
@ -16,21 +19,17 @@ export class AgentProcess {
stdio: 'inherit',
stderr: 'inherit',
});
AgentProcess.runningCount++;
let last_restart = Date.now();
agentProcess.on('exit', (code, signal) => {
console.log(`Agent process exited with code ${code} and signal ${signal}`);
this.running = false;
mainProxy.logoutAgent(this.name);
if (code !== 0) {
if (code !== 0 && signal !== 'SIGINT') {
// agent must run for at least 10 seconds before restarting
if (Date.now() - last_restart < 10000) {
console.error(`Agent process ${profile} exited too quickly and will not be restarted.`);
AgentProcess.runningCount--;
if (AgentProcess.runningCount <= 0) {
console.error('All agent processes have ended. Exiting.');
process.exit(0);
}
return;
}
console.log('Restarting agent...');
@ -42,5 +41,18 @@ export class AgentProcess {
agentProcess.on('error', (err) => {
console.error('Agent process error:', err);
});
this.process = agentProcess;
}
stop() {
if (!this.running) return;
this.process.kill('SIGINT');
}
continue() {
if (!this.running) {
this.start(this.profile, true, 'Agent process restarted.', this.count_id);
}
}
}

54
src/process/main_proxy.js Normal file
View file

@ -0,0 +1,54 @@
import { io } from 'socket.io-client';
import settings from '../../settings.js';
// Singleton mindserver proxy for the main process
class MainProxy {
constructor() {
if (MainProxy.instance) {
return MainProxy.instance;
}
this.socket = null;
this.connected = false;
this.agent_processes = {};
MainProxy.instance = this;
}
connect() {
if (this.connected) return;
this.socket = io(`http://${settings.mindserver_host}:${settings.mindserver_port}`);
this.connected = true;
this.socket.on('stop-agent', (agentName) => {
if (this.agent_processes[agentName]) {
this.agent_processes[agentName].stop();
}
});
this.socket.on('start-agent', (agentName) => {
if (this.agent_processes[agentName]) {
this.agent_processes[agentName].continue();
}
});
this.socket.on('register-agents-success', () => {
console.log('Agents registered');
});
}
addAgent(agent) {
this.agent_processes.push(agent);
}
logoutAgent(agentName) {
this.socket.emit('logout-agent', agentName);
}
registerAgent(name, process) {
this.socket.emit('register-agents', [name]);
this.agent_processes[name] = process;
}
}
export const mainProxy = new MainProxy();

View file

@ -7,7 +7,9 @@ import { fileURLToPath } from 'url';
// Module-level variables
let io;
let server;
const connectedAgents = {};
const registeredAgents = new Set();
const inGameAgents = {};
const agentManagers = {}; // socket for main process that registers/controls agents
// Initialize the server
export function createMindServer(port = 8080) {
@ -24,28 +26,81 @@ export function createMindServer(port = 8080) {
let curAgentName = null;
console.log('Client connected');
socket.emit('agents-update', Object.keys(connectedAgents));
agentsUpdate(socket);
socket.on('register-agent', (agentName) => {
console.log('Agent registered:', agentName);
connectedAgents[agentName] = socket;
curAgentName = agentName;
io.emit('agents-update', Object.keys(connectedAgents));
socket.on('register-agents', (agentNames) => {
console.log(`Registering agents: ${agentNames}`);
agentNames.forEach(name => registeredAgents.add(name));
for (let name of agentNames) {
agentManagers[name] = socket;
}
socket.emit('register-agents-success');
agentsUpdate();
});
socket.on('chat-message', (agentName, json) => {
console.log(`${curAgentName} received message from ${agentName}: ${json}`);
const agentSocket = connectedAgents[agentName];
if (agentSocket) {
agentSocket.emit('chat-message', curAgentName, json);
socket.on('login-agent', (agentName) => {
if (curAgentName && curAgentName !== agentName) {
console.warn(`Agent ${agentName} already logged in as ${curAgentName}`);
return;
}
if (registeredAgents.has(agentName)) {
curAgentName = agentName;
inGameAgents[agentName] = socket;
agentsUpdate();
} else {
console.warn(`Agent ${agentName} not registered`);
}
});
socket.on('logout-agent', (agentName) => {
if (inGameAgents[agentName]) {
delete inGameAgents[agentName];
agentsUpdate();
}
});
socket.on('disconnect', () => {
console.log('Client disconnected');
delete connectedAgents[socket.id];
io.emit('agents-update', Object.keys(connectedAgents));
if (inGameAgents[curAgentName]) {
delete inGameAgents[curAgentName];
agentsUpdate();
}
});
socket.on('chat-message', (agentName, json) => {
if (!inGameAgents[agentName]) {
console.warn(`Agent ${agentName} tried to send a message but is not logged in`);
return;
}
console.log(`${curAgentName} sending message to ${agentName}: ${json.message}`);
inGameAgents[agentName].emit('chat-message', curAgentName, json);
});
socket.on('restart-agent', (agentName) => {
console.log(`Restarting agent: ${agentName}`);
inGameAgents[agentName].emit('restart-agent');
});
socket.on('stop-agent', (agentName) => {
let manager = agentManagers[agentName];
if (manager) {
manager.emit('stop-agent', agentName);
}
else {
console.warn(`Stopping unregisterd agent ${agentName}`);
}
});
socket.on('start-agent', (agentName) => {
let manager = agentManagers[agentName];
if (manager) {
manager.emit('start-agent', agentName);
}
else {
console.warn(`Starting unregisterd agent ${agentName}`);
}
});
});
server.listen(port, 'localhost', () => {
@ -54,6 +109,18 @@ export function createMindServer(port = 8080) {
return server;
}
function agentsUpdate(socket) {
if (!socket) {
socket = io;
}
let agents = [];
registeredAgents.forEach(name => {
agents.push({name, in_game: !!inGameAgents[name]});
});
socket.emit('agents-update', agents);
}
// Optional: export these if you need access to them from other files
export const getIO = () => io;
export const getServer = () => server;

View file

@ -1,33 +1,67 @@
<!DOCTYPE html>
<html>
<head>
<title>Mindcraft Agents</title>
<title>Mindcraft</title>
<script src="/socket.io/socket.io.js"></script>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
background: #f0f0f0;
background: #1a1a1a;
color: #e0e0e0;
}
#agents {
background: white;
background: #2d2d2d;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
h1 {
color: #333;
color: #ffffff;
}
.agent {
margin: 10px 0;
padding: 10px;
background: #f8f8f8;
background: #363636;
border-radius: 4px;
display: flex;
justify-content: space-between;
align-items: center;
}
.restart-btn, .start-btn, .stop-btn {
color: white;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
margin-left: 5px;
}
.restart-btn {
background: #4CAF50;
}
.start-btn {
background: #2196F3;
}
.stop-btn {
background: #f44336;
}
.restart-btn:hover { background: #45a049; }
.start-btn:hover { background: #1976D2; }
.stop-btn:hover { background: #d32f2f; }
.status-icon {
font-size: 12px;
margin-right: 8px;
}
.status-icon.online {
color: #4CAF50;
}
.status-icon.offline {
color: #f44336;
}
</style>
</head>
<body>
<h1>Connected Mindcraft Agents</h1>
<h1>Mindcraft</h1>
<div id="agents"></div>
<script>
@ -36,9 +70,36 @@
socket.on('agents-update', (agents) => {
agentsDiv.innerHTML = agents.length ?
agents.map(name => `<div class="agent">${name}</div>`).join('') :
agents.map(agent => `
<div class="agent">
<span>
<span class="status-icon ${agent.in_game ? 'online' : 'offline'}"></span>
${agent.name}
</span>
<div>
${agent.in_game ? `
<button class="stop-btn" onclick="stopAgent('${agent.name}')">Stop</button>
<button class="restart-btn" onclick="restartAgent('${agent.name}')">Restart</button>
` : `
<button class="start-btn" onclick="startAgent('${agent.name}')">Start</button>
`}
</div>
</div>
`).join('') :
'<div class="agent">No agents connected</div>';
});
function restartAgent(agentName) {
socket.emit('restart-agent', agentName);
}
function startAgent(agentName) {
socket.emit('start-agent', agentName);
}
function stopAgent(agentName) {
socket.emit('stop-agent', agentName);
}
</script>
</body>
</html>