connect main process to mindserver, start/stop/restart controls

This commit is contained in:
MaxRobinsonTheGreat 2024-12-01 22:27:42 -06:00
parent b1fd881e65
commit 5278ecb72c
8 changed files with 272 additions and 58 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

@ -12,7 +12,7 @@ import { isOtherAgent, initConversationManager, sendToBot, endAllChats, response
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);
@ -47,7 +44,7 @@ export class Agent {
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;
@ -159,7 +159,7 @@ export class Agent {
}
else {
const translation = await handleTranslation("Hello world! I am "+this.name);
this.bot.chat(translation);
this.openChat(translation);
}
}
@ -204,10 +204,10 @@ export class Agent {
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
@ -325,11 +325,22 @@ export class Agent {
message = message.replaceAll('\n', ' ');
if (self_prompt)
this.bot.chat(message);
this.openChat(message);
else
this.bot.whisper(to_player, message);
}
openChat(message) {
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);
}
}
startEvents() {
// Custom events
this.bot.on('time', () => {
@ -421,7 +432,7 @@ export class Agent {
cleanKill(msg='Killing agent process...') {
this.history.add('system', msg);
this.bot.chat('Restarting.')
this.openChat('Restarting.');
this.history.save();
process.exit(1);
}

View file

@ -2,20 +2,22 @@ import { io } from 'socket.io-client';
import { recieveFromBot, updateAgents } 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;
@ -35,14 +37,15 @@ class ServerProxy {
this.socket.on('agents-update', (agents) => {
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,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} received message from ${agentName}: ${json}`);
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>