improved respond timing logic, handle interruptions/self-prompting

This commit is contained in:
MaxRobinsonTheGreat 2024-11-21 23:32:25 -06:00
parent 74701ea663
commit 44c0526231
6 changed files with 168 additions and 82 deletions

View file

@ -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 = '';

View file

@ -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', () => {

View file

@ -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);
}
},
// {

View file

@ -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();
}

View file

@ -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;

View file

@ -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) {