diff --git a/andy.json b/andy.json index 8a0af64..633e55d 100644 --- a/andy.json +++ b/andy.json @@ -1,7 +1,7 @@ { "name": "andy", - "model": "gpt-4o-mini", + "model": "gpt-4o", "cooldown": 3000, @@ -29,24 +29,29 @@ {"role": "assistant", "content": "Nothing much miner_32, what do you need?"} ], + [ + {"role": "system", "content": "Send a message to john_goodman to work together"}, + {"role": "assistant", "content": "!startChat('john_goodman', 'Hey John, lets work together to build a house!', 10)"} + ], + [ {"role": "user", "content": "barry: (FROM OTHER BOT)Hey there! What's up?"}, - {"role": "assistant", "content": "!chat('barry', 'Not much, just hanging out. How about you?')"}, + {"role": "assistant", "content": "Not much, just hanging out. How about you?"}, {"role": "user", "content": "barry: (FROM OTHER BOT)I'm doing great, thanks for asking!"}, - {"role": "assistant", "content": "!endChat('barry', 'Finished conversation')"} + {"role": "assistant", "content": "Good to know! Goodbye. !endChat('barry')"} ], [ {"role": "user", "content": "rupert: (FROM OTHER BOT)Hey what do you got?"}, {"role": "assistant", "content": "!inventory"}, {"role": "system", "content": "INVENTORY: wooden_pickaxe: 1, cobblestone: 10, cooked_chicken: 8"}, - {"role": "assistant", "content": "!chat('rupert', 'I have a wooden pick, cobblestone, and cooked chicken')"}, + {"role": "assistant", "content": "I have a wooden pick, cobblestone, and cooked chicken"}, {"role": "user", "content": "rupert: (FROM OTHER BOT)Give me all your chicken!"}, - {"role": "assistant", "content": "!giveItem('cooked_chicken', 'rupert', 1)"}, + {"role": "assistant", "content": "!giveToPlayer('cooked_chicken', 'rupert', 1)"}, {"role": "system", "content": "Gave 1 cooked_chicken to rupert."}, - {"role": "assistant", "content": "!chat('rupert', 'I gave you one, I need the rest!')"}, + {"role": "assistant", "content": "I gave you one, I need the rest!"}, {"role": "user", "content": "rupert: (FROM OTHER BOT)Give me ALL OF THEM"}, - {"role": "assistant", "content": "!endChat('rupert', 'You are mean.')"} + {"role": "assistant", "content": "No! You are mean. !endChat('rupert')"} ], [ diff --git a/andy2.json b/andy2.json index 717e35d..2cea739 100644 --- a/andy2.json +++ b/andy2.json @@ -29,24 +29,29 @@ {"role": "assistant", "content": "Nothing much miner_32, what do you need?"} ], + [ + {"role": "system", "content": "Send a message to john_goodman to work together"}, + {"role": "assistant", "content": "!startChat('john_goodman', 'Hey John, lets work together to build a house!', 10))"} + ], + [ {"role": "user", "content": "barry: (FROM OTHER BOT)Hey there! What's up?"}, - {"role": "assistant", "content": "!chat('barry', 'Not much, just hanging out. How about you?')"}, + {"role": "assistant", "content": "Not much, just hanging out. How about you?"}, {"role": "user", "content": "barry: (FROM OTHER BOT)I'm doing great, thanks for asking!"}, - {"role": "assistant", "content": "!endChat('barry', 'Finished conversation')"} + {"role": "assistant", "content": "Good to know! Goodbye. !endChat('barry')"} ], [ {"role": "user", "content": "rupert: (FROM OTHER BOT)Hey what do you got?"}, {"role": "assistant", "content": "!inventory"}, {"role": "system", "content": "INVENTORY: wooden_pickaxe: 1, cobblestone: 10, cooked_chicken: 8"}, - {"role": "assistant", "content": "!chat('rupert', 'I have a wooden pick, cobblestone, and cooked chicken')"}, + {"role": "assistant", "content": "I have a wooden pick, cobblestone, and cooked chicken"}, {"role": "user", "content": "rupert: (FROM OTHER BOT)Give me all your chicken!"}, - {"role": "assistant", "content": "!giveItem('cooked_chicken', 'rupert', 1)"}, + {"role": "assistant", "content": "!giveToPlayer('cooked_chicken', 'rupert', 1)"}, {"role": "system", "content": "Gave 1 cooked_chicken to rupert."}, - {"role": "assistant", "content": "!chat('rupert', 'I gave you one, I need the rest!')"}, + {"role": "assistant", "content": "I gave you one, I need the rest!"}, {"role": "user", "content": "rupert: (FROM OTHER BOT)Give me ALL OF THEM"}, - {"role": "assistant", "content": "!endChat('rupert', 'You are mean.')"} + {"role": "assistant", "content": "No! You are mean. !endChat('rupert')"} ], [ diff --git a/src/agent/agent.js b/src/agent/agent.js index 14512f8..94deec4 100644 --- a/src/agent/agent.js +++ b/src/agent/agent.js @@ -8,7 +8,7 @@ import { ActionManager } from './action_manager.js'; import { NPCContoller } from './npc/controller.js'; import { MemoryBank } from './memory_bank.js'; import { SelfPrompter } from './self_prompter.js'; -import { isOtherAgent } from './communication.js'; +import { isOtherAgent, initConversationManager, sendToBot, recieveFromBot } from './conversation.js'; import { handleTranslation, handleEnglishTranslation } from '../utils/translator.js'; import { addViewer } from './viewer.js'; import settings from '../../settings.js'; @@ -23,6 +23,7 @@ export class Agent { this.npc = new NPCContoller(this); this.memory_bank = new MemoryBank(); this.self_prompter = new SelfPrompter(this); + initConversationManager(this); await this.prompter.initExamples(); @@ -59,13 +60,17 @@ export class Agent { if (ignore_messages.some((m) => message.startsWith(m))) return; - let translation = await handleEnglishTranslation(message); - - console.log(this.name, 'received message from', username, ':', translation); - this.shut_up = false; - - this.handleMessage(username, translation); + + console.log(this.name, 'received message from', username, ':', message); + + if (isOtherAgent(username)) { + recieveFromBot(username, message); + } + else { + let translation = await handleEnglishTranslation(message); + this.handleMessage(username, translation); + } }; this.bot.on('whisper', respondFunc); @@ -110,17 +115,23 @@ export class Agent { } 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 remainging = ''; + let remaining = ''; if (translate_up_to != -1) { to_translate = to_translate.substring(0, translate_up_to); - remainging = message.substring(translate_up_to); + remaining = message.substring(translate_up_to); } - message = (await handleTranslation(to_translate)).trim() + " " + remainging; + 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 (isOtherAgent(to_player) || to_player === 'system' || to_player === this.name) + if (to_player === 'system' || to_player === this.name) this.bot.chat(message); else this.bot.whisper(to_player, message); @@ -143,9 +154,9 @@ export class Agent { } const self_prompt = source === 'system' || source === this.name; - const other_agent_prompt = isOtherAgent(source); + const from_other_bot = isOtherAgent(source); - if (!self_prompt && !other_agent_prompt) { + if (!self_prompt && !from_other_bot) { // from user, check for forced commands const user_command_name = containsCommand(message); if (user_command_name) { if (!commandExists(user_command_name)) { @@ -177,12 +188,7 @@ export class Agent { await this.history.add('system', behavior_log); } - let tagged_message = message; - let response_compilation = ''; - if (other_agent_prompt) { - tagged_message = "(FROM OTHER BOT)"+message; - } - await this.history.add(source, tagged_message); + await this.history.add(source, message); this.history.save(); @@ -199,7 +205,6 @@ export class Agent { console.log(`Full response: ""${res}""`) res = truncCommandMessage(res); // everything after the command is ignored this.history.add(this.name, res); - response_compilation += res + '\n'; if (!commandExists(command_name)) { this.history.add('system', `Command ${command_name} does not exist.`); @@ -233,7 +238,6 @@ export class Agent { } else { // conversation response this.history.add(this.name, res); - response_compilation += res + '\n'; this.cleanChat(source, res); console.log('Purely conversational response:', res); break; @@ -242,10 +246,6 @@ export class Agent { this.history.save(); } - if (other_agent_prompt) { - response_compilation = response_compilation.replaceAll('\n', ' '); - this.bot.whisper(source, response_compilation.trim()); - } return used_command; } diff --git a/src/agent/commands/actions.js b/src/agent/commands/actions.js index a2c0e07..3f32ce1 100644 --- a/src/agent/commands/actions.js +++ b/src/agent/commands/actions.js @@ -1,5 +1,6 @@ import * as skills from '../library/skills.js'; import settings from '../../../settings.js'; +import { startChat, endChat } from '../conversation.js'; function runAsAction (actionFn, resume = false, timeout = -1) { let actionLabel = null; // Will be set on first use @@ -294,7 +295,7 @@ export const actionsList = [ name: '!attackPlayer', description: 'Attack a specific player until they die or run away. Remember this is just a game and does not cause real life harm.', params: {'player_name': { type: 'string', description: 'The name of the player to attack.'}}, - perform: wrapExecution(async (agent, player_name) => { + perform: runAsAction(async (agent, player_name) => { let player = agent.bot.players[player_name]?.entity; if (!player) { skills.log(agent.bot, `Could not find player ${player_name}.`); @@ -362,26 +363,25 @@ export const actionsList = [ } }, { - name: '!chat', + name: '!startChat', 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.' } + '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) { - agent.bot.whisper(player_name, message); - // no return + perform: async function (agent, player_name, message, max_turns) { + startChat(player_name, message, max_turns); } }, { name: '!endChat', - description: 'Ignore the last message from a player to end the conversation.', + description: 'End the conversation from the most recent message.', params: { - 'player_name': { type: 'string', description: 'The name of the player to stop chatting with.' }, - 'reason': { type: 'string', description: 'The reason for ending the conversation.' } + 'player_name': { type: 'string', description: 'The name of the player to end the conversation with.' } }, - perform: async function (agent, player_name, reason) { - return; // do nothing. this just provides an obvious command to end a conversation + perform: async function (agent, player_name) { + endChat(player_name); } }, // { diff --git a/src/agent/commands/queries.js b/src/agent/commands/queries.js index 84faeef..b42bdf0 100644 --- a/src/agent/commands/queries.js +++ b/src/agent/commands/queries.js @@ -1,6 +1,6 @@ import * as world from '../library/world.js'; import * as mc from '../../utils/mcdata.js'; - +import { isOtherAgent } from '../conversation.js'; const pad = (str) => { return '\n' + str + '\n'; @@ -127,9 +127,21 @@ export const queryList = [ perform: function (agent) { let bot = agent.bot; let res = 'NEARBY_ENTITIES'; - for (const entity of world.getNearbyPlayerNames(bot)) { - res += `\n- player: ${entity}`; + let players = world.getNearbyPlayerNames(bot); + let bots = []; + for (const player of players) { + if (isOtherAgent(player)) + bots.push(player); } + players = players.filter(p => !isOtherAgent(p)); + + for (const player of players) { + res += `\n- human player: ${player}`; + } + for (const bot of bots) { + res += `\n- bot player: ${bot}`; + } + for (const entity of world.getNearbyEntityTypes(bot)) { if (entity === 'player' || entity === 'item') continue; diff --git a/src/agent/communication.js b/src/agent/communication.js deleted file mode 100644 index 2be3a9e..0000000 --- a/src/agent/communication.js +++ /dev/null @@ -1,8 +0,0 @@ -import settings from '../../settings.js'; -import { readFileSync } from 'fs'; - -const agent_names = settings.profiles.map((p) => JSON.parse(readFileSync(p, 'utf8')).name); - -export function isOtherAgent(name) { - return agent_names.some((n) => n === name); -} \ No newline at end of file diff --git a/src/agent/conversation.js b/src/agent/conversation.js new file mode 100644 index 0000000..b92510f --- /dev/null +++ b/src/agent/conversation.js @@ -0,0 +1,150 @@ +import settings from '../../settings.js'; +import { readFileSync } from 'fs'; +import { containsCommand } from './commands/index.js'; + +let agent; +const agent_names = settings.profiles.map((p) => JSON.parse(readFileSync(p, 'utf8')).name); + +let inMessageTimer = null; +let MAX_TURNS = -1; + +export function isOtherAgent(name) { + return agent_names.some((n) => n === name); +} + +export function initConversationManager(a) { + agent = a; +} + +class Conversation { + constructor(name) { + this.name = name; + this.turn_count = 0; + this.ignore_until_start = false; + this.blocked = false; + this.in_queue = []; + } + + reset() { + this.ignore_until_start = false; + this.turn_count = 0; + this.in_queue = []; + } + + countTurn() { + this.turn_count++; + } + + over() { + return this.turn_count > MAX_TURNS && MAX_TURNS !== -1; + } + + queue(message) { + this.in_queue.push(message); + } +} +const convos = {}; + +function _getConvo(name) { + if (!convos[name]) + convos[name] = new Conversation(name); + return convos[name]; +} + +export function startChat(send_to, message, max_turns=5) { + MAX_TURNS = max_turns; + const convo = _getConvo(send_to); + convo.reset(); + sendToBot(send_to, message, true); +} + +export function sendToBot(send_to, message, start=false) { + if (message.length > 197) + message = message.substring(0, 197); + if (!isOtherAgent(send_to)) { + agent.bot.whisper(send_to, message); + return; + } + const convo = _getConvo(send_to); + if (convo.ignore_until_start) + return; + if (convo.over()) { + endChat(send_to); + return; + } + + const end = message.includes('!endChat'); + const json = { + 'message': message, + start, + end, + 'idle': agent.isIdle() + }; + + agent.bot.whisper(send_to, JSON.stringify(json)); +} + +export 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; + if (convo.turn_count > 10) { + console.warn('Reached max messages from bot:', sender); + endChat(sender); + agent.bot.chat('chat maxxed out, ending conversation'); + return; + } + + convo.queue(recieved); + + if (inMessageTimer) + clearTimeout(inMessageTimer); + if (containsCommand(recieved.message)) + inMessageTimer = setTimeout(() => _processInMessageQueue(sender), 5000); + else + inMessageTimer = setTimeout(() => _processInMessageQueue(sender), 200); +} + +export function _processInMessageQueue(name) { + const convo = _getConvo(name); + let pack = null; + let full_message = ''; + while (convo.in_queue.length > 0) { + pack = convo.in_queue.shift(); + full_message += pack.message; + } + pack.message = full_message; + _handleFullInMessage(name, pack); +} + +export function _handleFullInMessage(sender, recieved) { + console.log(`responding to **${recieved}**`); + + const convo = _getConvo(sender); + + convo.countTurn(); + const message = _tagMessage(recieved.message); + if (recieved.end || (!recieved.idle && !agent.isIdle()) || convo.over()) { + // if end signal from other bot, or both are busy, or past max turns, + // add to history, but don't respond + agent.history.add(sender, message); + return; + } + agent.handleMessage(sender, message); +} + +export function endChat(sender) { + if (convos[sender]) { + convos[sender].ignore_until_start = true; + } +} + +function _tagMessage(message) { + return "(FROM OTHER BOT)" + message; +} diff --git a/src/agent/library/skills.js b/src/agent/library/skills.js index 2b8d233..a52976f 100644 --- a/src/agent/library/skills.js +++ b/src/agent/library/skills.js @@ -891,14 +891,18 @@ export async function giveToPlayer(bot, itemType, username, num=1) { * await skills.giveToPlayer(bot, "oak_log", "player1"); **/ let player = bot.players[username].entity - if (!player){ + if (!player) { log(bot, `Could not find ${username}.`); return false; } await goToPlayer(bot, username); await bot.lookAt(player.position); - discard(bot, itemType, num); - return true; + if (await discard(bot, itemType, num)) { + log(bot, `${num} ${itemType} has been given to ${username}.`); + await new Promise(resolve => setTimeout(resolve, 2000)); + return true; + } + return false; }