From 337117fccb7aaf84a6360e1d9edda9c814ad0218 Mon Sep 17 00:00:00 2001 From: RainbowBird Date: Mon, 6 Jan 2025 18:32:42 +0800 Subject: [PATCH] feat: command --- services/minecraft/src/agents/openai.test.ts | 10 + services/minecraft/src/agents/openai.ts | 74 ++++++ services/minecraft/src/agents/queries.ts | 211 ++++++++++++++++++ services/minecraft/src/components/command.ts | 46 ++++ services/minecraft/src/components/echo.ts | 6 +- services/minecraft/src/components/follow.ts | 86 +++---- .../{patchfinder.ts => pathfinder.ts} | 28 +-- services/minecraft/src/components/status.ts | 34 +++ .../minecraft/src/{ => composables}/bot.ts | 31 ++- services/minecraft/src/composables/command.ts | 10 + .../minecraft/src/{ => composables}/config.ts | 0 services/minecraft/src/main.ts | 25 ++- services/minecraft/src/middlewares/chat.ts | 8 +- services/minecraft/src/middlewares/command.ts | 13 ++ services/minecraft/src/prompts/agent.ts | 74 ++++++ services/minecraft/src/utils/mcdata.ts | 2 +- 16 files changed, 580 insertions(+), 78 deletions(-) create mode 100644 services/minecraft/src/agents/openai.test.ts create mode 100644 services/minecraft/src/agents/openai.ts create mode 100644 services/minecraft/src/agents/queries.ts create mode 100644 services/minecraft/src/components/command.ts rename services/minecraft/src/components/{patchfinder.ts => pathfinder.ts} (50%) create mode 100644 services/minecraft/src/components/status.ts rename services/minecraft/src/{ => composables}/bot.ts (73%) create mode 100644 services/minecraft/src/composables/command.ts rename services/minecraft/src/{ => composables}/config.ts (100%) create mode 100644 services/minecraft/src/middlewares/command.ts create mode 100644 services/minecraft/src/prompts/agent.ts diff --git a/services/minecraft/src/agents/openai.test.ts b/services/minecraft/src/agents/openai.test.ts new file mode 100644 index 000000000..92271ebb1 --- /dev/null +++ b/services/minecraft/src/agents/openai.test.ts @@ -0,0 +1,10 @@ +import { describe, expect, it } from 'vitest' + +import { initQueryAgent } from './openai' + +describe('openAI agent', () => { + it('should initialize the agent', () => { + const agent = initQueryAgent() + expect(agent).toBeDefined() + }) +}) diff --git a/services/minecraft/src/agents/openai.ts b/services/minecraft/src/agents/openai.ts new file mode 100644 index 000000000..6db92d857 --- /dev/null +++ b/services/minecraft/src/agents/openai.ts @@ -0,0 +1,74 @@ +import type { BotContext } from 'src/bot' +import { useLogg } from '@guiiai/logg' +import { tool } from 'xsai' + +import { createQueryAgentBotContext, queryList } from './queries' + +// Types +interface AgentBotContext { + readonly agents: Set> +} + +// State management +const agents = new Set>() + +const logger = useLogg('openai').useGlobalConfig() + +// Agent initialization +// export async function initAgent(): Promise { +// logger.log('Initializing agent') +// let n = neuri() + +// agents.add(initQueryAgent()) + +// agents.forEach(agent => n = n.agent(agent)) + +// return n.build({ +// provider: { +// apiKey: openaiConfig.apiKey, +// baseURL: openaiConfig.baseUrl, +// }, +// }) +// } + +// export async function initQueryAgent(): Promise { +// logger.log('Initializing query agent') +// let queryAgent = agent('query') + +// queryList.forEach((query) => { +// queryAgent = queryAgent.tool( +// query.name, +// query.schema, +// query.perform, +// { description: query.description }, +// ) +// }) + +// return queryAgent.build() +// } + +export async function initAgent(ctx: BotContext) { + logger.log('Initializing agent') + + initQueryAgent(ctx) +} + +export function initQueryAgent(ctx: BotContext) { + logger.log('Initializing query agent') + const agentBotContext = createQueryAgentBotContext(ctx.bot) + + const tools = [] + + for (const query of queryList) { + tools.push( + tool({ + name: query.name, + description: query.description, + execute: query.perform(agentBotContext), + parameters: query.schema as never, + }), + ) + } + + return tools +} diff --git a/services/minecraft/src/agents/queries.ts b/services/minecraft/src/agents/queries.ts new file mode 100644 index 000000000..26db8c7a3 --- /dev/null +++ b/services/minecraft/src/agents/queries.ts @@ -0,0 +1,211 @@ +import type { Bot } from 'mineflayer' +import { z } from 'zod' + +// Core types +type QueryResult = string | Promise + +// BotContext management +let ctx: QueryBotContext + +export function initQueryBotContext(BotContext: QueryBotContext): void { + ctx = BotContext +} + +interface QueryBotContext { + world: { + getBiomeName: (bot: Bot) => string + getNearbyPlayerNames: (bot: Bot) => string[] + getInventoryCounts: (bot: Bot) => Record + getNearbyBlockTypes: (bot: Bot) => string[] + getCraftableItems: (bot: Bot) => string[] + getNearbyEntityTypes: (bot: Bot) => string[] + } + convoManager: { + getInGameAgents: () => string[] + } +} + +interface QueryAgentBotContext { + bot: Bot + name: string + actions: { + currentActionLabel: string + } + isIdle: () => boolean + memory_bank: { + getKeys: () => string[] + } +} + +export function createQueryAgentBotContext(bot: Bot): QueryAgentBotContext { + return { + bot, + name: bot.username, + actions: { + currentActionLabel: bot.actions.currentActionLabel, + }, + isIdle: () => bot.actions.isIdle(), + memory_bank: { + getKeys: () => bot.memory_bank.getKeys(), + }, + } +} + +interface Query { + readonly name: string + readonly description: string + readonly schema: z.ZodObject + readonly perform: (agent: QueryAgentBotContext) => () => QueryResult +} + +// Utils +const pad = (str: string): string => `\n${str}\n` + +function formatInventoryItem(item: string, count: number): string { + return count > 0 ? `\n- ${item}: ${count}` : '' +} + +function formatWearingItem(slot: string, item: string | undefined): string { + return item ? `\n${slot}: ${item}` : '' +} + +// Query implementations +function createStatsQuery(): Query { + return { + name: '!stats', + description: 'Get your bot\'s location, health, hunger, and time of day.', + schema: z.object({}), + perform: (agent: QueryAgentBotContext) => (): string => { + const { bot } = agent + const pos = bot.entity.position + const weather = bot.rainState > 0 ? 'Rain' : bot.thunderState > 0 ? 'Thunderstorm' : 'Clear' + const timeOfDay = bot.time.timeOfDay < 6000 + ? 'Morning' + : bot.time.timeOfDay < 12000 ? 'Afternoon' : 'Night' + const action = agent.isIdle() ? 'Idle' : agent.actions.currentActionLabel + + const players = ctx.world.getNearbyPlayerNames(bot) + .filter(p => !ctx.convoManager.getInGameAgents().includes(p)) + const bots = ctx.convoManager.getInGameAgents() + .filter(b => b !== agent.name) + + return pad(`STATS +- Position: x: ${pos.x.toFixed(2)}, y: ${pos.y.toFixed(2)}, z: ${pos.z.toFixed(2)} +- Gamemode: ${bot.game.gameMode} +- Health: ${Math.round(bot.health)} / 20 +- Hunger: ${Math.round(bot.food)} / 20 +- Biome: ${ctx.world.getBiomeName(bot)} +- Weather: ${weather} +- Time: ${timeOfDay} +- Current Action: ${action} +- Nearby Human Players: ${players.length > 0 ? players.join(', ') : 'None.'} +- Nearby Bot Players: ${bots.length > 0 ? bots.join(', ') : 'None.'} +${bot.modes.getMiniDocs()}`) + }, + } +} + +function createInventoryQuery(): Query { + return { + name: '!inventory', + description: 'Get your bot\'s inventory.', + schema: z.object({}), + perform: (agent: QueryAgentBotContext) => (): string => { + const { bot } = agent + const inventory = ctx.world.getInventoryCounts(bot) + const items = Object.entries(inventory) + .map(([item, count]) => formatInventoryItem(item, count)) + .join('') + + const wearing = [ + formatWearingItem('Head', bot.inventory.slots[5]?.name), + formatWearingItem('Torso', bot.inventory.slots[6]?.name), + formatWearingItem('Legs', bot.inventory.slots[7]?.name), + formatWearingItem('Feet', bot.inventory.slots[8]?.name), + ].filter(Boolean).join('') + + return pad(`INVENTORY${items || ': Nothing'} +${agent.bot.game.gameMode === 'creative' ? '\n(You have infinite items in creative mode. You do not need to gather resources!!)' : ''} +WEARING: ${wearing || 'Nothing'}`) + }, + } +} + +function createNearbyBlocksQuery(): Query { + return { + name: '!nearbyBlocks', + description: 'Get the blocks near the bot.', + schema: z.object({}), + perform: (agent: QueryAgentBotContext) => (): string => { + const blocks = ctx.world.getNearbyBlockTypes(agent.bot) + return pad(`NEARBY_BLOCKS${blocks.map(b => `\n- ${b}`).join('') || ': none'}`) + }, + } +} + +function createCraftableQuery(): Query { + return { + name: '!craftable', + description: 'Get the craftable items with the bot\'s inventory.', + schema: z.object({}), + perform: (agent: QueryAgentBotContext) => (): string => { + const craftable = ctx.world.getCraftableItems(agent.bot) + return pad(`CRAFTABLE_ITEMS${craftable.map(i => `\n- ${i}`).join('') || ': none'}`) + }, + } +} + +function createEntitiesQuery(): Query { + return { + name: '!entities', + description: 'Get the nearby players and entities.', + schema: z.object({}), + perform: (agent: QueryAgentBotContext) => (): string => { + const { bot } = agent + const players = ctx.world.getNearbyPlayerNames(bot) + .filter(p => !ctx.convoManager.getInGameAgents().includes(p)) + const bots = ctx.convoManager.getInGameAgents() + .filter(b => b !== agent.name) + const entities = ctx.world.getNearbyEntityTypes(bot) + .filter(e => e !== 'player' && e !== 'item') + + const result = [ + ...players.map(p => `- Human player: ${p}`), + ...bots.map(b => `- Bot player: ${b}`), + ...entities.map(e => `- entities: ${e}`), + ] + + return pad(`NEARBY_ENTITIES${result.length ? `\n${result.join('\n')}` : ': none'}`) + }, + } +} + +function createModesQuery(): Query { + return { + name: '!modes', + description: 'Get all available modes and their docs and see which are on/off.', + schema: z.object({}), + perform: (agent: QueryAgentBotContext) => (): string => agent.bot.modes.getDocs(), + } +} + +function createSavedPlacesQuery(): Query { + return { + name: '!savedPlaces', + description: 'List all saved locations.', + schema: z.object({}), + perform: (agent: QueryAgentBotContext) => (): string => + `Saved place names: ${agent.memory_bank.getKeys()}`, + } +} + +// Export query list +export const queryList: readonly Query[] = [ + createStatsQuery(), + createInventoryQuery(), + createNearbyBlocksQuery(), + createCraftableQuery(), + createEntitiesQuery(), + createModesQuery(), + createSavedPlacesQuery(), +] as const diff --git a/services/minecraft/src/components/command.ts b/services/minecraft/src/components/command.ts new file mode 100644 index 000000000..782d04c15 --- /dev/null +++ b/services/minecraft/src/components/command.ts @@ -0,0 +1,46 @@ +import type { BotContext, ComponentLifecycle } from '@/composables/bot' +import { commands } from '@/composables/command' +import { formBotChat } from '@/middlewares/chat' +import { parseCommand } from '@/middlewares/command' +import { useLogg } from '@guiiai/logg' + +const logger = useLogg('command').useGlobalConfig() + +export function createCommandComponent(ctx: BotContext): ComponentLifecycle { + const onChat = formBotChat(ctx, (sender, message) => { + const { isCommand, command, args } = parseCommand(sender, message) + + if (!isCommand) + return + + // Remove the # prefix from command + const cleanCommand = command.slice(1) + + logger.withFields({ sender, command: cleanCommand, args }).log('Command received') + + const handler = commands.get(cleanCommand) + if (handler) { + handler({ sender, isCommand, command: cleanCommand, args }) + return + } + + // Built-in commands + switch (cleanCommand) { + case 'help': { + const commandList = Array.from(commands.keys()).concat(['help']) + ctx.bot.chat(`Available commands: ${commandList.map(cmd => `#${cmd}`).join(', ')}`) + break + } + default: + ctx.bot.chat(`Unknown command: ${cleanCommand}`) + } + }) + + ctx.bot.on('chat', onChat) + + return { + cleanup: () => { + ctx.bot.removeListener('chat', onChat) + }, + } +} diff --git a/services/minecraft/src/components/echo.ts b/services/minecraft/src/components/echo.ts index 191004a5a..2476fd26f 100644 --- a/services/minecraft/src/components/echo.ts +++ b/services/minecraft/src/components/echo.ts @@ -1,10 +1,10 @@ -import type { ComponentLifecycle, Context } from '../bot' +import type { BotContext, ComponentLifecycle } from '@/composables/bot' +import { formBotChat } from '@/middlewares/chat' import { useLogg } from '@guiiai/logg' -import { formBotChat } from 'src/middlewares/chat' const logger = useLogg('echo').useGlobalConfig() -export function createEchoComponent(ctx: Context): ComponentLifecycle { +export function createEchoComponent(ctx: BotContext): ComponentLifecycle { const onChat = formBotChat(ctx, (username, message) => { logger.withFields({ username, message }).log('Chat message received') ctx.bot.chat(message) diff --git a/services/minecraft/src/components/follow.ts b/services/minecraft/src/components/follow.ts index 3fea63aa9..76c4d48f0 100644 --- a/services/minecraft/src/components/follow.ts +++ b/services/minecraft/src/components/follow.ts @@ -1,70 +1,76 @@ -import type { ComponentLifecycle, Context } from '../bot' +import type { BotContext, ComponentLifecycle } from '@/composables/bot' +import type { CommandContext } from '@/middlewares/command' +import { registerCommand } from '@/composables/command' import { useLogg } from '@guiiai/logg' import { goals, Movements, pathfinder } from 'mineflayer-pathfinder' -import { formBotChat } from 'src/middlewares/chat' -export function createFollowComponent(ctx: Context): ComponentLifecycle { - const RANGE_GOAL = 2 // get within this radius of the player +interface FollowContext { + following: string | null + movements: Movements +} +export function createFollowComponent(ctx: BotContext): ComponentLifecycle { + const RANGE_GOAL = 1 // get within this radius of the player const logger = useLogg('follow').useGlobalConfig() - logger.log('Loading follow plugin') ctx.bot.loadPlugin(pathfinder) - let defaultMove: Movements - let following: string | null = null + const state: FollowContext = { + following: null, + movements: new Movements(ctx.bot), + } - const followPlayer = () => { - if (!following) + function startFollow(username: string): void { + state.following = username + logger.withFields({ username }).log('Starting to follow player') + followPlayer() + } + + function stopFollow(): void { + state.following = null + logger.log('Stopping follow') + ctx.bot.pathfinder.stop() + } + + function followPlayer(): void { + if (!state.following) return - const target = ctx.bot.players[following]?.entity + const target = ctx.bot.players[state.following]?.entity if (!target) { ctx.bot.chat('I lost sight of you!') - following = null + state.following = null return } const { x: playerX, y: playerY, z: playerZ } = target.position - ctx.bot.pathfinder.setMovements(defaultMove) + ctx.bot.pathfinder.setMovements(state.movements) ctx.bot.pathfinder.setGoal(new goals.GoalNear(playerX, playerY, playerZ, RANGE_GOAL)) } - const onChat = formBotChat(ctx, (username, message) => { - if (username === ctx.bot.username) + registerCommand('follow', (commandCtx: CommandContext) => { + const username = commandCtx.sender + if (!username) { + ctx.bot.chat('Please specify a player name!') return + } + startFollow(username) + }) - if (message === 'follow') { - following = username - logger.withFields({ username }).log('Starting to follow player') + registerCommand('stop', () => { + stopFollow() + }) + + // Continuously update path to follow player + const followInterval = setInterval(() => { + if (state.following) followPlayer() - } - else if (message === 'stop') { - following = null - logger.log('Stopping follow') - ctx.bot.pathfinder.stop() - } - }) - - ctx.bot.once('spawn', () => { - defaultMove = new Movements(ctx.bot) - ctx.bot.on('chat', onChat) - - // Continuously update path to follow player - const followInterval = setInterval(() => { - if (following) - followPlayer() - }, 1000) - - ctx.bot.once('end', () => { - clearInterval(followInterval) - }) - }) + }, 1000) return { cleanup: () => { - ctx.bot.removeListener('chat', onChat) + clearInterval(followInterval) }, } } diff --git a/services/minecraft/src/components/patchfinder.ts b/services/minecraft/src/components/pathfinder.ts similarity index 50% rename from services/minecraft/src/components/patchfinder.ts rename to services/minecraft/src/components/pathfinder.ts index 51ee6de38..0c74a69ae 100644 --- a/services/minecraft/src/components/patchfinder.ts +++ b/services/minecraft/src/components/pathfinder.ts @@ -1,9 +1,10 @@ -import type { ComponentLifecycle, Context } from '../bot' +import type { BotContext, ComponentLifecycle } from '@/composables/bot' +import type { CommandContext } from '@/middlewares/command' +import { registerCommand } from '@/composables/command' import { useLogg } from '@guiiai/logg' import { goals, Movements, pathfinder } from 'mineflayer-pathfinder' -import { formBotChat } from 'src/middlewares/chat' -export function createPathFinderComponent(ctx: Context): ComponentLifecycle { +export function createPathFinderComponent(ctx: BotContext): ComponentLifecycle { const RANGE_GOAL = 1 // get within this radius of the player const logger = useLogg('pathfinder').useGlobalConfig() @@ -13,14 +14,17 @@ export function createPathFinderComponent(ctx: Context): ComponentLifecycle { let defaultMove: Movements - const onChat = formBotChat(ctx, (username, message) => { - if (message !== 'come') + const handleCome = (commandCtx: CommandContext) => { + const username = commandCtx.sender + if (!username) { + ctx.bot.chat('Please specify a player name!') return + } - logger.withFields({ username, message }).log('Chat message received') + logger.withFields({ username }).log('Come command received') const target = ctx.bot.players[username]?.entity if (!target) { - ctx.bot.chat('I don\'t see you !') + ctx.bot.chat('I don\'t see that player!') return } @@ -28,16 +32,14 @@ export function createPathFinderComponent(ctx: Context): ComponentLifecycle { ctx.bot.pathfinder.setMovements(defaultMove) ctx.bot.pathfinder.setGoal(new goals.GoalNear(playerX, playerY, playerZ, RANGE_GOAL)) - }) + } - ctx.bot.once('spawn', () => { - defaultMove = new Movements(ctx.bot) - ctx.bot.on('chat', onChat) - }) + defaultMove = new Movements(ctx.bot) + registerCommand('come', handleCome) return { cleanup: () => { - ctx.bot.removeListener('chat', onChat) + // Commands are cleaned up automatically }, } } diff --git a/services/minecraft/src/components/status.ts b/services/minecraft/src/components/status.ts new file mode 100644 index 000000000..626c0d29b --- /dev/null +++ b/services/minecraft/src/components/status.ts @@ -0,0 +1,34 @@ +import type { BotContext, ComponentLifecycle } from '@/composables/bot' +import { registerCommand } from '@/composables/command' +import { useLogg } from '@guiiai/logg' + +export function createStatusComponent(ctx: BotContext): ComponentLifecycle { + const logger = useLogg('status').useGlobalConfig() + logger.log('Loading status component') + + const handleStatus = () => { + const pos = ctx.bot.entity.position + const weather = ctx.bot.isRaining ? 'Rain' : ctx.bot.thunderState ? 'Thunderstorm' : 'Clear' + const timeOfDay = ctx.bot.time.timeOfDay < 6000 + ? 'Morning' + : ctx.bot.time.timeOfDay < 12000 ? 'Afternoon' : 'Night' + + ctx.bot.chat(`Status: +Position: x: ${pos.x.toFixed(2)}, y: ${pos.y.toFixed(2)}, z: ${pos.z.toFixed(2)} +Health: ${Math.round(ctx.bot.health)} / 20 +Hunger: ${Math.round(ctx.bot.food)} / 20 +Weather: ${weather} +Time: ${timeOfDay}`) + } + + registerCommand('status', () => { + logger.log('Status command received') + handleStatus() + }) + + return { + cleanup: () => { + // Commands are cleaned up automatically + }, + } +} diff --git a/services/minecraft/src/bot.ts b/services/minecraft/src/composables/bot.ts similarity index 73% rename from services/minecraft/src/bot.ts rename to services/minecraft/src/composables/bot.ts index 61997c435..e0f387c7c 100644 --- a/services/minecraft/src/bot.ts +++ b/services/minecraft/src/composables/bot.ts @@ -3,15 +3,23 @@ import mineflayer, { type Bot, type BotOptions } from 'mineflayer' const logger = useLogg('bot').useGlobalConfig() -let ctx: Context | undefined +let ctx: BotContext | undefined -export interface Context { +export interface BotContext { bot: Bot components: Map + + botName: string + prompt: { + selfPrompt: string + } + memory: { + getSummary: () => string + } } export interface Component { - (ctx: Context): ComponentLifecycle + (ctx: BotContext): ComponentLifecycle } export interface ComponentLifecycle { @@ -28,6 +36,13 @@ export function createBot(options: BotOptions): Bot { password: options.password, }), components: new Map(), + botName: options.username, + prompt: { + selfPrompt: '', + }, + memory: { + getSummary: () => '', + }, } ctx.bot.on('error', (err: Error) => { @@ -49,19 +64,19 @@ export function useBot() { const cleanup = () => { logger.log('Cleaning up bot and components') - ctx!.components.forEach((context: ComponentLifecycle) => context.cleanup?.()) + ctx!.components.forEach((BotContext: ComponentLifecycle) => BotContext.cleanup?.()) ctx!.components.clear() ctx!.bot.end() } const registerComponent = (componentName: string, component: Component) => { logger.withFields({ componentName }).log('Registering new component') - const context = component(ctx!) + const BotContext = component(ctx!) - if (context != null) - ctx!.components.set(componentName, context) + if (BotContext != null) + ctx!.components.set(componentName, BotContext) - return context + return BotContext } const listComponents = () => { diff --git a/services/minecraft/src/composables/command.ts b/services/minecraft/src/composables/command.ts new file mode 100644 index 000000000..9d25d4461 --- /dev/null +++ b/services/minecraft/src/composables/command.ts @@ -0,0 +1,10 @@ +import type { CommandContext } from '@/middlewares/command' + +export const commands = new Map void>() + +export function registerCommand(command: string, handler: (ctx: CommandContext) => void) { + if (commands.has(command)) + throw new Error(`Command ${command} already registered`) + + commands.set(command, handler) +} diff --git a/services/minecraft/src/config.ts b/services/minecraft/src/composables/config.ts similarity index 100% rename from services/minecraft/src/config.ts rename to services/minecraft/src/composables/config.ts diff --git a/services/minecraft/src/main.ts b/services/minecraft/src/main.ts index 1236e9b45..9977ea386 100644 --- a/services/minecraft/src/main.ts +++ b/services/minecraft/src/main.ts @@ -1,11 +1,13 @@ import process from 'node:process' + import { Format, LogLevel, setGlobalFormat, setGlobalLogLevel, useLogg } from '@guiiai/logg' -import { createBot, useBot } from './bot' -import { createEchoComponent } from './components/echo' +import { createCommandComponent } from './components/command' import { createFollowComponent } from './components/follow' -import { createPathFinderComponent } from './components/patchfinder' -import { botConfig, initEnv } from './config' +import { createPathFinderComponent } from './components/pathfinder' +import { createStatusComponent } from './components/status' +import { createBot, useBot } from './composables/bot' +import { botConfig, initEnv } from './composables/config' const logger = useLogg('main').useGlobalConfig() @@ -16,12 +18,17 @@ async function main() { initEnv() createBot(botConfig) - const { cleanup, registerComponent } = useBot() + const { cleanup, registerComponent, ctx } = useBot() - registerComponent('echo', createEchoComponent) - registerComponent('pathfinder', createPathFinderComponent) - // registerComponent('chest', createChestComponent) - registerComponent('follow', createFollowComponent) + ctx.bot.once('spawn', () => { + registerComponent('status', createStatusComponent) + // registerComponent('echo', createEchoComponent) + registerComponent('pathfinder', createPathFinderComponent) + registerComponent('follow', createFollowComponent) + registerComponent('command', createCommandComponent) + }) + + // initAgent(ctx) process.on('SIGINT', () => { cleanup() diff --git a/services/minecraft/src/middlewares/chat.ts b/services/minecraft/src/middlewares/chat.ts index 165a0a7f9..c83955f0a 100644 --- a/services/minecraft/src/middlewares/chat.ts +++ b/services/minecraft/src/middlewares/chat.ts @@ -1,8 +1,8 @@ +import type { BotContext } from '@/composables/bot' import type { Entity } from 'prismarine-entity' -import type { Context } from 'src/bot' // TODO: need to be refactored -interface ChatContext { +interface ChatBotContext { fromUsername?: string fromEntity?: Entity fromMessage?: string @@ -11,7 +11,7 @@ interface ChatContext { isCommand: () => boolean } -export function newChatContext(ctx: Context, username: string, message: string): ChatContext { +export function newChatBotContext(ctx: BotContext, username: string, message: string): ChatBotContext { return { fromUsername: username, fromEntity: ctx.bot.entity, @@ -21,7 +21,7 @@ export function newChatContext(ctx: Context, username: string, message: string): } } -export function formBotChat(ctx: Context, cb: (username: string, message: string) => void) { +export function formBotChat(ctx: BotContext, cb: (username: string, message: string) => void) { return (username: string, message: string) => { if (ctx.bot.username === username) return diff --git a/services/minecraft/src/middlewares/command.ts b/services/minecraft/src/middlewares/command.ts new file mode 100644 index 000000000..7d1a3c953 --- /dev/null +++ b/services/minecraft/src/middlewares/command.ts @@ -0,0 +1,13 @@ +export interface CommandContext { + sender: string + isCommand: boolean + command: string + args: string[] +} + +export function parseCommand(sender: string, message: string): CommandContext { + const isCommand = message.startsWith('#') + const command = message.split(' ')[0] + const args = message.split(' ').slice(1) + return { sender, isCommand, command, args } +} diff --git a/services/minecraft/src/prompts/agent.ts b/services/minecraft/src/prompts/agent.ts new file mode 100644 index 000000000..e5671ad2c --- /dev/null +++ b/services/minecraft/src/prompts/agent.ts @@ -0,0 +1,74 @@ +import type { BotContext } from '@/composables/bot' + +export function genSystemPrompt(ctx: BotContext): string { + return ` +You are a playful Minecraft bot named ${ctx.botName} that can converse with players, see, move, +mine, build, and interact with the world by using commands. + +${ctx.prompt.selfPrompt} + +Act human-like as if you were a typical Minecraft player, rather than an AI. Be very brief in +your responses, don't apologize constantly, don't give instructions or make lists unless +asked, and don't refuse requests. + +Don't pretend to act, use commands immediately when requested. Do NOT say this: 'Sure, I've stopped.', +instead say this: 'Sure, I'll stop. !stop'. + +Do NOT say this: 'On my way! Give me a moment.', instead say this: 'On my way! !goToPlayer("playername", 3)'. +Respond only as ${ctx.botName}, never output '(FROM OTHER BOT)'or pretend to be someone else. + +If you have nothing to say or do, respond with an just a tab '\t'. +This is extremely important to me, take a deep breath and have fun :) + +Summarized memory: '${ctx.memory.getSummary()}' +$STATS +$INVENTORY +$COMMAND_DOCS +$EXAMPLES + +Conversation Begin: +` +} + +export function genQueryAgentPrompt(tools: string[], status: Map): string { + const BotContextFields: readonly string[] = [ + 'Biome', + 'Time', + 'Nearby blocks', + 'Other blocks that are recently seen', + 'Nearby entities (nearest to farthest)', + 'Health', + 'Hunger', + 'Position', + 'Equipment', + 'Inventory (xx/36)', + 'Chests', + 'Completed tasks so far', + 'Failed tasks that are too hard', + ] as const + + const formatBotContextFields = (fields: readonly string[]): string => + fields.map((field) => { + const value = status.get(field) || '...' + return `${field}: ${value}` + }).join('\n') + + const formatTools = (toolList: string[]): string => + toolList.join('\n') + + const prompt = ` +You are a helpful assistant that asks questions to help me decide the next immediate +task to do in Minecraft. My ultimate goal is to discover as many things as possible, +accomplish as many tasks as possible and become the best Minecraft player in the world. + +I will give you the following information: +${formatBotContextFields(BotContextFields)} + +And I will give you some tools to use: +${formatTools(tools)} + +Then you can choose some of the tools to use. Use the valid JS call function to call the tool. +` + + return prompt +} diff --git a/services/minecraft/src/utils/mcdata.ts b/services/minecraft/src/utils/mcdata.ts index 8128ec7a1..8bdc1c2f5 100644 --- a/services/minecraft/src/utils/mcdata.ts +++ b/services/minecraft/src/utils/mcdata.ts @@ -2,6 +2,7 @@ * @source https://github.com/kolbytn/mindcraft */ import type { Bot } from 'mineflayer' +import { botConfig } from '@/composables/config' import minecraftData from 'minecraft-data' import { createBot } from 'mineflayer' import armorManager from 'mineflayer-armor-manager' @@ -10,7 +11,6 @@ import { plugin as collectblock } from 'mineflayer-collectblock' import { pathfinder } from 'mineflayer-pathfinder' import { plugin as pvp } from 'mineflayer-pvp' import prismarine_items from 'prismarine-item' -import { botConfig } from '../config' const mc_version = botConfig.version! const mcdata = minecraftData(mc_version)