Spaces:
Sleeping
Sleeping
| import { v } from 'convex/values'; | |
| import { internalAction } from '../_generated/server'; | |
| import { WorldMap, serializedWorldMap } from './worldMap'; | |
| import { rememberConversation } from '../agent/memory'; | |
| import { GameId, agentId, conversationId, playerId } from './ids'; | |
| import { | |
| continueConversationMessage, | |
| leaveConversationMessage, | |
| startConversationMessage, | |
| } from '../agent/conversation'; | |
| import { assertNever } from '../util/assertNever'; | |
| import { serializedAgent } from './agent'; | |
| import { ACTIVITIES, ACTIVITY_COOLDOWN, CONVERSATION_COOLDOWN } from '../constants'; | |
| import { api, internal } from '../_generated/api'; | |
| import { sleep } from '../util/sleep'; | |
| import { serializedPlayer } from './player'; | |
| export const agentRememberConversation = internalAction({ | |
| args: { | |
| worldId: v.id('worlds'), | |
| playerId, | |
| agentId, | |
| conversationId, | |
| operationId: v.string(), | |
| }, | |
| handler: async (ctx, args) => { | |
| await rememberConversation( | |
| ctx, | |
| args.worldId, | |
| args.agentId as GameId<'agents'>, | |
| args.playerId as GameId<'players'>, | |
| args.conversationId as GameId<'conversations'>, | |
| ); | |
| await sleep(Math.random() * 1000); | |
| await ctx.runMutation(api.aiTown.main.sendInput, { | |
| worldId: args.worldId, | |
| name: 'finishRememberConversation', | |
| args: { | |
| agentId: args.agentId, | |
| operationId: args.operationId, | |
| }, | |
| }); | |
| }, | |
| }); | |
| export const agentGenerateMessage = internalAction({ | |
| args: { | |
| worldId: v.id('worlds'), | |
| playerId, | |
| agentId, | |
| conversationId, | |
| otherPlayerId: playerId, | |
| operationId: v.string(), | |
| type: v.union(v.literal('start'), v.literal('continue'), v.literal('leave')), | |
| messageUuid: v.string(), | |
| }, | |
| handler: async (ctx, args) => { | |
| let completionFn; | |
| switch (args.type) { | |
| case 'start': | |
| completionFn = startConversationMessage; | |
| break; | |
| case 'continue': | |
| completionFn = continueConversationMessage; | |
| break; | |
| case 'leave': | |
| completionFn = leaveConversationMessage; | |
| break; | |
| default: | |
| assertNever(args.type); | |
| } | |
| const completion = await completionFn( | |
| ctx, | |
| args.worldId, | |
| args.conversationId as GameId<'conversations'>, | |
| args.playerId as GameId<'players'>, | |
| args.otherPlayerId as GameId<'players'>, | |
| ); | |
| // TODO: stream in the text instead of reading it all at once. | |
| const text = await completion.readAll(); | |
| await ctx.runMutation(internal.aiTown.agent.agentSendMessage, { | |
| worldId: args.worldId, | |
| conversationId: args.conversationId, | |
| agentId: args.agentId, | |
| playerId: args.playerId, | |
| text, | |
| messageUuid: args.messageUuid, | |
| leaveConversation: args.type === 'leave', | |
| operationId: args.operationId, | |
| }); | |
| }, | |
| }); | |
| export const agentDoSomething = internalAction({ | |
| args: { | |
| worldId: v.id('worlds'), | |
| player: v.object(serializedPlayer), | |
| agent: v.object(serializedAgent), | |
| map: v.object(serializedWorldMap), | |
| otherFreePlayers: v.array(v.object(serializedPlayer)), | |
| operationId: v.string(), | |
| }, | |
| handler: async (ctx, args) => { | |
| const { player, agent } = args; | |
| const map = new WorldMap(args.map); | |
| const now = Date.now(); | |
| // Don't try to start a new conversation if we were just in one. | |
| const justLeftConversation = | |
| agent.lastConversation && now < agent.lastConversation + CONVERSATION_COOLDOWN; | |
| // Don't try again if we recently tried to find someone to invite. | |
| const recentlyAttemptedInvite = | |
| agent.lastInviteAttempt && now < agent.lastInviteAttempt + CONVERSATION_COOLDOWN; | |
| const recentActivity = player.activity && now < player.activity.until + ACTIVITY_COOLDOWN; | |
| // Decide whether to do an activity or wander somewhere. | |
| if (!player.pathfinding) { | |
| if (recentActivity || justLeftConversation) { | |
| await sleep(Math.random() * 1000); | |
| await ctx.runMutation(api.aiTown.main.sendInput, { | |
| worldId: args.worldId, | |
| name: 'finishDoSomething', | |
| args: { | |
| operationId: args.operationId, | |
| agentId: agent.id, | |
| destination: wanderDestination(map), | |
| }, | |
| }); | |
| return; | |
| } else { | |
| // TODO: have LLM choose the activity & emoji | |
| const activity = ACTIVITIES[Math.floor(Math.random() * ACTIVITIES.length)]; | |
| await sleep(Math.random() * 1000); | |
| await ctx.runMutation(api.aiTown.main.sendInput, { | |
| worldId: args.worldId, | |
| name: 'finishDoSomething', | |
| args: { | |
| operationId: args.operationId, | |
| agentId: agent.id, | |
| activity: { | |
| description: activity.description, | |
| emoji: activity.emoji, | |
| until: Date.now() + activity.duration, | |
| }, | |
| }, | |
| }); | |
| return; | |
| } | |
| } | |
| const invitee = | |
| justLeftConversation || recentlyAttemptedInvite | |
| ? undefined | |
| : await ctx.runQuery(internal.aiTown.agent.findConversationCandidate, { | |
| now, | |
| worldId: args.worldId, | |
| player: args.player, | |
| otherFreePlayers: args.otherFreePlayers, | |
| }); | |
| // TODO: We hit a lot of OCC errors on sending inputs in this file. It's | |
| // easy for them to get scheduled at the same time and line up in time. | |
| await sleep(Math.random() * 1000); | |
| await ctx.runMutation(api.aiTown.main.sendInput, { | |
| worldId: args.worldId, | |
| name: 'finishDoSomething', | |
| args: { | |
| operationId: args.operationId, | |
| agentId: args.agent.id, | |
| invitee, | |
| }, | |
| }); | |
| }, | |
| }); | |
| function wanderDestination(worldMap: WorldMap) { | |
| // Wander someonewhere at least one tile away from the edge. | |
| return { | |
| x: 1 + Math.floor(Math.random() * (worldMap.width - 2)), | |
| y: 1 + Math.floor(Math.random() * (worldMap.height - 2)), | |
| }; | |
| } |