Skip to content

Commit

Permalink
Merge pull request #383 from ai16z/ropresearch/contextual-threads
Browse files Browse the repository at this point in the history
feat: Contextual Twitter Threads + Spam Reduction
  • Loading branch information
ponderingdemocritus authored Nov 18, 2024
2 parents f107e38 + 578a42a commit 290d7fb
Show file tree
Hide file tree
Showing 7 changed files with 476 additions and 229 deletions.
11 changes: 10 additions & 1 deletion packages/client-telegram/src/messageManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ The goal is to decide whether {{agentName}} should respond to the last message.
{{recentMessages}}
Thread of Tweets You Are Replying To:
{{formattedConversation}}
# INSTRUCTIONS: Choose the option that best describes {{agentName}}'s response to the last message. Ignore messages if they are addressed to someone else.
` + shouldRespondFooter;

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

export class MessageManager {
Expand Down
184 changes: 179 additions & 5 deletions packages/client-twitter/src/interactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
import { stringToUuid } from "@ai16z/eliza";
import { ClientBase } from "./base.ts";
import { buildConversationThread, sendTweet, wait } from "./utils.ts";
import { embeddingZeroVector } from "@ai16z/eliza";

export const twitterMessageHandlerTemplate =
`{{timeline}}
Expand All @@ -38,6 +39,14 @@ Recent interactions between {{agentName}} and other users:
{{recentPosts}}
# Task: Generate a post/reply in the voice, style and perspective of {{agentName}} (@{{twitterUserName}}) while using the thread of tweets as additional context:
Current Post:
{{currentPost}}
Thread of Tweets You Are Replying To:
{{formattedConversation}}
{{actions}}
# Task: Generate a post in the voice, style and perspective of {{agentName}} (@{{twitterUserName}}). Include an action, if appropriate. {{actionNames}}:
Expand All @@ -64,6 +73,10 @@ IMPORTANT: {{agentName}} (aka @{{twitterUserName}}) is particularly sensitive ab
{{currentPost}}
Thread of Tweets You Are Replying To:
{{formattedConversation}}
# 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.
` + shouldRespondFooter;

Expand Down Expand Up @@ -107,6 +120,7 @@ export class TwitterInteractionClient extends ClientBase {

// for each tweet candidate, handle the tweet
for (const tweet of uniqueTweetCandidates) {
// console.log("tweet:", tweet);
if (
!this.lastCheckedTweetId ||
parseInt(tweet.id) > this.lastCheckedTweetId
Expand All @@ -126,7 +140,8 @@ export class TwitterInteractionClient extends ClientBase {
"twitter"
);

await buildConversationThread(tweet, this);
const thread = await buildConversationThread(tweet, this);
console.log("thread", thread);

const message = {
content: { text: tweet.text },
Expand All @@ -138,6 +153,7 @@ export class TwitterInteractionClient extends ClientBase {
await this.handleTweet({
tweet,
message,
thread,
});

// Update the last checked tweet ID after processing each tweet
Expand Down Expand Up @@ -185,9 +201,11 @@ export class TwitterInteractionClient extends ClientBase {
private async handleTweet({
tweet,
message,
thread,
}: {
tweet: Tweet;
message: Memory;
thread: Tweet[];
}) {
if (tweet.username === this.runtime.getSetting("TWITTER_USERNAME")) {
// console.log("skipping tweet from bot itself", tweet.id);
Expand Down Expand Up @@ -221,6 +239,23 @@ export class TwitterInteractionClient extends ClientBase {
);
}

console.log("Thread: ", thread);
const formattedConversation = thread
.map(
(tweet) => `@${tweet.username} (${new Date(
tweet.timestamp * 1000
).toLocaleString("en-US", {
hour: "2-digit",
minute: "2-digit",
month: "short",
day: "numeric",
})}):
${tweet.text}`
)
.join("\n\n");

console.log("formattedConversation: ", formattedConversation);

const formattedHomeTimeline =
`# ${this.runtime.character.name}'s Home Timeline\n\n` +
homeTimeline
Expand All @@ -233,6 +268,7 @@ export class TwitterInteractionClient extends ClientBase {
twitterClient: this.twitterClient,
twitterUserName: this.runtime.getSetting("TWITTER_USERNAME"),
currentPost,
formattedConversation,
timeline: formattedHomeTimeline,
});

Expand Down Expand Up @@ -276,15 +312,18 @@ export class TwitterInteractionClient extends ClientBase {
twitterShouldRespondTemplate,
});

console.log("composeContext done");

const shouldRespond = await generateShouldRespond({
runtime: this.runtime,
context: shouldRespondContext,
modelClass: ModelClass.SMALL,
modelClass: ModelClass.LARGE,
});

if (!shouldRespond) {
// Promise<"RESPOND" | "IGNORE" | "STOP" | null> {
if (shouldRespond !== "RESPOND") {
console.log("Not responding to message");
return { text: "", action: "IGNORE" };
return { text: "Response Decision:", action: shouldRespond };
}

const context = composeContext({
Expand All @@ -299,13 +338,18 @@ export class TwitterInteractionClient extends ClientBase {
const response = await generateMessageResponse({
runtime: this.runtime,
context,
modelClass: ModelClass.SMALL,
modelClass: ModelClass.MEDIUM,
});

const removeQuotes = (str: string) =>
str.replace(/^['"](.*)['"]$/, "$1");

const stringId = stringToUuid(tweet.id + "-" + this.runtime.agentId);

response.inReplyTo = stringId;

response.text = removeQuotes(response.text);

if (response.text) {
try {
const callback: HandlerCallback = async (response: Content) => {
Expand Down Expand Up @@ -359,4 +403,134 @@ export class TwitterInteractionClient extends ClientBase {
}
}
}

async buildConversationThread(
tweet: Tweet,
maxReplies: number = 10
): Promise<Tweet[]> {
const thread: Tweet[] = [];
const visited: Set<string> = new Set();

async function processThread(currentTweet: Tweet, depth: number = 0) {
console.log("Processing tweet:", {
id: currentTweet.id,
inReplyToStatusId: currentTweet.inReplyToStatusId,
depth: depth,
});

if (!currentTweet) {
console.log("No current tweet found for thread building");
return;
}

if (depth >= maxReplies) {
console.log("Reached maximum reply depth", depth);
return;
}

// Handle memory storage
const memory = await this.runtime.messageManager.getMemoryById(
stringToUuid(currentTweet.id + "-" + this.runtime.agentId)
);
if (!memory) {
const roomId = stringToUuid(
currentTweet.conversationId + "-" + this.runtime.agentId
);
const userId = stringToUuid(currentTweet.userId);

await this.runtime.ensureConnection(
userId,
roomId,
currentTweet.username,
currentTweet.name,
"twitter"
);

this.runtime.messageManager.createMemory({
id: stringToUuid(
currentTweet.id + "-" + this.runtime.agentId
),
agentId: this.runtime.agentId,
content: {
text: currentTweet.text,
source: "twitter",
url: currentTweet.permanentUrl,
inReplyTo: currentTweet.inReplyToStatusId
? stringToUuid(
currentTweet.inReplyToStatusId +
"-" +
this.runtime.agentId
)
: undefined,
},
createdAt: currentTweet.timestamp * 1000,
roomId,
userId:
currentTweet.userId === this.twitterUserId
? this.runtime.agentId
: stringToUuid(currentTweet.userId),
embedding: embeddingZeroVector,
});
}

if (visited.has(currentTweet.id)) {
console.log("Already visited tweet:", currentTweet.id);
return;
}

visited.add(currentTweet.id);
thread.unshift(currentTweet);

console.log("Current thread state:", {
length: thread.length,
currentDepth: depth,
tweetId: currentTweet.id,
});

if (currentTweet.inReplyToStatusId) {
console.log(
"Fetching parent tweet:",
currentTweet.inReplyToStatusId
);
try {
const parentTweet = await this.twitterClient.getTweet(
currentTweet.inReplyToStatusId
);

if (parentTweet) {
console.log("Found parent tweet:", {
id: parentTweet.id,
text: parentTweet.text?.slice(0, 50),
});
await processThread(parentTweet, depth + 1);
} else {
console.log(
"No parent tweet found for:",
currentTweet.inReplyToStatusId
);
}
} catch (error) {
console.log("Error fetching parent tweet:", {
tweetId: currentTweet.inReplyToStatusId,
error,
});
}
} else {
console.log("Reached end of reply chain at:", currentTweet.id);
}
}

// Need to bind this context for the inner function
await processThread.bind(this)(tweet, 0);

console.log("Final thread built:", {
totalTweets: thread.length,
tweetIds: thread.map((t) => ({
id: t.id,
text: t.text?.slice(0, 50),
})),
});

return thread;
}
}
Loading

0 comments on commit 290d7fb

Please sign in to comment.