feat: command
This commit is contained in:
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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<Agent | Promise<Agent>>
|
||||
}
|
||||
|
||||
// State management
|
||||
const agents = new Set<Agent | Promise<Agent>>()
|
||||
|
||||
const logger = useLogg('openai').useGlobalConfig()
|
||||
|
||||
// Agent initialization
|
||||
// export async function initAgent(): Promise<Neuri> {
|
||||
// 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<Agent> {
|
||||
// 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
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
import type { Bot } from 'mineflayer'
|
||||
import { z } from 'zod'
|
||||
|
||||
// Core types
|
||||
type QueryResult = string | Promise<string>
|
||||
|
||||
// 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<string, number>
|
||||
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<any>
|
||||
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
|
||||
@@ -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)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
+15
-13
@@ -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
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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<string, ComponentLifecycle>
|
||||
|
||||
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 = () => {
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { CommandContext } from '@/middlewares/command'
|
||||
|
||||
export const commands = new Map<string, (ctx: CommandContext) => 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)
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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, string>): 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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user