import settings from '../../settings.js'; import { readFileSync } from 'fs'; import { containsCommand } from './commands/index.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 = []; class Conversation { constructor(name) { this.name = name; this.active = false; this.ignore_until_start = false; this.blocked = false; this.in_queue = []; this.inMessageTimer = null; } reset() { this.active = false; this.ignore_until_start = false; this.in_queue = []; this.inMessageTimer = null; } 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 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; } 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 (!agent.self_prompter.isPaused()) { 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.isActive()) { await agent.self_prompter.pause(); } if (convo.active) return; convo.active = true; this.activeConversation = convo; this._startMonitor(); this.sendToBot(send_to, message, true, false); } startConversationFromOtherBot(name) { const convo = this._getConvo(name); convo.active = true; this.activeConversation = convo; this._startMonitor(); } sendToBot(send_to, message, start=false, open_chat=true) { if (!this.isOtherAgent(send_to)) { console.warn(`${agent.name} tried to send bot message to non-bot ${send_to}`); return; } const convo = this._getConvo(send_to); if (settings.chat_bot_messages && open_chat) 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 receiveFromBot(sender, received) { const convo = this._getConvo(sender); if (convo.ignore_until_start && !received.start) return; // check if any convo is active besides the sender if (this.inConversation() && !this.inConversation(sender)) { this.sendToBot(sender, `I'm talking to someone else, try again later. !endConversation("${sender}")`, false, false); this.endConversation(sender); return; } if (received.start) { convo.reset(); this.startConversationFromOtherBot(sender); } this._clearMonitorTimeouts(); convo.queue(received); // responding to conversation takes priority over self prompting if (agent.self_prompter.isActive()){ await agent.self_prompter.pause(); } _scheduleProcessInMessage(sender, received, 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(); if (this.activeConversation.name === sender) { this._stopMonitor(); this.activeConversation = null; if (agent.self_prompter.isPaused() && !this.inConversation()) { _resumeSelfPrompter(); } } } } endAllConversations() { for (const sender in this.convos) { this.endConversation(sender); } if (agent.self_prompter.isPaused()) { _resumeSelfPrompter(); } } forceEndCurrentConversation() { if (this.activeConversation) { let sender = this.activeConversation.name; this.sendToBot(sender, '!endConversation("' + sender + '")', false, false); this.endConversation(sender); } } } const convoManager = new ConversationManager(); export default convoManager; /* This function controls conversation flow by deciding when the bot responds. The logic is as follows: - If neither bot is busy, respond quickly with a small delay. - If only the other bot is busy, respond with a long delay to allow it to finish short actions (ex check inventory) - If I'm busy but other bot isn't, let LLM decide whether to respond - If both bots are busy, don't respond until someone is done, excluding a few actions that allow fast responses - New messages received during the delay will reset the delay following this logic, and be queued to respond in bulk */ const talkOverActions = ['stay', 'followPlayer', 'mode:']; // all mode actions const fastDelay = 200; const longDelay = 5000; async function _scheduleProcessInMessage(sender, received, convo) { if (convo.inMessageTimer) clearTimeout(convo.inMessageTimer); let otherAgentBusy = containsCommand(received.message); const scheduleResponse = (delay) => convo.inMessageTimer = setTimeout(() => _processInMessageQueue(sender), delay); if (!agent.isIdle() && otherAgentBusy) { // both are busy let canTalkOver = talkOverActions.some(a => agent.actions.currentActionLabel.includes(a)); if (canTalkOver) scheduleResponse(fastDelay) // otherwise don't respond } else if (otherAgentBusy) // other bot is busy but I'm not scheduleResponse(longDelay); else if (!agent.isIdle()) { // I'm busy but other bot isn't let canTalkOver = talkOverActions.some(a => agent.actions.currentActionLabel.includes(a)); if (canTalkOver) { scheduleResponse(fastDelay); } else { let shouldRespond = await agent.prompter.promptShouldRespondToBot(received.message); console.log(`${agent.name} decided to ${shouldRespond?'respond':'not respond'} to ${sender}`); if (shouldRespond) scheduleResponse(fastDelay); } } else { // neither are busy scheduleResponse(fastDelay); } } function _processInMessageQueue(name) { const convo = convoManager._getConvo(name); _handleFullInMessage(name, _compileInMessages(convo)); } function _compileInMessages(convo) { let pack = {}; let full_message = ''; while (convo.in_queue.length > 0) { pack = convo.in_queue.shift(); full_message += pack.message; } pack.message = full_message; return pack; } function _handleFullInMessage(sender, received) { console.log(`${agent.name} responding to "${received.message}" from ${sender}`); const convo = convoManager._getConvo(sender); convo.active = true; let message = _tagMessage(received.message); if (received.end) { convoManager.endConversation(sender); message = `Conversation with ${sender} ended with message: "${message}"`; sender = 'system'; // bot will respond to system instead of the other bot } else if (received.start) agent.shut_up = false; convo.inMessageTimer = null; agent.handleMessage(sender, message); } function _tagMessage(message) { return "(FROM OTHER BOT)" + message; } async function _resumeSelfPrompter() { await new Promise(resolve => setTimeout(resolve, 5000)); if (agent.self_prompter.isPaused() && !convoManager.inConversation()) { agent.self_prompter.start(); } }