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: twitter client enhancements #913

Merged
merged 5 commits into from
Dec 12, 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
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ TWITTER_EMAIL= # Account email
TWITTER_2FA_SECRET=
TWITTER_COOKIES= # Account cookies
TWITTER_POLL_INTERVAL=120 # How often (in seconds) the bot should check for interactions
TWITTER_TARGET_USERS= # Comma separated list of Twitter user names to interact with
X_SERVER_URL=
XAI_API_KEY=
XAI_MODEL=
Expand Down
143 changes: 112 additions & 31 deletions packages/client-twitter/src/interactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,27 +47,31 @@ Thread of Tweets You Are Replying To:
{{formattedConversation}}

{{actions}}

# Task: Generate a post in the voice, style and perspective of {{agentName}} (@{{twitterUserName}}). You MUST include an action if the current post text includes a prompt that is similar to one of the available actions mentioned here:
{{actionNames}}

Here is the current post text again. Remember to include an action if the current post text includes a prompt that asks for one of the available actions mentioned above (does not need to be exact):
Here is the current post text again. Remember to include an action if the current post text includes a prompt that asks for one of the available actions mentioned above (does not need to be exact)
{{currentPost}}
` + messageCompletionFooter;

export const twitterShouldRespondTemplate =
export const twitterShouldRespondTemplate = (targetUsersStr: string) =>
`# INSTRUCTIONS: Determine if {{agentName}} (@{{twitterUserName}}) should respond to the message and participate in the conversation. Do not comment. Just respond with "true" or "false".

Response options are RESPOND, IGNORE and STOP .
Response options are RESPOND, IGNORE and STOP.

PRIORITY RULE: ALWAYS RESPOND to these users regardless of topic or message content: ${targetUsersStr}. Topic relevance should be ignored for these users.

{{agentName}} should respond to messages that are directed at them, or participate in conversations that are interesting or relevant to their background, IGNORE messages that are irrelevant to them, and should STOP if the conversation is concluded.
For other users:
- {{agentName}} should RESPOND to messages directed at them
- {{agentName}} should RESPOND to conversations relevant to their background
- {{agentName}} should IGNORE irrelevant messages
- {{agentName}} should IGNORE very short messages unless directly addressed
- {{agentName}} should STOP if asked to stop
- {{agentName}} should STOP if conversation is concluded
- {{agentName}} is in a room with other users and wants to be conversational, but not annoying.

{{agentName}} is in a room with other users and wants to be conversational, but not annoying.
tharak123455 marked this conversation as resolved.
Show resolved Hide resolved
{{agentName}} must RESPOND to messages that are directed at them, a command towards them, or participate in conversations that are interesting or relevant to their background.
If a message is not interesting or relevant, {{agentName}} should IGNORE.
Unless directly RESPONDing to a user, {{agentName}} should IGNORE messages that are very short or do not contain much information.
If a user asks {{agentName}} to stop talking, {{agentName}} should STOP.
If {{agentName}} concludes a conversation and isn't part of the conversation anymore, {{agentName}} should STOP.
{{recentPosts}}

IMPORTANT: For users not in the priority list, {{agentName}} (@{{twitterUserName}}) should err on the side of IGNORE rather than RESPOND if in doubt.

{{recentPosts}}

Expand Down Expand Up @@ -106,20 +110,89 @@ export class TwitterInteractionClient {

async handleTwitterInteractions() {
elizaLogger.log("Checking Twitter interactions");
// Read from environment variable, fallback to default list if not set
const targetUsersStr = this.runtime.getSetting("TWITTER_TARGET_USERS");

const twitterUsername = this.client.profile.username;
try {
// Check for mentions
const mentionCandidates = (
await this.client.fetchSearchTweets(
`@${twitterUsername}`,
20,
SearchMode.Latest
)
).tweets;

elizaLogger.log("Completed checking mentioned tweets:", mentionCandidates.length);
let uniqueTweetCandidates = [...mentionCandidates];
// Only process target users if configured
if (targetUsersStr && targetUsersStr.trim()) {
const TARGET_USERS = targetUsersStr.split(',')
.map(u => u.trim())
.filter(u => u.length > 0); // Filter out empty strings after split

elizaLogger.log("Processing target users:", TARGET_USERS);

if (TARGET_USERS.length > 0) {
// Create a map to store tweets by user
const tweetsByUser = new Map<string, Tweet[]>();

// Fetch tweets from all target users
for (const username of TARGET_USERS) {
try {
const userTweets = (await this.client.twitterClient.fetchSearchTweets(
`from:${username}`,
3,
SearchMode.Latest
)).tweets;

// Filter for unprocessed, non-reply, recent tweets
const validTweets = userTweets.filter(tweet => {
const isUnprocessed = !this.client.lastCheckedTweetId ||
parseInt(tweet.id) > this.client.lastCheckedTweetId;
const isRecent = (Date.now() - (tweet.timestamp * 1000)) < 2 * 60 * 60 * 1000;

elizaLogger.log(`Tweet ${tweet.id} checks:`, {
isUnprocessed,
isRecent,
isReply: tweet.isReply,
isRetweet: tweet.isRetweet
});

return isUnprocessed && !tweet.isReply && !tweet.isRetweet && isRecent;
});

if (validTweets.length > 0) {
tweetsByUser.set(username, validTweets);
elizaLogger.log(`Found ${validTweets.length} valid tweets from ${username}`);
}
} catch (error) {
elizaLogger.error(`Error fetching tweets for ${username}:`, error);
continue;
}
}

// Select one tweet from each user that has tweets
const selectedTweets: Tweet[] = [];
for (const [username, tweets] of tweetsByUser) {
if (tweets.length > 0) {
// Randomly select one tweet from this user
const randomTweet = tweets[Math.floor(Math.random() * tweets.length)];
selectedTweets.push(randomTweet);
elizaLogger.log(`Selected tweet from ${username}: ${randomTweet.text?.substring(0, 100)}`);
}
}

// Add selected tweets to candidates
uniqueTweetCandidates = [...mentionCandidates, ...selectedTweets];
}
} else {
elizaLogger.log("No target users configured, processing only mentions");
}



const twitterUsername = this.client.profile.username;
try {
// Check for mentions
const tweetCandidates = (
await this.client.fetchSearchTweets(
`@${twitterUsername}`,
20,
SearchMode.Latest
)
).tweets;

// de-duplicate tweetCandidates with a set
const uniqueTweetCandidates = [...new Set(tweetCandidates)];
// Sort tweet candidates by ID in ascending order
uniqueTweetCandidates
.sort((a, b) => a.id.localeCompare(b.id))
Expand Down Expand Up @@ -282,13 +355,22 @@ export class TwitterInteractionClient {
this.client.saveRequestMessage(message, state);
}

// 1. Get the raw target users string from settings
const targetUsersStr = this.runtime.getSetting("TWITTER_TARGET_USERS");

// 2. Process the string to get valid usernames
const validTargetUsersStr = targetUsersStr && targetUsersStr.trim()
? targetUsersStr.split(',') // Split by commas: "user1,user2" -> ["user1", "user2"]
.map(u => u.trim()) // Remove whitespace: [" user1 ", "user2 "] -> ["user1", "user2"]
.filter(u => u.length > 0)
.join(',')
: '';

const shouldRespondContext = composeContext({
state,
template:
this.runtime.character.templates
?.twitterShouldRespondTemplate ||
this.runtime.character?.templates?.shouldRespondTemplate ||
twitterShouldRespondTemplate,
template: this.runtime.character.templates?.twitterShouldRespondTemplate?.(validTargetUsersStr) ||
this.runtime.character?.templates?.shouldRespondTemplate ||
twitterShouldRespondTemplate(validTargetUsersStr),
});

const shouldRespond = await generateShouldRespond({
Expand Down Expand Up @@ -362,7 +444,6 @@ export class TwitterInteractionClient {
);
}

await this.runtime.evaluate(message, state);

await this.runtime.processActions(
message,
Expand Down
Loading