Skip to content

Commit 290d7fb

Browse files
Merge pull request #383 from ai16z/ropresearch/contextual-threads
feat: Contextual Twitter Threads + Spam Reduction
2 parents f107e38 + 578a42a commit 290d7fb

File tree

7 files changed

+476
-229
lines changed

7 files changed

+476
-229
lines changed

packages/client-telegram/src/messageManager.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ The goal is to decide whether {{agentName}} should respond to the last message.
8989
9090
{{recentMessages}}
9191
92+
Thread of Tweets You Are Replying To:
93+
94+
{{formattedConversation}}
95+
9296
# INSTRUCTIONS: Choose the option that best describes {{agentName}}'s response to the last message. Ignore messages if they are addressed to someone else.
9397
` + shouldRespondFooter;
9498

@@ -122,7 +126,12 @@ Note that {{agentName}} is capable of reading/seeing/hearing various forms of me
122126
123127
{{recentMessages}}
124128
125-
# Instructions: Write the next message for {{agentName}}. Include an action, if appropriate. {{actionNames}}
129+
# Task: Generate a post/reply in the voice, style and perspective of {{agentName}} (@{{twitterUserName}}) while using the thread of tweets as additional context:
130+
Current Post:
131+
{{currentPost}}
132+
Thread of Tweets You Are Replying To:
133+
134+
{{formattedConversation}}
126135
` + messageCompletionFooter;
127136

128137
export class MessageManager {

packages/client-twitter/src/interactions.ts

+179-5
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
import { stringToUuid } from "@ai16z/eliza";
1515
import { ClientBase } from "./base.ts";
1616
import { buildConversationThread, sendTweet, wait } from "./utils.ts";
17+
import { embeddingZeroVector } from "@ai16z/eliza";
1718

1819
export const twitterMessageHandlerTemplate =
1920
`{{timeline}}
@@ -38,6 +39,14 @@ Recent interactions between {{agentName}} and other users:
3839
3940
{{recentPosts}}
4041
42+
43+
# Task: Generate a post/reply in the voice, style and perspective of {{agentName}} (@{{twitterUserName}}) while using the thread of tweets as additional context:
44+
Current Post:
45+
{{currentPost}}
46+
Thread of Tweets You Are Replying To:
47+
48+
{{formattedConversation}}
49+
4150
{{actions}}
4251
4352
# Task: Generate a post in the voice, style and perspective of {{agentName}} (@{{twitterUserName}}). Include an action, if appropriate. {{actionNames}}:
@@ -64,6 +73,10 @@ IMPORTANT: {{agentName}} (aka @{{twitterUserName}}) is particularly sensitive ab
6473
6574
{{currentPost}}
6675
76+
Thread of Tweets You Are Replying To:
77+
78+
{{formattedConversation}}
79+
6780
# INSTRUCTIONS: Respond with [RESPOND] if {{agentName}} should respond, or [IGNORE] if {{agentName}} should not respond to the last message and [STOP] if {{agentName}} should stop participating in the conversation.
6881
` + shouldRespondFooter;
6982

@@ -107,6 +120,7 @@ export class TwitterInteractionClient extends ClientBase {
107120

108121
// for each tweet candidate, handle the tweet
109122
for (const tweet of uniqueTweetCandidates) {
123+
// console.log("tweet:", tweet);
110124
if (
111125
!this.lastCheckedTweetId ||
112126
parseInt(tweet.id) > this.lastCheckedTweetId
@@ -126,7 +140,8 @@ export class TwitterInteractionClient extends ClientBase {
126140
"twitter"
127141
);
128142

129-
await buildConversationThread(tweet, this);
143+
const thread = await buildConversationThread(tweet, this);
144+
console.log("thread", thread);
130145

131146
const message = {
132147
content: { text: tweet.text },
@@ -138,6 +153,7 @@ export class TwitterInteractionClient extends ClientBase {
138153
await this.handleTweet({
139154
tweet,
140155
message,
156+
thread,
141157
});
142158

143159
// Update the last checked tweet ID after processing each tweet
@@ -185,9 +201,11 @@ export class TwitterInteractionClient extends ClientBase {
185201
private async handleTweet({
186202
tweet,
187203
message,
204+
thread,
188205
}: {
189206
tweet: Tweet;
190207
message: Memory;
208+
thread: Tweet[];
191209
}) {
192210
if (tweet.username === this.runtime.getSetting("TWITTER_USERNAME")) {
193211
// console.log("skipping tweet from bot itself", tweet.id);
@@ -221,6 +239,23 @@ export class TwitterInteractionClient extends ClientBase {
221239
);
222240
}
223241

242+
console.log("Thread: ", thread);
243+
const formattedConversation = thread
244+
.map(
245+
(tweet) => `@${tweet.username} (${new Date(
246+
tweet.timestamp * 1000
247+
).toLocaleString("en-US", {
248+
hour: "2-digit",
249+
minute: "2-digit",
250+
month: "short",
251+
day: "numeric",
252+
})}):
253+
${tweet.text}`
254+
)
255+
.join("\n\n");
256+
257+
console.log("formattedConversation: ", formattedConversation);
258+
224259
const formattedHomeTimeline =
225260
`# ${this.runtime.character.name}'s Home Timeline\n\n` +
226261
homeTimeline
@@ -233,6 +268,7 @@ export class TwitterInteractionClient extends ClientBase {
233268
twitterClient: this.twitterClient,
234269
twitterUserName: this.runtime.getSetting("TWITTER_USERNAME"),
235270
currentPost,
271+
formattedConversation,
236272
timeline: formattedHomeTimeline,
237273
});
238274

@@ -276,15 +312,18 @@ export class TwitterInteractionClient extends ClientBase {
276312
twitterShouldRespondTemplate,
277313
});
278314

315+
console.log("composeContext done");
316+
279317
const shouldRespond = await generateShouldRespond({
280318
runtime: this.runtime,
281319
context: shouldRespondContext,
282-
modelClass: ModelClass.SMALL,
320+
modelClass: ModelClass.LARGE,
283321
});
284322

285-
if (!shouldRespond) {
323+
// Promise<"RESPOND" | "IGNORE" | "STOP" | null> {
324+
if (shouldRespond !== "RESPOND") {
286325
console.log("Not responding to message");
287-
return { text: "", action: "IGNORE" };
326+
return { text: "Response Decision:", action: shouldRespond };
288327
}
289328

290329
const context = composeContext({
@@ -299,13 +338,18 @@ export class TwitterInteractionClient extends ClientBase {
299338
const response = await generateMessageResponse({
300339
runtime: this.runtime,
301340
context,
302-
modelClass: ModelClass.SMALL,
341+
modelClass: ModelClass.MEDIUM,
303342
});
304343

344+
const removeQuotes = (str: string) =>
345+
str.replace(/^['"](.*)['"]$/, "$1");
346+
305347
const stringId = stringToUuid(tweet.id + "-" + this.runtime.agentId);
306348

307349
response.inReplyTo = stringId;
308350

351+
response.text = removeQuotes(response.text);
352+
309353
if (response.text) {
310354
try {
311355
const callback: HandlerCallback = async (response: Content) => {
@@ -359,4 +403,134 @@ export class TwitterInteractionClient extends ClientBase {
359403
}
360404
}
361405
}
406+
407+
async buildConversationThread(
408+
tweet: Tweet,
409+
maxReplies: number = 10
410+
): Promise<Tweet[]> {
411+
const thread: Tweet[] = [];
412+
const visited: Set<string> = new Set();
413+
414+
async function processThread(currentTweet: Tweet, depth: number = 0) {
415+
console.log("Processing tweet:", {
416+
id: currentTweet.id,
417+
inReplyToStatusId: currentTweet.inReplyToStatusId,
418+
depth: depth,
419+
});
420+
421+
if (!currentTweet) {
422+
console.log("No current tweet found for thread building");
423+
return;
424+
}
425+
426+
if (depth >= maxReplies) {
427+
console.log("Reached maximum reply depth", depth);
428+
return;
429+
}
430+
431+
// Handle memory storage
432+
const memory = await this.runtime.messageManager.getMemoryById(
433+
stringToUuid(currentTweet.id + "-" + this.runtime.agentId)
434+
);
435+
if (!memory) {
436+
const roomId = stringToUuid(
437+
currentTweet.conversationId + "-" + this.runtime.agentId
438+
);
439+
const userId = stringToUuid(currentTweet.userId);
440+
441+
await this.runtime.ensureConnection(
442+
userId,
443+
roomId,
444+
currentTweet.username,
445+
currentTweet.name,
446+
"twitter"
447+
);
448+
449+
this.runtime.messageManager.createMemory({
450+
id: stringToUuid(
451+
currentTweet.id + "-" + this.runtime.agentId
452+
),
453+
agentId: this.runtime.agentId,
454+
content: {
455+
text: currentTweet.text,
456+
source: "twitter",
457+
url: currentTweet.permanentUrl,
458+
inReplyTo: currentTweet.inReplyToStatusId
459+
? stringToUuid(
460+
currentTweet.inReplyToStatusId +
461+
"-" +
462+
this.runtime.agentId
463+
)
464+
: undefined,
465+
},
466+
createdAt: currentTweet.timestamp * 1000,
467+
roomId,
468+
userId:
469+
currentTweet.userId === this.twitterUserId
470+
? this.runtime.agentId
471+
: stringToUuid(currentTweet.userId),
472+
embedding: embeddingZeroVector,
473+
});
474+
}
475+
476+
if (visited.has(currentTweet.id)) {
477+
console.log("Already visited tweet:", currentTweet.id);
478+
return;
479+
}
480+
481+
visited.add(currentTweet.id);
482+
thread.unshift(currentTweet);
483+
484+
console.log("Current thread state:", {
485+
length: thread.length,
486+
currentDepth: depth,
487+
tweetId: currentTweet.id,
488+
});
489+
490+
if (currentTweet.inReplyToStatusId) {
491+
console.log(
492+
"Fetching parent tweet:",
493+
currentTweet.inReplyToStatusId
494+
);
495+
try {
496+
const parentTweet = await this.twitterClient.getTweet(
497+
currentTweet.inReplyToStatusId
498+
);
499+
500+
if (parentTweet) {
501+
console.log("Found parent tweet:", {
502+
id: parentTweet.id,
503+
text: parentTweet.text?.slice(0, 50),
504+
});
505+
await processThread(parentTweet, depth + 1);
506+
} else {
507+
console.log(
508+
"No parent tweet found for:",
509+
currentTweet.inReplyToStatusId
510+
);
511+
}
512+
} catch (error) {
513+
console.log("Error fetching parent tweet:", {
514+
tweetId: currentTweet.inReplyToStatusId,
515+
error,
516+
});
517+
}
518+
} else {
519+
console.log("Reached end of reply chain at:", currentTweet.id);
520+
}
521+
}
522+
523+
// Need to bind this context for the inner function
524+
await processThread.bind(this)(tweet, 0);
525+
526+
console.log("Final thread built:", {
527+
totalTweets: thread.length,
528+
tweetIds: thread.map((t) => ({
529+
id: t.id,
530+
text: t.text?.slice(0, 50),
531+
})),
532+
});
533+
534+
return thread;
535+
}
362536
}

0 commit comments

Comments
 (0)