Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sweep: 玩家作为一个非 Agent 角色,本身也可以与 Agent 进行互动,譬如和 Agent 进行对话,Agent 会根据自己的设定对玩家进行回复我想知道这部分代码在哪里实现的 #10

Open
6 tasks done
2642543078 opened this issue Nov 14, 2023 · 2 comments · May be fixed by #11
Labels
sweep Assigns Sweep to an issue or pull request.

Comments

@2642543078
Copy link

2642543078 commented Nov 14, 2023

Details

No response

Checklist
  • Modify convex/agent.ts ! No changes made
  • Modify convex/conversation.tsf724484
  • Check convex/conversation.ts
  • Modify convex/conversation.ts0529692
  • Check convex/conversation.ts970ea0e
  • Check convex/conversation.ts

Flowchart

@2642543078 2642543078 added the sweep Assigns Sweep to an issue or pull request. label Nov 14, 2023
Copy link
Contributor

sweep-ai bot commented Nov 14, 2023

Here's the PR! #11.

Sweep Basic Tier: I'm using GPT-4. You have 5 GPT-4 tickets left for the month and 3 for the day. (tracking ID: 39797902a5)

For more GPT-4 tickets, visit our payment portal. For a one week free trial, try Sweep Pro (unlimited GPT-4 tickets).

Actions (click)

  • ↻ Restart Sweep

Sandbox Execution ✓

Here are the sandbox execution logs prior to making any changes:

Sandbox logs for d76e3ca
npm i 1/6 ✓
added 572 packages, and audited 573 packages in 38s
157 packages are looking for funding
  run `npm fund` for details
5 vulnerabilities (1 low, 4 moderate)
To address issues that do not require attention, run:
  npm audit fix
To address all issues, run:
  npm audit fix --force
Run `npm audit` for details.
npm notice 
npm notice New major version of npm available! 9.6.7 -> 10.2.3
npm notice Changelog: 
npm notice Run `npm install -g [email protected]` to update!
npm notice 
trunk init 2/6 ✓
⡿ Downloading Trunk 1.17.2...
⡿ Downloading Trunk 1.17.2...
⢿ Downloading Trunk 1.17.2...
⣻ Downloading Trunk 1.17.2...
⣽ Downloading Trunk 1.17.2...
⣾ Downloading Trunk 1.17.2...
⣷ Downloading Trunk 1.17.2...
✔ Downloading Trunk 1.17.2... done
⡿ Verifying Trunk sha256...
✔ Verifying Trunk sha256... done
⡿ Unpacking Trunk...
✔ Unpacking Trunk... done


✔ 13 linters were enabled (.trunk/trunk.yaml)
  checkov 3.0.35 (1 docker, 8 json, 5 yaml files)
  eslint 8.53.0 (7 javascript, 51 typescript files)
  git-diff-check (375 files)
  hadolint 2.12.0 (1 docker file) (created .hadolint.yaml)
  markdownlint 0.37.0 (2 markdown files) (created .markdownlint.yaml)
  osv-scanner 1.4.3 (1 lockfile file)
  oxipng 9.0.0 (275 png files)
  prettier 3.1.0 (1 css, 7 javascript, 7 json, 2 markdown, 1 prettier_supported_configs, 51 typescript, 5 yaml files)
  svgo 3.0.3 (10 svg files) (created svgo.config.js)
  terrascan 1.18.3 (1 docker file)
  trivy 0.47.0 (1 docker, 5 yaml files)
  trufflehog 3.62.1 (376 files)
  yamllint 1.33.0 (5 yaml files) (created .yamllint.yaml)
Next Steps
 1. Read documentation
    Our documentation can be found at https://docs.trunk.io
 2. Get help and give feedback
    Join the Trunk community at https://slack.trunk.io
trunk fmt convex/conversation.ts || exit 0 3/6 ✓
Checked 1 file
✔ No issues
trunk check --fix --print-failures convex/conversation.ts 4/6 ✓
  ISSUES  
convex/conversation.ts:9:6
 9:6  medium  'Relation' is defined but never used.  eslint/@typescript-eslint/no-unused-vars
Checked 1 file
1 existing issue
trunk fmt convex/conversation.ts || exit 0 5/6 ✓
Checked 1 file
✔ No issues
trunk check --fix --print-failures convex/conversation.ts 6/6 ✓
Checked 1 file
✔ No issues

Sandbox passed on the latest main, so sandbox checks will be enabled for this issue.


Step 1: 🔎 Searching

I found the following snippets in your repository. I will now analyze these snippets and come up with a plan.

Some code snippets I looked at (click to expand). If some file is missing from here, you can mention the path in the ticket description.

cat-town/convex/agent.ts

Lines 87 to 155 in d76e3ca

});
function divideIntoGroups(players: Player[]) {
const playerById = new Map(players.map((p) => [p.id, p]));
const groups: Player[][] = [];
const solos: Player[] = [];
while (playerById.size > 0) {
const player = playerById.values().next().value;
playerById.delete(player.id);
const nearbyPlayers = getNearbyPlayers(player.motion, [...playerById.values()]);
if (nearbyPlayers.length > 0) {
// If you only want to do 1:1 conversations, use this:
// groups.push([player, nearbyPlayers[0]]);
// playerById.delete(nearbyPlayers[0].id);
// otherwise, do more than 1:1 conversations by adding them all:
groups.push([player, ...nearbyPlayers]);
for (const nearbyPlayer of nearbyPlayers) {
playerById.delete(nearbyPlayer.id);
}
} else {
solos.push(player);
}
}
return { groups, solos };
}
async function handleAgentSolo(ctx: ActionCtx, player: Player, memory: MemoryDB, done: DoneFn) {
// console.debug('handleAgentSolo: ', player.name, player.id);
// Handle new observations: it can look at the agent's lastWakeTs for a delta.
// Calculate scores
// Run reflection on memories once in a while
await memory.reflectOnMemories(player.id, player.name);
// Future: Store observations about seeing players in conversation
// might include new observations -> add to memory with openai embeddings
// Later: handle object ownership?
// Based on plan and observations, determine next action:
// if so, add new memory for new plan, and return new action
const walk = player.motion.type === 'stopped' || player.motion.targetEndTs < Date.now();
// Ignore everyone we last said something to.
const ignore =
player.motion.type === 'walking' ? player.motion.ignore : player.lastChat?.message.to ?? [];
await done(player.agentId, { type: walk ? 'walk' : 'continue', ignore });
}
export async function handleAgentInteraction(
ctx: ActionCtx,
players: Player[],
memory: MemoryDB,
done: DoneFn,
) {
// TODO: pick a better conversation starter
const leader = players[0];
for (const player of players) {
const imWalkingHere =
player.motion.type === 'walking' && player.motion.targetEndTs > Date.now();
// Get players to walk together and face each other
if (player.agentId) {
if (player === leader) {
if (imWalkingHere) {
await ctx.runMutation(internal.journal.stop, {
playerId: player.id,
});
}
} else {
await ctx.runMutation(internal.journal.walk, {
agentId: player.agentId,
target: leader.id,
ignore: players.map((p) => p.id),
});

// How close a player can be to have a conversation.
export const NEARBY_DISTANCE = 3;
// Close enough to stop and observe something.
export const CLOSE_DISTANCE = 1;
// 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 = 20_000;
// 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;

import { cronJobs } from 'convex/server';
import { internalMutation } from './_generated/server';
import { getLatestPlayerMotion } from './journal';
import {
AGENT_THINKING_TOO_LONG,
VACUUM_BATCH_SIZE,
VACUUM_JOURNAL_AGE,
VACUUM_MEMORIES_AGE,
} from './config';
import { enqueueAgentWake } from './engine';
import { internal } from './_generated/api';
import { TableNames } from './_generated/dataModel';
export const recoverThinkingAgents = internalMutation({
args: {},
handler: async (ctx, args) => {
const world = await ctx.db.query('worlds').order('desc').first();
if (!world) throw new Error('No world found');
// Future: we can check all players, but for now just the most recent world.
const ts = Date.now();
const agentDocs = await ctx.db
.query('agents')
.withIndex('by_worldId_thinking', (q) => q.eq('worldId', world._id).eq('thinking', true))
.filter((q) => q.lt(q.field('lastWakeTs'), Date.now() - AGENT_THINKING_TOO_LONG))
.collect();
if (agentDocs.length !== 0) {
// We can just enqueue one, since they're all at the same time.
const scheduled = await enqueueAgentWake(ctx, agentDocs[0]._id, world._id, ts);
for (const agentDoc of agentDocs) {
console.error(`Agent ${agentDoc._id} was thinking too long. Resetting`);
await ctx.db.patch(agentDoc._id, {
thinking: false,
nextWakeTs: ts,
scheduled,
});
}
}
},
});
// Allow 1s for a character to start moving after ending a walk.
const BUFFER = 1_000;
export const recoverStoppedAgents = internalMutation({
args: {},
handler: async (ctx, args) => {
const world = await ctx.db.query('worlds').order('desc').first();
if (!world) throw new Error('No world found');
if (world.frozen) {
console.debug("Didn't tick: world frozen");
return;
}
// Future: we can check all players, but for now just the most recent world.
const agentDocs = await ctx.db
.query('agents')
.withIndex('by_worldId_thinking', (q) => q.eq('worldId', world._id).eq('thinking', false))
.collect();
for (const agentDoc of agentDocs) {
const motion = await getLatestPlayerMotion(ctx.db, agentDoc.playerId);
if (motion.type === 'walking' && motion.targetEndTs < Date.now() - BUFFER) {
console.error("We found a stationary agent that's not thinking. Tick time");
await enqueueAgentWake(ctx, agentDoc._id, world._id, Date.now());
return;
}
}
},
});
export const vacuumOldEntries = internalMutation({
handler: async (
ctx,
{
table,
age,
...args
}: {
table: TableNames;
untilTs?: number;
age: number;
cursor: null | string;
soFar: number;
},
) => {
const untilTs = args.untilTs ?? Date.now() - age;
const results = await ctx.db
.query(table)
.withIndex('by_creation_time', (q) => q.lt('_creationTime', untilTs))
.paginate({ cursor: args.cursor, numItems: VACUUM_BATCH_SIZE });
for (const doc of results.page) {
await ctx.db.delete(doc._id);
}
if (results.isDone) {
console.debug(`Vacuumed ${results.page.length} old ${table} entries.`);

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: Player[],
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 animal = player.name === 'Tilly' ? 'dog' : 'cat';
const prompt: LLMMessage[] = [
{
role: 'user',
content: `You are a ${animal} whose name is ${player.name}. Talk as if you are a ${animal}. Reference information provided to you in the conversation`,
},
{
role: 'user',
content:
`You just saw ${newFriendsNames}. You should greet them and start a conversation with them. Below are some of your memories about ${newFriendsNames}:` +
//relationships.map((r) => r.relationship).join('\n') +
convoMemories.map((r) => r.memory.description).join('\n') +
`\n${player.name}:`,
},
];
const stop = newFriendsNames.map((name) => name + ':');
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} left the conversation.`;
case 'responded':
return `${m.fromName} to ${m.toNames.join(',')}: ${m.content}\n`;
}
}
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(

import { v } from 'convex/values';
import { internal } from './_generated/api';
import { Doc, Id } from './_generated/dataModel';
import { DatabaseReader, DatabaseWriter, MutationCtx, internalMutation } from './_generated/server';
import { TICK_DEBOUNCE, WORLD_IDLE_THRESHOLD } from './config';
import { asyncMap, pruneNull } from './lib/utils';
export const tick = internalMutation({
args: { worldId: v.id('worlds'), noSchedule: v.optional(v.boolean()) },
handler: async (ctx, { worldId, noSchedule }) => {
const ts = Date.now();
// Fetch the first recent heartbeat.
if (!(await getRecentHeartbeat(ctx.db, worldId))) {
console.debug("Didn't tick: no heartbeat recently");
return;
}
const world = await ctx.db.get(worldId);
if (!world) throw new Error("Didn't tick: No world found");
if (world.frozen && !noSchedule) throw new Error("Didn't tick: world frozen");
// Fetch agents to wake up: not already thinking
const agentDocs = await ctx.db
.query('agents')
.withIndex('by_worldId_thinking', (q) => q.eq('worldId', worldId).eq('thinking', false))
.collect();
if (!agentDocs.length) {
console.debug("Didn't tick: all agents thinking");
return;
}
const agentsEagerToWake = agentDocs.filter((a) => a.nextWakeTs && a.nextWakeTs <= ts);
const agentIdsToWake = new Set([
...agentsEagerToWake.flatMap((a) => [a._id, ...(a.alsoWake ?? [])]),
]);
const nextToWake = agentDocs.find((a) => !agentIdsToWake.has(a._id) && a.nextWakeTs > ts);
if (nextToWake && !nextToWake.scheduled) {
await ctx.db.patch(nextToWake._id, { scheduled: true });
await ctx.scheduler.runAt(nextToWake.nextWakeTs, internal.engine.tick, {
worldId,
});
}
if (!agentsEagerToWake.length) {
console.debug("Didn't tick: spurious, no agents eager to wake up");
return;
}
const agentsToWake = pruneNull(await asyncMap(agentIdsToWake, ctx.db.get)).filter(
(a) => !a.thinking,
);
for (const agentDoc of agentsToWake) {
await ctx.db.patch(agentDoc._id, { thinking: true, lastWakeTs: ts });
}
const playerIds = agentsToWake.map((a) => a.playerId);
await ctx.scheduler.runAfter(0, internal.agent.runAgentBatch, { playerIds, noSchedule });
},
});
async function getRecentHeartbeat(db: DatabaseReader, worldId: Id<'worlds'>) {
return (
db
.query('heartbeats')
// Tip: by fetching heartbeats this way, the transaction doesn't conflict
// with new heartbeats being added, assuming it wasn't the first heartbeat
.withIndex('by_creation_time', (q) =>
q.gt('_creationTime', Date.now() - WORLD_IDLE_THRESHOLD),
)
.first()
);
}
export const agentDone = internalMutation({
args: {
agentId: v.id('agents'),
otherAgentIds: v.optional(v.array(v.id('agents'))),
wakeTs: v.number(),
noSchedule: v.optional(v.boolean()),
},
handler: async (ctx, args) => {
const agentDoc = await ctx.db.get(args.agentId);
if (!agentDoc) throw new Error(`Agent ${args.agentId} not found`);
if (!agentDoc.thinking) {
throw new Error('Agent was not thinking: did you call agentDone twice for the same agent?');
}
const nextWakeTs = Math.ceil(args.wakeTs / TICK_DEBOUNCE) * TICK_DEBOUNCE;
await ctx.db.replace(args.agentId, {
playerId: agentDoc.playerId,
worldId: agentDoc.worldId,
thinking: false,
lastWakeTs: agentDoc.nextWakeTs,
nextWakeTs,
alsoWake: args.otherAgentIds,
scheduled: await enqueueAgentWake(
ctx,
args.agentId,
agentDoc.worldId,
nextWakeTs,
args.noSchedule,
),
});
},
});
export async function enqueueAgentWake(
ctx: MutationCtx,
agentId: Id<'agents'>,
worldId: Id<'worlds'>,
atTs: number,
noSchedule?: boolean,
) {
// Future: Debounce wakups by looking 100ms into the future.
const nextScheduled = await ctx.db
.query('agents')
.withIndex('by_worldId_thinking', (q) =>
q.eq('worldId', worldId).eq('thinking', false).lte('nextWakeTs', atTs),
)
.first();
if (nextScheduled) {
if (!nextScheduled.scheduled) {
throw new Error("Next scheduled agent isn't scheduled: " + JSON.stringify(nextScheduled));
}
// We are effectively scheduled since it'll wake up at the same time.
if (nextScheduled.nextWakeTs === atTs) {
return true;
}
// Another agent will be scheduled before us
if (nextScheduled._id !== agentId) {
return false;
}
}
if (!noSchedule) await ctx.scheduler.runAt(atTs, internal.engine.tick, { worldId });
return true;
}
export const freezeAll = internalMutation({
args: {},
handler: async (ctx, args) => {
const worlds = await ctx.db.query('worlds').collect();
for (const world of worlds) {
await ctx.db.patch(world._id, { frozen: true });
}
},
});
export const unfreeze = internalMutation({
args: { worldId: v.optional(v.id('worlds')) },
handler: async (ctx, args) => {
const world = await ctx.db.query('worlds').order('desc').first();
if (!world) throw new Error("Didn't unfreeze: No world found");
await ctx.db.patch(world._id, { frozen: false });
await ctx.scheduler.runAfter(0, internal.engine.tick, { worldId: world._id });
},


Step 2: ⌨️ Coding

  • Modify convex/agent.ts ! No changes made
Modify convex/agent.ts with contents:
• Explain the `handleAgentInteraction` function to the user. This function is responsible for managing the interaction between agents and players. It determines the leader of the conversation and makes the players walk together and face each other.
  • Check convex/agent.ts
Run `convex/agent.ts` through the sandbox.
  • Modify convex/conversation.tsf724484
Modify convex/conversation.ts with contents:
• Explain the `startConversation` function to the user. This function is responsible for initiating a conversation between the agent and the players. It fetches the agent's memories about the players and uses them to generate a conversation prompt.
• Explain the `chatCompletion` function to the user. This function generates the agent's response based on the conversation prompt.
  • Check convex/conversation.ts
Sandbox logs for
trunk fmt convex/conversation.ts || exit 0 1/2 ✓
Checked 1 file
✔ No issues
trunk check --fix --print-failures convex/conversation.ts 2/2 ❌ (`1`)
  ISSUES  
convex/conversation.ts:21:11
  9:6   medium  'Relation' is defined but never used.            eslint/@typescript-eslint/no-unused-vars
 21:11  medium  'embedding' is assigned a value but never used.  eslint/@typescript-eslint/no-unused-vars
Checked 1 file
1 existing issue
✖ 1 new issue
  • Modify convex/conversation.ts0529692
Modify convex/conversation.ts with contents: Remove the unused type 'Relation' from the file. This type is defined but not used anywhere in the code, so it can be safely removed.

Next, remove the unused variable 'embedding'. This variable is assigned a value but is not used anywhere in the code. Therefore, the line of code where 'embedding' is assigned a value can be removed.

  • Check convex/conversation.ts970ea0e
Sandbox logs for https://github.com/ykhli/cat-town/commit/970ea0e3cc8d925e62aad63437ca08ea7fa8a7a2
trunk fmt convex/conversation.ts || exit 0 1/2 ✓
 ✔ Formatted convex/conversation.ts
Re-checking autofixed files...


Checked 1 file
✔ No issues
trunk check --fix --print-failures convex/conversation.ts 2/2 ✓
Checked 1 file
✔ No issues
  • Check convex/conversation.ts
Sandbox logs for
trunk fmt convex/conversation.ts || exit 0 1/2 ✓
Checked 1 file
✔ No issues
trunk check --fix --print-failures convex/conversation.ts 2/2 ✓
Checked 1 file
✔ No issues

Step 3: 🔁 Code Review

I have finished reviewing the code for completeness. I did not find errors for sweep/player-agent-interaction-explanation.


🎉 Latest improvements to Sweep:


💡 To recreate the pull request edit the issue title or description. To tweak the pull request, leave a comment on the pull request.
Join Our Discord

@2642543078
Copy link
Author

我想要在斯坦福小镇里实现这个功能 问下需要添加哪些文件呢 或者说在这个猫镇上哪里的代码是表示这部分的

@sweep-ai sweep-ai bot linked a pull request Nov 14, 2023 that will close this issue
2 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
sweep Assigns Sweep to an issue or pull request.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

1 participant