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

feat: Contextual Twitter Threads + Spam Reduction #383

Merged
merged 4 commits into from
Nov 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -278,15 +314,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 @@ -301,13 +340,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 @@ -361,4 +405,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