diff --git a/.gitignore b/.gitignore index 343d841..d838f96 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,5 @@ tasks/construction_tasks/train_multiagent_construction_tasks.json tasks/construction_tasks/test/** tasks/construction_tasks/train/** server_data* -**/.DS_Store \ No newline at end of file +**/.DS_Store +src/mindcraft-py/__pycache__/ diff --git a/main.js b/main.js index 521aadf..590348c 100644 --- a/main.js +++ b/main.js @@ -1,9 +1,7 @@ -import { AgentProcess } from './src/process/agent_process.js'; +import * as Mindcraft from './src/mindcraft/mindcraft.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() { @@ -24,35 +22,51 @@ function parseArguments() { .alias('help', 'h') .parse(); } - -function getProfiles(args) { - return args.profiles || settings.profiles; +const args = parseArguments(); +if (args.profiles) { + settings.profiles = args.profiles; } - -async function main() { - if (settings.host_mindserver) { - const mindServer = createMindServer(settings.mindserver_port); +if (args.task_path) { + let tasks = JSON.parse(readFileSync(args.task_path, 'utf8')); + if (args.task_id) { + settings.task = tasks[args.task_id]; + settings.task.task_id = args.task_id; } - mainProxy.connect(); - - const args = parseArguments(); - const profiles = getProfiles(args); - console.log(profiles); - const { load_memory, init_message } = settings; - - for (let i=0; i setTimeout(resolve, 1000)); + else { + throw new Error('task_id is required when task_path is provided'); } } -try { - main(); -} catch (error) { - console.error('An error occurred:', error); - process.exit(1); +// these environment variables override certain settings +if (process.env.MINECRAFT_PORT) { + settings.port = process.env.MINECRAFT_PORT; } +if (process.env.MINDSERVER_PORT) { + settings.mindserver_port = process.env.MINDSERVER_PORT; +} +if (process.env.PROFILES && JSON.parse(process.env.PROFILES).length > 0) { + settings.profiles = JSON.parse(process.env.PROFILES); +} +if (process.env.INSECURE_CODING) { + settings.allow_insecure_coding = true; +} +if (process.env.BLOCKED_ACTIONS) { + settings.blocked_actions = JSON.parse(process.env.BLOCKED_ACTIONS); +} +if (process.env.MAX_MESSAGES) { + settings.max_messages = process.env.MAX_MESSAGES; +} +if (process.env.NUM_EXAMPLES) { + settings.num_examples = process.env.NUM_EXAMPLES; +} +if (process.env.LOG_ALL) { + settings.log_all_prompts = process.env.LOG_ALL; +} + +Mindcraft.init(false, settings.mindserver_port); + +for (let profile of settings.profiles) { + const profile_json = JSON.parse(readFileSync(profile, 'utf8')); + settings.profile = profile_json; + Mindcraft.createAgent(settings); +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index cc29f6b..63e7bb8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ botocore==1.37.11 pandas==2.2.3 prettytable==3.16.0 tqdm==4.62.3 +python-socketio[client] \ No newline at end of file diff --git a/settings.js b/settings.js index b782097..4e191ec 100644 --- a/settings.js +++ b/settings.js @@ -5,12 +5,9 @@ const settings = { "auth": "offline", // or "microsoft" // the mindserver manages all agents and hosts the UI - "host_mindserver": true, // if true, the mindserver will be hosted on this machine. otherwise, specify a public IP address - "mindserver_host": "localhost", "mindserver_port": 8080, - // the base profile is shared by all bots for default prompts/examples/modes - "base_profile": "./profiles/defaults/survival.json", // also see creative.json, god_mode.json + "base_profile": "survival", // survival, creative, or god_mode "profiles": [ "./andy.json", // "./profiles/gpt.json", @@ -25,12 +22,13 @@ const settings = { // using more than 1 profile requires you to /msg each bot indivually // individual profiles override values from the base profile ], + "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 "speak": false, // allows all bots to speak through system text-to-speech. works on windows, mac, on linux you need to `apt install espeak` "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... + "render_bot_views": 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 "allow_vision": false, // allows vision model to interpret screenshots as inputs @@ -47,30 +45,4 @@ const settings = { "log_all_prompts": false, // log ALL prompts to file } -// these environment variables override certain settings -if (process.env.MINECRAFT_PORT) { - settings.port = process.env.MINECRAFT_PORT; -} -if (process.env.MINDSERVER_PORT) { - settings.mindserver_port = process.env.MINDSERVER_PORT; -} -if (process.env.PROFILES && JSON.parse(process.env.PROFILES).length > 0) { - settings.profiles = JSON.parse(process.env.PROFILES); -} -if (process.env.INSECURE_CODING) { - settings.allow_insecure_coding = true; -} -if (process.env.BLOCKED_ACTIONS) { - settings.blocked_actions = JSON.parse(process.env.BLOCKED_ACTIONS); -} -if (process.env.MAX_MESSAGES) { - settings.max_messages = process.env.MAX_MESSAGES; -} -if (process.env.NUM_EXAMPLES) { - settings.num_examples = process.env.NUM_EXAMPLES; -} -if (process.env.LOG_ALL) { - settings.log_all_prompts = process.env.LOG_ALL; -} - export default settings; diff --git a/src/agent/agent.js b/src/agent/agent.js index 3cd671b..7bc692d 100644 --- a/src/agent/agent.js +++ b/src/agent/agent.js @@ -12,41 +12,28 @@ 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 settings from '../../settings.js'; -import { serverProxy } from './agent_proxy.js'; +import { serverProxy } from './mindserver_proxy.js'; +import settings from './settings.js'; import { Task } from './tasks/tasks.js'; import { say } from './speak.js'; export class Agent { - async start(profile_fp, load_mem=false, init_message=null, count_id=0, task_path=null, task_id=null) { + async start(load_mem=false, init_message=null, count_id=0) { this.last_sender = null; this.count_id = count_id; - if (!profile_fp) { - throw new Error('No profile filepath provided'); - } - - console.log('Starting agent initialization with profile:', profile_fp); // Initialize components with more detailed error handling - console.log('Initializing action manager...'); + console.log(`Initializing agent ${this.name}...`); this.actions = new ActionManager(this); - console.log('Initializing prompter...'); - this.prompter = new Prompter(this, profile_fp); + this.prompter = new Prompter(this, settings.profile); this.name = this.prompter.getName(); - console.log('Initializing history...'); this.history = new History(this); - console.log('Initializing coder...'); this.coder = new Coder(this); - console.log('Initializing npc controller...'); this.npc = new NPCContoller(this); - console.log('Initializing memory bank...'); this.memory_bank = new MemoryBank(); - console.log('Initializing self prompter...'); this.self_prompter = new SelfPrompter(this); convoManager.initAgent(this); - console.log('Initializing examples...'); await this.prompter.initExamples(); - console.log('Initializing task...'); // load mem first before doing task let save_data = null; @@ -59,19 +46,15 @@ export class Agent { } else { taskStart = Date.now(); } - this.task = new Task(this, task_path, task_id, taskStart); + this.task = new Task(this, settings.task, taskStart); this.blocked_actions = settings.blocked_actions.concat(this.task.blocked_actions || []); blacklistCommands(this.blocked_actions); - serverProxy.connect(this); - console.log(this.name, 'logging into minecraft...'); this.bot = initBot(this.name); initModes(this); - - this.bot.on('login', () => { console.log(this.name, 'logged in!'); serverProxy.login(); @@ -90,6 +73,8 @@ export class Agent { try { clearTimeout(spawnTimeout); addBrowserViewer(this.bot, count_id); + console.log('Initializing vision intepreter...'); + this.vision_interpreter = new VisionInterpreter(this, settings.allow_vision); // wait for a bit so stats are not undefined await new Promise((resolve) => setTimeout(resolve, 1000)); @@ -101,22 +86,19 @@ export class Agent { this.startEvents(); if (!load_mem) { - if (task_path !== null) { + if (settings.task) { this.task.initBotTask(); this.task.setAgentGoal(); } } else { // set the goal without initializing the rest of the task - if (task_path !== null) { + if (settings.task) { this.task.setAgentGoal(); } } await new Promise((resolve) => setTimeout(resolve, 10000)); this.checkAllPlayersPresent(); - - console.log('Initializing vision intepreter...'); - this.vision_interpreter = new VisionInterpreter(this, settings.allow_vision); } catch (error) { console.error('Error in spawn event:', error); @@ -160,8 +142,12 @@ export class Agent { this.respondFunc = respondFunc; this.bot.on('whisper', respondFunc); - if (settings.profiles.length === 1) - this.bot.on('chat', respondFunc); + + this.bot.on('chat', (username, message) => { + if (serverProxy.getNumOtherAgents() > 0) return; + // only respond to open chat messages when there are no other agents + respondFunc(username, message); + }); // Set up auto-eat this.bot.autoEat.options = { diff --git a/src/agent/agent_proxy.js b/src/agent/agent_proxy.js deleted file mode 100644 index b0333b1..0000000 --- a/src/agent/agent_proxy.js +++ /dev/null @@ -1,73 +0,0 @@ -import { io } from 'socket.io-client'; -import convoManager from './conversation.js'; -import settings from '../../settings.js'; - -class AgentServerProxy { - constructor() { - if (AgentServerProxy.instance) { - return AgentServerProxy.instance; - } - - this.socket = null; - this.connected = false; - AgentServerProxy.instance = this; - } - - connect(agent) { - if (this.connected) return; - - this.agent = agent; - - this.socket = io(`http://${settings.mindserver_host}:${settings.mindserver_port}`); - this.connected = true; - - this.socket.on('connect', () => { - console.log('Connected to MindServer'); - }); - - this.socket.on('disconnect', () => { - console.log('Disconnected from MindServer'); - this.connected = false; - }); - - this.socket.on('chat-message', (agentName, json) => { - convoManager.receiveFromBot(agentName, json); - }); - - this.socket.on('agents-update', (agents) => { - convoManager.updateAgents(agents); - }); - - this.socket.on('restart-agent', (agentName) => { - console.log(`Restarting agent: ${agentName}`); - this.agent.cleanKill(); - }); - - this.socket.on('send-message', (agentName, message) => { - try { - this.agent.respondFunc("NO USERNAME", message); - } catch (error) { - console.error('Error: ', JSON.stringify(error, Object.getOwnPropertyNames(error))); - } - }); - } - - login() { - this.socket.emit('login-agent', this.agent.name); - } - - shutdown() { - this.socket.emit('shutdown'); - } - - getSocket() { - return this.socket; - } -} - -// Create and export a singleton instance -export const serverProxy = new AgentServerProxy(); - -export function sendBotChatToServer(agentName, json) { - serverProxy.getSocket().emit('chat-message', agentName, json); -} diff --git a/src/agent/coder.js b/src/agent/coder.js index 956c8fe..18a5f26 100644 --- a/src/agent/coder.js +++ b/src/agent/coder.js @@ -1,6 +1,5 @@ import { writeFile, readFile, mkdirSync } from 'fs'; -import settings from '../../settings.js'; -import { makeCompartment } from './library/lockdown.js'; +import { makeCompartment, lockdown } from './library/lockdown.js'; import * as skills from './library/skills.js'; import * as world from './library/world.js'; import { Vec3 } from 'vec3'; @@ -27,6 +26,7 @@ export class Coder { async generateCode(agent_history) { this.agent.bot.modes.pause('unstuck'); + lockdown(); // this message history is transient and only maintained in this function let messages = agent_history.getHistory(); messages.push({role: 'system', content: 'Code generation started. Write code in codeblock in your response:'}); diff --git a/src/agent/commands/actions.js b/src/agent/commands/actions.js index b2b3ccb..e321764 100644 --- a/src/agent/commands/actions.js +++ b/src/agent/commands/actions.js @@ -1,5 +1,5 @@ import * as skills from '../library/skills.js'; -import settings from '../../../settings.js'; +import settings from '../settings.js'; import convoManager from '../conversation.js'; @@ -46,7 +46,7 @@ export const actionsList = [ result = 'Error generating code: ' + e.toString(); } }; - await agent.actions.runAction('action:newAction', actionFn); + await agent.actions.runAction('action:newAction', actionFn, {timeout: settings.code_timeout_mins}); return result; } }, diff --git a/src/agent/conversation.js b/src/agent/conversation.js index 41c6888..1cd781e 100644 --- a/src/agent/conversation.js +++ b/src/agent/conversation.js @@ -1,10 +1,9 @@ -import settings from '../../settings.js'; -import { readFileSync } from 'fs'; +import settings from './settings.js'; import { containsCommand } from './commands/index.js'; -import { sendBotChatToServer } from './agent_proxy.js'; +import { sendBotChatToServer } from './mindserver_proxy.js'; let agent; -let agent_names = settings.profiles.map((p) => JSON.parse(readFileSync(p, 'utf8')).name); +let agent_names = []; let agents_in_game = []; class Conversation { diff --git a/src/agent/history.js b/src/agent/history.js index 13b9c79..04a72f7 100644 --- a/src/agent/history.js +++ b/src/agent/history.js @@ -1,6 +1,6 @@ import { writeFileSync, readFileSync, mkdirSync, existsSync } from 'fs'; import { NPCData } from './npc/data.js'; -import settings from '../../settings.js'; +import settings from './settings.js'; export class History { diff --git a/src/agent/library/lockdown.js b/src/agent/library/lockdown.js index 2d8f79d..2db7e3f 100644 --- a/src/agent/library/lockdown.js +++ b/src/agent/library/lockdown.js @@ -4,16 +4,22 @@ import 'ses'; // We disable some of the taming to allow for more flexibility // For configuration, see https://github.com/endojs/endo/blob/master/packages/ses/docs/lockdown.md -lockdown({ - // basic devex and quality of life improvements - localeTaming: 'unsafe', - consoleTaming: 'unsafe', - errorTaming: 'unsafe', - stackFiltering: 'verbose', - // allow eval outside of created compartments - // (mineflayer dep "protodef" uses eval) - evalTaming: 'unsafeEval', -}); + +let lockeddown = false; +export function lockdown() { + if (lockeddown) return; + lockeddown = true; + lockdown({ + // basic devex and quality of life improvements + localeTaming: 'unsafe', + consoleTaming: 'unsafe', + errorTaming: 'unsafe', + stackFiltering: 'verbose', + // allow eval outside of created compartments + // (mineflayer dep "protodef" uses eval) + evalTaming: 'unsafeEval', + }); +} export const makeCompartment = (endowments = {}) => { return new Compartment({ diff --git a/src/agent/mindserver_proxy.js b/src/agent/mindserver_proxy.js new file mode 100644 index 0000000..4907253 --- /dev/null +++ b/src/agent/mindserver_proxy.js @@ -0,0 +1,115 @@ +import { io } from 'socket.io-client'; +import convoManager from './conversation.js'; +import { setSettings } from './settings.js'; + +// agents connection to mindserver +// always connect to localhost + +class MindServerProxy { + constructor() { + if (MindServerProxy.instance) { + return MindServerProxy.instance; + } + + this.socket = null; + this.connected = false; + this.agents = []; + MindServerProxy.instance = this; + } + + async connect(name, port) { + if (this.connected) return; + + this.name = name; + this.socket = io(`http://localhost:${port}`); + + await new Promise((resolve, reject) => { + this.socket.on('connect', resolve); + this.socket.on('connect_error', (err) => { + console.error('Connection failed:', err); + reject(err); + }); + }); + + this.connected = true; + console.log(name, 'connected to MindServer'); + + this.socket.on('disconnect', () => { + console.log('Disconnected from MindServer'); + this.connected = false; + }); + + this.socket.on('chat-message', (agentName, json) => { + convoManager.receiveFromBot(agentName, json); + }); + + this.socket.on('agents-update', (agents) => { + this.agents = agents; + convoManager.updateAgents(agents); + if (this.agent?.task) { + console.log(this.agent.name, 'updating available agents'); + this.agent.task.updateAvailableAgents(agents); + } + }); + + this.socket.on('restart-agent', (agentName) => { + console.log(`Restarting agent: ${agentName}`); + this.agent.cleanKill(); + }); + + this.socket.on('send-message', (agentName, message) => { + try { + this.agent.respondFunc("NO USERNAME", message); + } catch (error) { + console.error('Error: ', JSON.stringify(error, Object.getOwnPropertyNames(error))); + } + }); + + // Request settings and wait for response + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error('Settings request timed out after 5 seconds')); + }, 5000); + + this.socket.emit('get-settings', name, (response) => { + clearTimeout(timeout); + if (response.error) { + return reject(new Error(response.error)); + } + setSettings(response.settings); + resolve(); + }); + }); + } + + setAgent(agent) { + this.agent = agent; + } + + getAgents() { + return this.agents; + } + + getNumOtherAgents() { + return this.agents.length - 1; + } + + login() { + this.socket.emit('login-agent', this.agent.name); + } + + shutdown() { + this.socket.emit('shutdown'); + } + + getSocket() { + return this.socket; + } +} + +// Create and export a singleton instance +export const serverProxy = new MindServerProxy(); + +export function sendBotChatToServer(agentName, json) { + serverProxy.getSocket().emit('chat-message', agentName, json); +} diff --git a/src/agent/modes.js b/src/agent/modes.js index 69b2f06..dc2b925 100644 --- a/src/agent/modes.js +++ b/src/agent/modes.js @@ -1,7 +1,7 @@ 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 settings from './settings.js' import convoManager from './conversation.js'; async function say(agent, message) { diff --git a/src/agent/settings.js b/src/agent/settings.js new file mode 100644 index 0000000..e9fd133 --- /dev/null +++ b/src/agent/settings.js @@ -0,0 +1,7 @@ +// extremely lightweight obj that can be imported/modified by any file +let settings = {}; +export default settings; +export function setSettings(new_settings) { + Object.keys(settings).forEach(key => delete settings[key]); + Object.assign(settings, new_settings); +} diff --git a/src/agent/tasks/tasks.js b/src/agent/tasks/tasks.js index a7948f5..b82540e 100644 --- a/src/agent/tasks/tasks.js +++ b/src/agent/tasks/tasks.js @@ -1,7 +1,6 @@ import { readFileSync , writeFileSync, existsSync} from 'fs'; import { executeCommand } from '../commands/index.js'; import { getPosition } from '../library/world.js'; -import settings from '../../../settings.js'; import { ConstructionTaskValidator, Blueprint } from './construction_tasks.js'; import { CookingTaskInitiator } from './cooking_tasks.js'; @@ -233,27 +232,26 @@ class CookingCraftingTaskValidator { } export class Task { - constructor(agent, task_path, task_id, taskStartTime = null) { + constructor(agent, task_data, taskStartTime = null) { this.agent = agent; this.data = null; if (taskStartTime !== null) this.taskStartTime = taskStartTime; else this.taskStartTime = Date.now(); - console.log("Task start time set to", this.taskStartTime); this.validator = null; this.reset_function = null; this.blocked_actions = []; - this.task_id = task_id; - - if (task_path && task_id) { - console.log('Starting task', task_id); - if (task_id.endsWith('hells_kitchen')) { + this.task_data = task_data; + if (task_data) { + console.log('Starting task', task_data.task_id); + console.log("Task start time set to", this.taskStartTime); + if (task_data.task_id.endsWith('hells_kitchen')) { // Reset hells_kitchen progress when a new task starts - hellsKitchenProgressManager.resetTask(task_id); + hellsKitchenProgressManager.resetTask(task_data.task_id); console.log('Reset Hells Kitchen progress for new task'); } - this.data = this.loadTask(task_path, task_id); + this.data = task_data; this.task_type = this.data.type; if (this.task_type === 'construction' && this.data.blueprint) { this.blueprint = new Blueprint(this.data.blueprint); @@ -300,7 +298,11 @@ export class Task { } this.name = this.agent.name; - this.available_agents = settings.profiles.map((p) => JSON.parse(readFileSync(p, 'utf8')).name); + this.available_agents = [] + } + + updateAvailableAgents(agents) { + this.available_agents = agents } // Add this method if you want to manually reset the hells_kitchen progress @@ -360,28 +362,6 @@ export class Task { return null; } - loadTask(task_path, task_id) { - try { - const tasksFile = readFileSync(task_path, 'utf8'); - const tasks = JSON.parse(tasksFile); - let task = tasks[task_id]; - task['task_id'] = task_id; - console.log(task); - console.log(this.agent.count_id); - if (!task) { - throw new Error(`Task ${task_id} not found`); - } - // if ((!task.agent_count || task.agent_count <= 1) && this.agent.count_id > 0) { - // task = null; - // } - - return task; - } catch (error) { - console.error('Error loading task:', error); - process.exit(1); - } - } - isDone() { let res = null; if (this.validator) diff --git a/src/agent/vision/browser_viewer.js b/src/agent/vision/browser_viewer.js index 9ae7c7b..6cce3ed 100644 --- a/src/agent/vision/browser_viewer.js +++ b/src/agent/vision/browser_viewer.js @@ -1,8 +1,8 @@ -import settings from '../../../settings.js'; +import settings from '../settings.js'; import prismarineViewer from 'prismarine-viewer'; const mineflayerViewer = prismarineViewer.mineflayer; export function addBrowserViewer(bot, count_id) { - if (settings.show_bot_views) + if (settings.render_bot_view) mineflayerViewer(bot, { port: 3000+count_id, firstPerson: true, }); } \ No newline at end of file diff --git a/src/mindcraft-py/example.py b/src/mindcraft-py/example.py new file mode 100644 index 0000000..b5775c1 --- /dev/null +++ b/src/mindcraft-py/example.py @@ -0,0 +1,27 @@ +import mindcraft +import json +import os + +# Initialize Mindcraft, starting the Node.js server +# This will also connect to the MindServer via websockets +mindcraft.init() + +# Get the directory of the current script +script_dir = os.path.dirname(os.path.abspath(__file__)) +profile_path = os.path.abspath(os.path.join(script_dir, '..', '..', 'andy.json')) + +# Load agent settings from a JSON file +try: + with open(profile_path, 'r') as f: + profile_data = json.load(f) + + settings = {"profile": profile_data} + mindcraft.create_agent(settings) + + settings_copy = settings.copy() + settings_copy['profile']['name'] = 'andy2' + mindcraft.create_agent(settings_copy) +except FileNotFoundError: + print(f"Error: Could not find andy.json at {profile_path}") + +mindcraft.wait() diff --git a/src/mindcraft-py/init-mindcraft.js b/src/mindcraft-py/init-mindcraft.js new file mode 100644 index 0000000..01a07e6 --- /dev/null +++ b/src/mindcraft-py/init-mindcraft.js @@ -0,0 +1,24 @@ +import * as Mindcraft from '../mindcraft/mindcraft.js'; +import settings from '../../settings.js'; +import yargs from 'yargs'; +import { hideBin } from 'yargs/helpers'; + +function parseArguments() { + return yargs(hideBin(process.argv)) + .option('mindserver_port', { + type: 'number', + describe: 'Mindserver port', + default: settings.mindserver_port + }) + .help() + .alias('help', 'h') + .parse(); +} + +const args = parseArguments(); + +settings.mindserver_port = args.mindserver_port; + +Mindcraft.init(settings.mindserver_port); + +console.log(`Mindcraft initialized with MindServer at localhost:${settings.mindserver_port}`); \ No newline at end of file diff --git a/src/mindcraft-py/mindcraft.py b/src/mindcraft-py/mindcraft.py new file mode 100644 index 0000000..d3c6049 --- /dev/null +++ b/src/mindcraft-py/mindcraft.py @@ -0,0 +1,99 @@ +import subprocess +import socketio +import time +import json +import os +import atexit +import threading +import sys +import signal + +class Mindcraft: + def __init__(self): + self.sio = socketio.Client() + self.process = None + self.connected = False + self.log_thread = None + + def _log_reader(self): + for line in iter(self.process.stdout.readline, ''): + sys.stdout.write(f'[Node.js] {line}') + sys.stdout.flush() + + def init(self, port=8080): + if self.process: + return + + self.port = port + + node_script_path = os.path.abspath(os.path.join(os.path.dirname(__file__), 'init-mindcraft.js')) + + self.process = subprocess.Popen([ + 'node', + node_script_path, + '--mindserver_port', str(self.port) + ], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1) + + self.log_thread = threading.Thread(target=self._log_reader) + self.log_thread.daemon = True + self.log_thread.start() + + atexit.register(self.shutdown) + time.sleep(2) # Give server time to start before connecting + + try: + self.sio.connect(f'http://localhost:{self.port}') + self.connected = True + print("Connected to MindServer. Mindcraft is initialized.") + except socketio.exceptions.ConnectionError as e: + print(f"Failed to connect to MindServer: {e}") + self.shutdown() + raise + + def create_agent(self, settings_json): + if not self.connected: + raise Exception("Not connected to MindServer. Call init() first.") + + profile_data = settings_json.get('profile', {}) + + def callback(response): + if response.get('success'): + print(f"Agent '{profile_data.get('name')}' created successfully") + else: + print(f"Error creating agent: {response.get('error', 'Unknown error')}") + + self.sio.emit('create-agent', settings_json, callback=callback) + + def shutdown(self): + if self.sio.connected: + self.sio.disconnect() + self.connected = False + if self.process: + self.process.terminate() + self.process.wait() + self.process = None + print("Mindcraft shut down.") + + def wait(self): + """Block the main thread until Ctrl+C is pressed so the server stays up,""" + print("Server is running. Press Ctrl+C to exit.") + try: + while True: + time.sleep(1) + except KeyboardInterrupt: + print("\nCtrl+C detected. Exiting...") + self.shutdown() + +mindcraft_instance = Mindcraft() + +def init(port=8080): + mindcraft_instance.init(port) + +def create_agent(settings_json): + mindcraft_instance.create_agent(settings_json) + +def shutdown(): + mindcraft_instance.shutdown() + +def wait(): + mindcraft_instance.wait() diff --git a/src/mindcraft/default_settings.json b/src/mindcraft/default_settings.json new file mode 100644 index 0000000..3ec448b --- /dev/null +++ b/src/mindcraft/default_settings.json @@ -0,0 +1,25 @@ +{ + "minecraft_version": "1.21.1", + "host": "127.0.0.1", + "port": 55916, + "auth": "offline", + "base_profile": "survival", + "load_memory": false, + "init_message": "Respond with hello world and your name", + "only_chat_with": [], + "speak": false, + "language": "en", + "allow_vision": false, + "blocked_actions" : ["!checkBlueprint", "!checkBlueprintLevel", "!getBlueprint", "!getBlueprintLevel"] , + "relevant_docs_count": 5, + "max_messages": 15, + "num_examples": 2, + "max_commands": -1, + "narrate_behavior": true, + "log_all_prompts": false, + "verbose_commands": true, + "chat_bot_messages": true, + "render_bot_view": false, + "allow_insecure_coding": false, + "code_timeout_mins": -1 +} \ No newline at end of file diff --git a/src/mindcraft/mindcraft.js b/src/mindcraft/mindcraft.js new file mode 100644 index 0000000..cd18748 --- /dev/null +++ b/src/mindcraft/mindcraft.js @@ -0,0 +1,64 @@ +import { createMindServer, registerAgent } from './mindserver.js'; +import { AgentProcess } from '../process/agent_process.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) { + if (connected) { + console.error('Already initiliazed!'); + return; + } + mindserver = createMindServer(host_public, port); + port = port; + connected = true; +} + +export async function createAgent(settings) { + if (!settings.profile.name) { + console.error('Agent name is required in profile'); + return; + } + settings = JSON.parse(JSON.stringify(settings)); + let agent_name = settings.profile.name; + registerAgent(settings); + let load_memory = settings.load_memory || false; + let init_message = settings.init_message || null; + const agentProcess = new AgentProcess(agent_name, port); + agentProcess.start(load_memory, init_message, agent_count); + agent_count++; + agent_processes[settings.profile.name] = agentProcess; +} + +export function getAgentProcess(agentName) { + return agent_processes[agentName]; +} + +export function startAgent(agentName) { + if (agent_processes[agentName]) { + agent_processes[agentName].continue(); + } + else { + console.error(`Cannot start agent ${agentName}; not found`); + } +} + +export function stopAgent(agentName) { + if (agent_processes[agentName]) { + agent_processes[agentName].stop(); + } +} + +export function shutdown() { + console.log('Shutting down'); + for (let agentName in agent_processes) { + agent_processes[agentName].stop(); + } + setTimeout(() => { + process.exit(0); + }, 2000); +} diff --git a/src/mindcraft/mindserver.js b/src/mindcraft/mindserver.js new file mode 100644 index 0000000..4449a23 --- /dev/null +++ b/src/mindcraft/mindserver.js @@ -0,0 +1,181 @@ +import { Server } from 'socket.io'; +import express from 'express'; +import http from 'http'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import * as mindcraft from './mindcraft.js'; +import { readFileSync } from 'fs'; +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +// Mindserver is: +// - central hub for communication between all agent processes +// - api to control from other languages and remote users +// - host for webapp + +let io; +let server; +const agent_connections = {}; + +const default_settings = JSON.parse(readFileSync(path.join(__dirname, 'default_settings.json'), 'utf8')); + +class AgentConnection { + constructor(settings) { + this.socket = null; + this.settings = settings; + this.in_game = false; + } + +} + +export function registerAgent(settings) { + let agentConnection = new AgentConnection(settings); + agent_connections[settings.profile.name] = agentConnection; +} + +export function logoutAgent(agentName) { + if (agent_connections[agentName]) { + agent_connections[agentName].in_game = false; + agentsUpdate(); + } +} + +// Initialize the server +export function createMindServer(host_public = false, port = 8080) { + const app = express(); + server = http.createServer(app); + io = new Server(server); + + // Serve static files + const __dirname = path.dirname(fileURLToPath(import.meta.url)); + app.use(express.static(path.join(__dirname, 'public'))); + + // Socket.io connection handling + io.on('connection', (socket) => { + let curAgentName = null; + console.log('Client connected'); + + agentsUpdate(socket); + + socket.on('create-agent', (settings, callback) => { + console.log('API create agent...'); + settings = { ...default_settings, ...settings }; + if (settings.profile?.name) { + if (settings.profile.name in agent_connections) { + callback({ success: false, error: 'Agent already exists' }); + return; + } + mindcraft.createAgent(settings); + callback({ success: true }); + } + else { + console.error('Agent name is required in profile'); + callback({ success: false, error: 'Agent name is required in profile' }); + } + }); + + socket.on('get-settings', (agentName, callback) => { + if (agent_connections[agentName]) { + callback({ settings: agent_connections[agentName].settings }); + } else { + callback({ error: `Agent '${agentName}' not found.` }); + } + }); + + socket.on('login-agent', (agentName) => { + if (agent_connections[agentName]) { + agent_connections[agentName].socket = socket; + agent_connections[agentName].in_game = true; + curAgentName = agentName; + agentsUpdate(); + } + else { + console.warn(`Unregistered agent ${agentName} tried to login`); + } + }); + + socket.on('disconnect', () => { + if (agent_connections[curAgentName]) { + console.log(`Agent ${curAgentName} disconnected`); + agent_connections[curAgentName].in_game = false; + agentsUpdate(); + } + }); + + socket.on('chat-message', (agentName, json) => { + if (!agent_connections[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}`); + agent_connections[agentName].socket.emit('chat-message', curAgentName, json); + }); + + socket.on('restart-agent', (agentName) => { + console.log(`Restarting agent: ${agentName}`); + agent_connections[agentName].socket.emit('restart-agent'); + }); + + socket.on('stop-agent', (agentName) => { + mindcraft.stopAgent(agentName); + }); + + socket.on('start-agent', (agentName) => { + mindcraft.startAgent(agentName); + }); + + socket.on('stop-all-agents', () => { + console.log('Killing all agents'); + for (let agentName in agent_connections) { + mindcraft.stopAgent(agentName); + } + }); + + socket.on('shutdown', () => { + console.log('Shutting down'); + for (let agentName in agent_connections) { + mindcraft.stopAgent(agentName); + } + // wait 2 seconds + setTimeout(() => { + console.log('Exiting MindServer'); + process.exit(0); + }, 2000); + + }); + + socket.on('send-message', (agentName, message) => { + if (!agent_connections[agentName]) { + console.warn(`Agent ${agentName} not in game, cannot send message via MindServer.`); + return + } + try { + console.log(`Sending message to agent ${agentName}: ${message}`); + agent_connections[agentName].socket.emit('send-message', agentName, message) + } catch (error) { + console.error('Error: ', error); + } + }); + }); + + let host = host_public ? '0.0.0.0' : 'localhost'; + server.listen(port, host, () => { + console.log(`MindServer running on port ${port}`); + }); + + return server; +} + +function agentsUpdate(socket) { + if (!socket) { + socket = io; + } + let agents = []; + for (let agentName in agent_connections) { + agents.push({name: agentName, in_game: agent_connections[agentName].in_game}); + }; + 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; diff --git a/src/server/public/index.html b/src/mindcraft/public/index.html similarity index 72% rename from src/server/public/index.html rename to src/mindcraft/public/index.html index c66a986..f16105c 100644 --- a/src/server/public/index.html +++ b/src/mindcraft/public/index.html @@ -64,10 +64,44 @@

Mindcraft

+
+ + +
+