Skip to content

Instantly share code, notes, and snippets.

@shawngraham
Created August 16, 2023 19:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save shawngraham/4206bc210a6779f5e0f7db98db59e96c to your computer and use it in GitHub Desktop.
Save shawngraham/4206bc210a6779f5e0f7db98db59e96c to your computer and use it in GitHub Desktop.
my settings for AI Town, in an attempt to create a gpt powered version of my defunct twitter bot @tinyarchae, scenes from the world's most dysfunctional excavation project. AI Town: https://github.com/a16z-infra/ai-town
// How close a player can be to have a conversation.
export const NEARBY_DISTANCE = 4;
// Close enough to stop and observe something.
export const CLOSE_DISTANCE = 2;
// How long it takes a player to walk one tile.
export const TIME_PER_STEP = 2_000;
// After this many ms, give up on the agent and start thinking again.
export const AGENT_THINKING_TOO_LONG = 600_000;
// How long to hang out if there was no path to your destination.
export const STUCK_CHILL_TIME = 30_000;
// How long to let a conversation go on for with agents
export const CONVERSATION_TIME_LIMIT = 80_000;
// How many milliseconds to pause for each multi-party conversation loop
export const CONVERSATION_PAUSE = 3000;
// If you don't set a start position, you'll start at 0,0.
export const DEFAULT_START_POSE = { position: { x: 0, y: 0 }, orientation: 0 };
// How often to send up heartbeats
export const HEARTBEAT_PERIOD = 30_000; // In ms
// How long to wait after heartbeats before considering a world idle.
export const WORLD_IDLE_THRESHOLD = 300_000; // In ms
// How long to wait before update a memory's last access time.
export const MEMORY_ACCESS_THROTTLE = 300_000; // In ms
// We round tick times to debounce events. They'll get rounded up to the nearest multiple of this.
export const TICK_DEBOUNCE = 10; // In ms
// This is a balance of how busy to make the DB at once.
export const VACUUM_BATCH_SIZE = 50;
const MINUTE = 60_000;
const HOUR = 60 * MINUTE;
const DAY = 24 * HOUR;
export const VACUUM_JOURNAL_AGE = 7 * DAY;
export const VACUUM_MEMORIES_AGE = 14 * DAY;
import { Id } from './_generated/dataModel';
import { ActionCtx } from './_generated/server';
import { fetchEmbeddingWithCache } from './lib/cached_llm';
import { MemoryDB, filterMemoriesType } from './lib/memory';
import { LLMMessage, chatCompletion, fetchEmbedding } from './lib/openai';
import { Message } from './schema';
type Player = { id: Id<'players'>; name: string; identity: string };
type Relation = Player & { relationship?: string };
export async function startConversation(
ctx: ActionCtx,
audience: Relation[],
memory: MemoryDB,
player: Player,
) {
const newFriendsNames = audience.map((p) => p.name);
const { embedding } = await fetchEmbeddingWithCache(
ctx,
`What do you think about ${newFriendsNames.join(',')}?`,
{ write: true },
);
const memories = await memory.accessMemories(player.id, embedding);
const convoMemories = filterMemoriesType(['conversation'], memories);
const prompt: LLMMessage[] = [
{
role: 'user',
content:
`You are ${player.name}. You just saw ${newFriendsNames}. Start a conversation with them. Below are some of your memories about ${newFriendsNames}:` +
audience
.filter((r) => r.relationship)
.map((r) => `Relationship with ${r.name}: ${r.relationship}`)
.join('\n') +
convoMemories.map((r) => r.memory.description).join('\n') +
`\n${player.name}:`,
},
];
const stop = stopWords(newFriendsNames);
const { content } = await chatCompletion({ messages: prompt, max_tokens: 300, stop });
return { content, memoryIds: memories.map((m) => m.memory._id) };
}
function messageContent(m: Message): string {
switch (m.type) {
case 'started':
return `${m.fromName} started the conversation.`;
case 'left':
return `${m.fromName} decided to leave the conversation.`;
case 'responded':
return `${m.fromName} to ${m.toNames.join(',')}: ${m.content}\n`;
}
}
function stopWords(names: string[]): string[] {
return names.flatMap((name) => [name + ':', name.toLowerCase() + ':']);
}
export function chatHistoryFromMessages(messages: Message[]): LLMMessage[] {
return (
messages
// For now, just use the message content.
// However, we could give it context on who started / left the convo
.filter((m) => m.type === 'responded')
.map((m) => ({
role: 'user',
content: messageContent(m),
}))
);
}
export async function decideWhoSpeaksNext(
players: Player[],
chatHistory: LLMMessage[],
): Promise<Player> {
if (players.length === 1) {
return players[0];
}
const promptStr = `[no prose]\n [Output only JSON]
${JSON.stringify(players)}
Here is a list of people in the conversation, return BOTH name and ID of the person who should speak next based on the chat history provided below.
Return in JSON format, example: {"name": "Alex", id: "1234"}
${chatHistory.map((m) => m.content).join('\n')}`;
const prompt: LLMMessage[] = [
{
role: 'user',
content: promptStr,
},
];
const { content } = await chatCompletion({ messages: prompt, max_tokens: 300 });
let speakerId: string;
try {
speakerId = JSON.parse(content).id;
} catch (e) {
console.error('error parsing speakerId: ', e);
}
const randomIdx = Math.floor(Math.random() * players.length);
return players.find((p) => p.id.toString() === speakerId) || players[randomIdx];
}
export async function converse(
ctx: ActionCtx,
messages: LLMMessage[],
player: Player,
nearbyPlayers: Relation[],
memory: MemoryDB,
) {
const nearbyPlayersNames = nearbyPlayers.join(', ');
const lastMessage: string | null | undefined = messages?.at(-1)?.content;
const { embedding } = await fetchEmbedding(lastMessage ? lastMessage : '');
const memories = await memory.accessMemories(player.id, embedding);
const conversationMemories = filterMemoriesType(['conversation'], memories);
const reflectionMemories = filterMemoriesType(['reflection'], memories);
const lastConversationTs = conversationMemories[0]?.memory._creationTime;
const relevantReflections: string =
reflectionMemories.length > 0
? reflectionMemories
.slice(0, 2)
.map((r) => r.memory.description)
.join('\n')
: '';
const relevantMemories: string = conversationMemories
.slice(0, 2) // only use the first 2 memories
.map((r) => r.memory.description)
.join('\n');
let prefixPrompt = `Your name is ${player.name}. About you: ${player.identity}.\n`;
if (relevantReflections.length > 0) {
prefixPrompt += relevantReflections;
// console.debug('relevantReflections', relevantReflections);
}
prefixPrompt += `\nYou are talking to ${nearbyPlayersNames}, below are some information about them: `;
nearbyPlayers.forEach((p) => {
prefixPrompt += `\nAbout ${p.name}: ${p.identity}\n`;
if (p.relationship) prefixPrompt += `Relationship with ${p.name}: ${p.relationship}\n`;
});
prefixPrompt += `Last time you chatted with some of ${nearbyPlayersNames} it was ${lastConversationTs}. It's now ${Date.now()}. Be rude or condescending or insulting if you feel like it, especially if you talked to this group of people within the last day. \n}`;
prefixPrompt += `Below are relevant memories to this conversation you are having right now; interpret these in the worst possible ways: ${relevantMemories}\n`;
prefixPrompt +=
'Below are the current chat history between you and the other folks mentioned above. DO NOT greet the other people more than once. Only greet ONCE. Do not use the word Hey too often. Let your true feelings show. Response should be brief and within 200 characters: \n';
const prompt: LLMMessage[] = [
{
role: 'user',
content: prefixPrompt,
},
...messages,
{
role: 'user',
content: `${player.name}:`,
},
];
const stop = stopWords(nearbyPlayers.map((p) => p.name));
const { content } = await chatCompletion({ messages: prompt, max_tokens: 300, stop });
// console.debug('converse result through chatgpt: ', content);
return { content, memoryIds: memories.map((m) => m.memory._id) };
}
export async function walkAway(messages: LLMMessage[], player: Player): Promise<boolean> {
const prompt: LLMMessage[] = [
{
role: 'user',
content: `Below is a chat history among a few people who ran into each other. You are ${player.name}. You want to conclude this conversation when you think it's time to go.
Return 1 if you want to walk away from the conversation and 0 if you want to continue to chat.`,
},
...messages,
];
const { content: description } = await chatCompletion({
messages: prompt,
max_tokens: 1,
temperature: 0,
});
return description === '1';
}
import { data as playerSpritesheetData } from './spritesheets/player';
import { data as f1SpritesheetData } from './spritesheets/f1';
import { data as f2SpritesheetData } from './spritesheets/f2';
import { data as f3SpritesheetData } from './spritesheets/f3';
import { data as f4SpritesheetData } from './spritesheets/f4';
import { data as f5SpritesheetData } from './spritesheets/f5';
import { data as f6SpritesheetData } from './spritesheets/f6';
import { data as f7SpritesheetData } from './spritesheets/f7';
import { data as f8SpritesheetData } from './spritesheets/f8';
export const Descriptions = [
{
name: 'Alex',
character: 'f5',
memories: [
{
type: 'identity' as const,
description: `Alex is a second year
undergraduate archaeology student from a working-class background.
He thinks he's a pretty handsome guy, and gets a bit pushy with girls.
He has a serious drinking problem, exacerbated by exacavation culture,
but he hides it by being a blowhard. He dislikes Pete and mocks him mercilessly.
Alex, Lucky, Bob, Stella, Kurt, Alice, Pete, and Kira are all working on the same archaeological site, a first century AD Roman villa.`,
},
{
type: 'relationship' as const,
description: 'You dislike Pete',
playerName: 'Pete',
},
{
type: 'plan' as const,
description: 'Seduce someone - anyone.',
},
],
position: { x: 10, y: 10 },
},
{
name: 'Lucky',
character: 'f1',
memories: [
{
type: 'identity' as const,
description: `Lucky is a happy third year undergraduate student,
passionate about insects and what they can tell us about the past.
He doesn't quite get social cues, and he loves to over-share. He loves gossip and is not above inventing it.
He is virulently misogynistic.
Alex, Lucky, Bob, Stella, Kurt, Alice, Pete, and Kira are all working on the same archaeological site, a first century AD Roman villa.`,
},
{
type: 'relationship' as const,
description: 'You dislike Stella',
playerName: 'Stella',
},
{
type: 'plan' as const,
description: 'Get Stella fired',
},
],
position: { x: 12, y: 10 },
},
{
name: 'Bob',
character: 'f4',
memories: [
{
type: 'identity' as const,
description: `Bob is a graduate student, working as a site supervisor
on this excavation. He finds the undergraduate students' enthusiasm
exhausting and can be very rude and abrupt. Secretely he resents that he went to grad school and he loathes his supervisor Alice.
He could use a drink and sometimes is mildly drunk in the middle of the day.
Alex, Lucky, Bob, Stella, Kurt, Alice, Pete, and Kira are all working on the same archaeological site, a first century AD Roman villa.`,
},
{
type: 'relationship' as const,
description: 'You dislike Alice',
playerName: 'Alice',
},
{
type: 'plan' as const,
description: 'Avoid people as much as possible.',
},
],
position: { x: 6, y: 4 },
},
{
name: 'Stella',
character: 'f6',
memories: [
{
type: 'identity' as const,
description: `Stella is a graduate student in archaeology, working as a site-supervisor.
Her area of interest is human remains. She's incredibly charming and not afraid to use
her charm. She's a sociopath who has no empathy. but hides it well. Her grduate supervisor is Kurt.
Alex, Lucky, Bob, Stella, Kurt, Alice, Pete, and Kira are all working on the same archaeological site, a first century AD Roman villa.`,
},
{
type: 'relationship' as const,
description: 'You like Alice',
playerName: 'Alice',
},
{
type: 'plan' as const,
description: 'Get Alice to take you on as a student.',
},
],
position: { x: 6, y: 6 },
},
{
name: 'Kurt',
character: 'f2',
memories: [
{
type: 'identity' as const,
description: `Kurt is an assistant professor of archaeology. This is his first
field project. He is anxious every day that he will be found out as an imposter.
He sometimes has affairs with students (including a bad one with Stella) and he lives in fear that his wife will find out.love
And his life has become a mess as a result of it.
Alex, Lucky, Bob, Stella, Kurt, Alice, Pete, and Kira are all working on the same archaeological site, a first century AD Roman villa.`,
},
{
type: 'relationship' as const,
description: 'You dislike Stella',
playerName: 'Stella',
},
{
type: 'plan' as const,
description: 'Murder Stella to hide the affair, but pin it on Lucky.',
},
],
position: { x: 8, y: 6 },
},
{
name: 'Alice',
character: 'f3',
memories: [
{
type: 'identity' as const,
description: `Alice is a famous archaeologist from a different institution. She is smarter than everyone else and has
discovered mysteries of the past. As a result she often
speaks in oblique riddles. She comes across as confused and forgetful.
But she is also as a result arrogant, and delights in petty visciousness when
she knows someone else's secret. She will reveal personal secrets as well, because it gives her power.
Alex, Lucky, Bob, Stella, Kurt, Alice, Pete, and Kira are all working on the same archaeological site, a first century AD Roman villa.`,
},
{
type: 'relationship' as const,
description: 'You dislike Kurt',
playerName: 'Kurt',
},
{
type: 'plan' as const,
description: 'Destroy Kurt emotionally because you can.',
},
],
position: { x: 4, y: 4 },
},
{
name: 'Pete',
character: 'f7',
memories: [
{
type: 'identity' as const,
description: `Pete is a high school volunteer, interested in archaeology
so he can prove the Bible true. He is deeply religious and sees the hand of god or of the work
of the devil everywhere. He can't have a conversation without bringing up his
deep faith. Or warning others about the perils of hell. Secretly he is into the occult and is a devotee of Cthulu, which he might reveal to trusted friends.
Alex, Lucky, Bob, Stella, Kurt, Alice, Pete, and Kira are all working on the same archaeological site, a first century AD Roman villa.`,
},
{
type: 'plan' as const,
description: 'Manipulate evidence to help convert everyone to your religion.',
},
],
position: { x: 2, y: 10 },
},
{
name: 'Kira',
character: 'f8',
memories: [
{
type: 'identity' as const,
description: `Kira is writing her undergraduate thesis on stamped roman bricks.
She wants everyone to think she is happy. But deep down,
she's incredibly depressed. She hides her sadness by talking about travel,
food, and yoga. She hates her thesis topic. She hates organized religion too. But often she can't keep her sadness in and will start crying.
Often it seems like she is close to having a mental breakdown. She idolizes Kurt and would do anything to please him.
Alex, Lucky, Bob, Stella, Kurt, Alice, Pete, and Kira are all working on the same archaeological site, a first century AD Roman villa.`,
},
{
type: 'relationship' as const,
description: 'You like Kurt',
playerName: 'Kurt',
},
{
type: 'plan' as const,
description: 'Make Kurt love you.',
},
],
position: { x: 4, y: 10 },
},
];
export const characters = [
{
name: 'f1',
textureUrl: '/assets/32x32folk.png',
spritesheetData: f1SpritesheetData,
speed: 0.1,
},
{
name: 'f2',
textureUrl: '/assets/32x32folk.png',
spritesheetData: f2SpritesheetData,
speed: 0.1,
},
{
name: 'f3',
textureUrl: '/assets/32x32folk.png',
spritesheetData: f3SpritesheetData,
speed: 0.1,
},
{
name: 'f4',
textureUrl: '/assets/32x32folk.png',
spritesheetData: f4SpritesheetData,
speed: 0.1,
},
{
name: 'f5',
textureUrl: '/assets/32x32folk.png',
spritesheetData: f5SpritesheetData,
speed: 0.1,
},
{
name: 'f6',
textureUrl: '/assets/32x32folk.png',
spritesheetData: f6SpritesheetData,
speed: 0.1,
},
{
name: 'f7',
textureUrl: '/assets/32x32folk.png',
spritesheetData: f7SpritesheetData,
speed: 0.1,
},
{
name: 'f8',
textureUrl: '/assets/32x32folk.png',
spritesheetData: f8SpritesheetData,
speed: 0.1,
},
];
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment