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)), | |
}; | |
} |