import { Id, TableNames } from './_generated/dataModel'; import { internal } from './_generated/api'; import { DatabaseReader, internalAction, internalMutation, mutation, query, } from './_generated/server'; import { v } from 'convex/values'; import schema from './schema'; import { DELETE_BATCH_SIZE } from './constants'; import { kickEngine, startEngine, stopEngine } from './aiTown/main'; import { insertInput } from './aiTown/insertInput'; import { fetchEmbedding, LLM_CONFIG } from './util/llm'; import { chatCompletion } from './util/llm'; import { startConversationMessage } from './agent/conversation'; import { GameId } from './aiTown/ids'; // Clear all of the tables except for the embeddings cache. const excludedTables: Array = ['embeddingsCache']; export const wipeAllTables = internalMutation({ handler: async (ctx) => { for (const tableName of Object.keys(schema.tables)) { if (excludedTables.includes(tableName as TableNames)) { continue; } await ctx.scheduler.runAfter(0, internal.testing.deletePage, { tableName, cursor: null }); } }, }); export const deletePage = internalMutation({ args: { tableName: v.string(), cursor: v.union(v.string(), v.null()), }, handler: async (ctx, args) => { const results = await ctx.db .query(args.tableName as TableNames) .paginate({ cursor: args.cursor, numItems: DELETE_BATCH_SIZE }); for (const row of results.page) { await ctx.db.delete(row._id); } if (!results.isDone) { await ctx.scheduler.runAfter(0, internal.testing.deletePage, { tableName: args.tableName, cursor: results.continueCursor, }); } }, }); export const kick = internalMutation({ handler: async (ctx) => { const { worldStatus } = await getDefaultWorld(ctx.db); await kickEngine(ctx, worldStatus.worldId); }, }); export const stopAllowed = query({ handler: async () => { return !process.env.STOP_NOT_ALLOWED; }, }); export const stop = mutation({ handler: async (ctx) => { if (process.env.STOP_NOT_ALLOWED) throw new Error('Stop not allowed'); const { worldStatus, engine } = await getDefaultWorld(ctx.db); if (worldStatus.status === 'inactive' || worldStatus.status === 'stoppedByDeveloper') { if (engine.running) { throw new Error(`Engine ${engine._id} isn't stopped?`); } console.debug(`World ${worldStatus.worldId} is already inactive`); return; } console.log(`Stopping engine ${engine._id}...`); await ctx.db.patch(worldStatus._id, { status: 'stoppedByDeveloper' }); await stopEngine(ctx, worldStatus.worldId); }, }); export const resume = mutation({ handler: async (ctx) => { const { worldStatus, engine } = await getDefaultWorld(ctx.db); if (worldStatus.status === 'running') { if (!engine.running) { throw new Error(`Engine ${engine._id} isn't running?`); } console.debug(`World ${worldStatus.worldId} is already running`); return; } console.log( `Resuming engine ${engine._id} for world ${worldStatus.worldId} (state: ${worldStatus.status})...`, ); await ctx.db.patch(worldStatus._id, { status: 'running' }); await startEngine(ctx, worldStatus.worldId); }, }); export const archive = internalMutation({ handler: async (ctx) => { const { worldStatus, engine } = await getDefaultWorld(ctx.db); if (engine.running) { throw new Error(`Engine ${engine._id} is still running!`); } console.log(`Archiving world ${worldStatus.worldId}...`); await ctx.db.patch(worldStatus._id, { isDefault: false }); }, }); async function getDefaultWorld(db: DatabaseReader) { const worldStatus = await db .query('worldStatus') .filter((q) => q.eq(q.field('isDefault'), true)) .first(); if (!worldStatus) { throw new Error('No default world found'); } const engine = await db.get(worldStatus.engineId); if (!engine) { throw new Error(`Engine ${worldStatus.engineId} not found`); } return { worldStatus, engine }; } export const debugCreatePlayers = internalMutation({ args: { numPlayers: v.number(), }, handler: async (ctx, args) => { const { worldStatus } = await getDefaultWorld(ctx.db); for (let i = 0; i < args.numPlayers; i++) { const inputId = await insertInput(ctx, worldStatus.worldId, 'join', { name: `Robot${i}`, description: `This player is a robot.`, character: `f${1 + (i % 8)}`, type: 'villager', }); } }, }); export const randomPositions = internalMutation({ handler: async (ctx) => { const { worldStatus } = await getDefaultWorld(ctx.db); const map = await ctx.db .query('maps') .withIndex('worldId', (q) => q.eq('worldId', worldStatus.worldId)) .unique(); if (!map) { throw new Error(`No map for world ${worldStatus.worldId}`); } const world = await ctx.db.get(worldStatus.worldId); if (!world) { throw new Error(`No world for world ${worldStatus.worldId}`); } for (const player of world.players) { await insertInput(ctx, world._id, 'moveTo', { playerId: player.id, destination: { x: 1 + Math.floor(Math.random() * (map.width - 2)), y: 1 + Math.floor(Math.random() * (map.height - 2)), }, }); } }, }); export const testEmbedding = internalAction({ args: { input: v.string() }, handler: async (_ctx, args) => { return await fetchEmbedding(args.input); }, }); export const testCompletion = internalAction({ args: {}, handler: async (ctx, args) => { return await chatCompletion({ messages: [ { content: 'You are helpful', role: 'system' }, { content: 'Where is pizza?', role: 'user' }, ], }); }, }); export const testConvo = internalAction({ args: {}, handler: async (ctx, args) => { const a: any = (await startConversationMessage( ctx, 'm1707m46wmefpejw1k50rqz7856qw3ew' as Id<'worlds'>, 'c:115' as GameId<'conversations'>, 'p:0' as GameId<'players'>, 'p:6' as GameId<'players'>, )) as any; return await a.readAll(); }, });