mirror of
https://github.com/kolbytn/mindcraft.git
synced 2025-09-08 02:52:59 +02:00
improved respond timing logic, handle interruptions/self-prompting
This commit is contained in:
parent
74701ea663
commit
44c0526231
6 changed files with 168 additions and 82 deletions
|
@ -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 = '';
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
// {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Add table
Reference in a new issue