diff --git a/src/agent/action_manager.js b/src/agent/action_manager.js index 833f3c0..bac2a04 100644 --- a/src/agent/action_manager.js +++ b/src/agent/action_manager.js @@ -46,7 +46,7 @@ export class ActionManager { assert(actionLabel != null, 'actionLabel is required for new resume'); this.resume_name = actionLabel; } - if (this.resume_func != null && this.agent.isIdle() && (!this.agent.self_prompter.on || new_resume)) { + if (this.resume_func != null && (this.agent.isIdle() || new_resume) && (!this.agent.self_prompter.on || new_resume)) { this.currentActionLabel = this.resume_name; let res = await this._executeAction(this.resume_name, this.resume_func, timeout); this.currentActionLabel = ''; diff --git a/src/agent/agent.js b/src/agent/agent.js index 1b7aa5f..02b825e 100644 --- a/src/agent/agent.js +++ b/src/agent/agent.js @@ -218,29 +218,6 @@ export class Agent { this.bot.interrupt_code = false; } - async cleanChat(to_player, message, translate_up_to=-1) { - if (isOtherAgent(to_player)) { - this.bot.chat(message); - sendToBot(to_player, message); - return; - } - - let to_translate = message; - let remaining = ''; - if (translate_up_to != -1) { - to_translate = to_translate.substring(0, translate_up_to); - remaining = message.substring(translate_up_to); - } - message = (await handleTranslation(to_translate)).trim() + " " + remaining; - // newlines are interpreted as separate chats, which triggers spam filters. replace them with spaces - message = message.replaceAll('\n', ' '); - - if (to_player === 'system' || to_player === this.name) - this.bot.chat(message); - else - this.bot.whisper(to_player, message); - } - shutUp() { this.shut_up = true; if (this.self_prompter.on) { @@ -281,7 +258,7 @@ export class Agent { } let execute_res = await executeCommand(this, message); if (execute_res) - this.cleanChat(source, execute_res); + this.routeResponse(source, execute_res); return true; } } @@ -336,14 +313,14 @@ export class Agent { this.self_prompter.handleUserPromptedCmd(self_prompt, isAction(command_name)); if (settings.verbose_commands) { - this.cleanChat(source, res, res.indexOf(command_name)); + this.routeResponse(source, res, res.indexOf(command_name)); } else { // only output command name let pre_message = res.substring(0, res.indexOf(command_name)).trim(); let chat_message = `*used ${command_name.substring(1)}*`; if (pre_message.length > 0) chat_message = `${pre_message} ${chat_message}`; - this.cleanChat(source, chat_message); + this.routeResponse(source, chat_message); } let execute_res = await executeCommand(this, res); @@ -358,7 +335,7 @@ export class Agent { } else { // conversation response this.history.add(this.name, res); - this.cleanChat(source, res); + this.routeResponse(source, res); console.log('Purely conversational response:', res); break; } @@ -369,6 +346,28 @@ export class Agent { return used_command; } + async routeResponse(to_player, message, translate_up_to=-1) { + if (isOtherAgent(to_player)) { + sendToBot(to_player, message); + return; + } + + let to_translate = message; + let remaining = ''; + if (translate_up_to != -1) { + to_translate = to_translate.substring(0, translate_up_to); + remaining = message.substring(translate_up_to); + } + message = (await handleTranslation(to_translate)).trim() + " " + remaining; + // newlines are interpreted as separate chats, which triggers spam filters. replace them with spaces + message = message.replaceAll('\n', ' '); + + if (to_player === 'system' || to_player === this.name) + this.bot.chat(message); + else + this.bot.whisper(to_player, message); + } + startEvents() { // Custom events this.bot.on('time', () => { diff --git a/src/agent/commands/actions.js b/src/agent/commands/actions.js index 81f4ab3..84b532b 100644 --- a/src/agent/commands/actions.js +++ b/src/agent/commands/actions.js @@ -1,6 +1,6 @@ import * as skills from '../library/skills.js'; import settings from '../../../settings.js'; -import { startChat, endChat } from '../conversation.js'; +import { startConversation, endConversation, inConversation, scheduleSelfPrompter, cancelSelfPrompter } from '../conversation.js'; function runAsAction (actionFn, resume = false, timeout = -1) { let actionLabel = null; // Will be set on first use @@ -350,7 +350,15 @@ export const actionsList = [ 'selfPrompt': { type: 'string', description: 'The goal prompt.' }, }, perform: async function (agent, prompt) { - agent.self_prompter.start(prompt); // don't await, don't return + if (inConversation()) { + // if conversing with another bot, dont start self-prompting yet + // wait until conversation ends + agent.self_prompter.setPrompt(prompt); + scheduleSelfPrompter(); + } + else { + agent.self_prompter.start(prompt); // don't await, don't return + } } }, { @@ -358,29 +366,29 @@ 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(); return 'Self-prompting stopped.'; } }, { - name: '!startChat', + name: '!startConversation', description: 'Send a message to a specific player to initiate conversation.', params: { 'player_name': { type: 'string', description: 'The name of the player to send the message to.' }, 'message': { type: 'string', description: 'The message to send.' }, - 'max_turns': { type: 'int', description: 'The maximum number of turns to allow in the conversation. -1 for unlimited.', domain: [-1, Number.MAX_SAFE_INTEGER] } }, - perform: async function (agent, player_name, message, max_turns) { - startChat(player_name, message, max_turns); + perform: async function (agent, player_name, message) { + startConversation(player_name, message); } }, { - name: '!endChat', + name: '!endConversation', description: 'End the conversation with the given player.', params: { 'player_name': { type: 'string', description: 'The name of the player to end the conversation with.' } }, perform: async function (agent, player_name) { - endChat(player_name); + endConversation(player_name); } }, // { diff --git a/src/agent/conversation.js b/src/agent/conversation.js index 25e45c1..78778c5 100644 --- a/src/agent/conversation.js +++ b/src/agent/conversation.js @@ -6,8 +6,7 @@ import { sendBotChatToServer } from './server_proxy.js'; let agent; let agent_names = settings.profiles.map((p) => JSON.parse(readFileSync(p, 'utf8')).name); -let inMessageTimer = null; -let MAX_TURNS = -1; +let self_prompter_paused = false; export function isOtherAgent(name) { return agent_names.some((n) => n === name); @@ -21,27 +20,56 @@ 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; - this.turn_count = 0; + 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.turn_count = 0; this.in_queue = []; + this.inMessageTimer = null; } - countTurn() { - this.turn_count++; - } - - over() { - return this.turn_count > MAX_TURNS && MAX_TURNS !== -1; + end() { + this.active = false; + this.ignore_until_start = true; } queue(message) { @@ -56,16 +84,21 @@ function _getConvo(name) { return convos[name]; } -export function startChat(send_to, message, max_turns=5) { - MAX_TURNS = max_turns; +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 (message.length > 197) - // message = message.substring(0, 197); + if (settings.chat_bot_messages) + agent.bot.chat(`(To ${send_to}) ${message}`); if (!isOtherAgent(send_to)) { agent.bot.whisper(send_to, message); return; @@ -73,30 +106,24 @@ export function sendToBot(send_to, message, start=false) { const convo = _getConvo(send_to); if (convo.ignore_until_start) return; - if (convo.over()) { - endChat(send_to); - return; - } - const end = message.includes('!endChat'); + const end = message.includes('!endConversation'); const json = { 'message': message, start, end, - 'idle': agent.isIdle() }; // agent.bot.whisper(send_to, JSON.stringify(json)); sendBotChatToServer(send_to, JSON.stringify(json)); } -export function recieveFromBot(sender, json) { +export async function recieveFromBot(sender, json) { const convo = _getConvo(sender); console.log(`decoding **${json}**`); const recieved = JSON.parse(json); if (recieved.start) { convo.reset(); - MAX_TURNS = -1; } if (convo.ignore_until_start) return; @@ -104,17 +131,58 @@ export function recieveFromBot(sender, json) { convo.queue(recieved); // responding to conversation takes priority over self prompting - if (agent.self_prompter.on) - agent.self_prompter.stopLoop(); + if (agent.self_prompter.on){ + await agent.self_prompter.stopLoop(); + self_prompter_paused = true; + } - if (inMessageTimer) - clearTimeout(inMessageTimer); - if (containsCommand(recieved.message)) - inMessageTimer = setTimeout(() => _processInMessageQueue(sender), 5000); - else - inMessageTimer = setTimeout(() => _processInMessageQueue(sender), 200); + _scheduleProcessInMessage(sender, recieved, convo); } + +/* +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 recieved during the delay will reset the delay following this logic, and be queued to respond in bulk +*/ +const talkOverActions = ['stay', 'followPlayer']; +const fastDelay = 200; +const longDelay = 5000; +async function _scheduleProcessInMessage(sender, recieved, convo) { + if (convo.inMessageTimer) + clearTimeout(convo.inMessageTimer); + let otherAgentBusy = containsCommand(recieved.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 shouldRespond = await agent.prompter.promptShouldRespondToBot(recieved.message); + console.log(`${agent.name} decision to respond: ${shouldRespond}`); + if (shouldRespond) + scheduleResponse(fastDelay); + } + else { + // neither are busy + scheduleResponse(fastDelay); + } +} + + export function _processInMessageQueue(name) { const convo = _getConvo(name); let pack = null; @@ -132,11 +200,10 @@ export function _handleFullInMessage(sender, recieved) { const convo = _getConvo(sender); - convo.countTurn(); const message = _tagMessage(recieved.message); - if (recieved.end || convo.over()) { - // if end signal from other bot, or both are busy, or past max turns, - // add to history, but don't respond + if (recieved.end) { + convo.end(); + // if end signal from other bot, add to history but don't respond agent.history.add(sender, message); return; } @@ -145,18 +212,13 @@ export function _handleFullInMessage(sender, recieved) { agent.handleMessage(sender, message); } -export function endChat(sender) { - if (convos[sender]) { - convos[sender].ignore_until_start = true; - } -} - -export function endAllChats() { - for (const sender in convos) { - convos[sender].ignore_until_start = true; - } -} function _tagMessage(message) { return "(FROM OTHER BOT)" + message; } + +async function _resumeSelfPrompter() { + await new Promise(resolve => setTimeout(resolve, 5000)); + self_prompter_paused = false; + agent.self_prompter.start(); +} diff --git a/src/agent/modes.js b/src/agent/modes.js index ee10390..9cd6687 100644 --- a/src/agent/modes.js +++ b/src/agent/modes.js @@ -106,6 +106,7 @@ const modes_list = [ const crashTimeout = setTimeout(() => { agent.cleanKill("Got stuck and couldn't get unstuck") }, 10000); await skills.moveAway(bot, 5); clearTimeout(crashTimeout); + say(agent, 'I\'m free.'); }); } this.last_time = Date.now(); @@ -280,12 +281,20 @@ const modes_list = [ async function execute(mode, agent, func, timeout=-1) { if (agent.self_prompter.on) agent.self_prompter.stopLoop(); + let interrupted_action = agent.actions.currentActionLabel; mode.active = true; let code_return = await agent.actions.runAction(`mode:${mode.name}`, async () => { await func(); }, { timeout }); mode.active = false; console.log(`Mode ${mode.name} finished executing, code_return: ${code_return.message}`); + if (interrupted_action && !agent.actions.resume_func && !agent.self_prompter.on) { + // auto prompt to respond to the interruption + let role = agent.last_sender ? 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.`); + } } let _agent = null; diff --git a/src/agent/self_prompter.js b/src/agent/self_prompter.js index 8c928e1..e79837e 100644 --- a/src/agent/self_prompter.js +++ b/src/agent/self_prompter.js @@ -12,7 +12,9 @@ export class SelfPrompter { start(prompt) { console.log('Self-prompting started.'); if (!prompt) { - return 'No prompt specified. Ignoring request.'; + if (!this.prompt) + return 'No prompt specified. Ignoring request.'; + prompt = this.prompt; } if (this.on) { this.prompt = prompt; @@ -22,6 +24,10 @@ export class SelfPrompter { this.startLoop(); } + setPrompt(prompt) { + this.prompt = prompt; + } + async startLoop() { if (this.loop_active) { console.warn('Self-prompt loop is already active. Ignoring request.'); @@ -76,6 +82,8 @@ export class SelfPrompter { async stopLoop() { // you can call this without await if you don't need to wait for it to finish + if (this.interrupt) + return; console.log('stopping self-prompt loop') this.interrupt = true; while (this.loop_active) {