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 Refactor #478

Merged
merged 9 commits into from
Nov 21, 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
249 changes: 118 additions & 131 deletions packages/client-twitter/src/base.ts

Large diffs are not rendered by default.

18 changes: 14 additions & 4 deletions packages/client-twitter/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,35 @@ import { TwitterPostClient } from "./post.ts";
import { TwitterSearchClient } from "./search.ts";
import { TwitterInteractionClient } from "./interactions.ts";
import { IAgentRuntime, Client, elizaLogger } from "@ai16z/eliza";
import { ClientBase } from "./base.ts";

class TwitterAllClient {
class TwitterManager {
client: ClientBase;
post: TwitterPostClient;
search: TwitterSearchClient;
interaction: TwitterInteractionClient;
constructor(runtime: IAgentRuntime) {
this.post = new TwitterPostClient(runtime);
this.client = new ClientBase(runtime);
this.post = new TwitterPostClient(this.client, runtime);
// this.search = new TwitterSearchClient(runtime); // don't start the search client by default
// this searches topics from character file, but kind of violates consent of random users
// burns your rate limit and can get your account banned
// use at your own risk
this.interaction = new TwitterInteractionClient(runtime);
this.interaction = new TwitterInteractionClient(this.client, runtime);
}
}

export const TwitterClientInterface: Client = {
async start(runtime: IAgentRuntime) {
elizaLogger.log("Twitter client started");
return new TwitterAllClient(runtime);
const manager = new TwitterManager(runtime);

await manager.client.init();

await manager.post.start();
await manager.interaction.start();

return manager;
},
async stop(runtime: IAgentRuntime) {
elizaLogger.warn("Twitter client does not support stopping yet");
Expand Down
93 changes: 53 additions & 40 deletions packages/client-twitter/src/interactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
stringToUuid,
elizaLogger,
} from "@ai16z/eliza";
import { ClientBase } from "./base.ts";
import { ClientBase } from "./base";
import { buildConversationThread, sendTweet, wait } from "./utils.ts";
import { embeddingZeroVector } from "@ai16z/eliza";

Expand Down Expand Up @@ -82,8 +82,16 @@ Thread of Tweets You Are Replying To:
# 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;

export class TwitterInteractionClient extends ClientBase {
onReady() {
export class TwitterInteractionClient {
client: ClientBase;
runtime: IAgentRuntime;

constructor(client: ClientBase, runtime: IAgentRuntime) {
this.client = client;
this.runtime = runtime;
}

async start() {
const handleTwitterInteractionsLoop = () => {
this.handleTwitterInteractions();
setTimeout(
Expand All @@ -94,45 +102,43 @@ export class TwitterInteractionClient extends ClientBase {
handleTwitterInteractionsLoop();
}

constructor(runtime: IAgentRuntime) {
super({
runtime,
});
}

async handleTwitterInteractions() {
elizaLogger.log("Checking Twitter interactions");

const twitterUsername = this.client.profile.username;
try {
// Check for mentions
const tweetCandidates = (
await this.fetchSearchTweets(
`@${this.runtime.getSetting("TWITTER_USERNAME")}`,
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))
.filter((tweet) => tweet.userId !== this.twitterUserId);
.filter((tweet) => tweet.userId !== this.client.profile.id);

// for each tweet candidate, handle the tweet
for (const tweet of uniqueTweetCandidates) {
// console.log("tweet:", tweet);
if (
!this.lastCheckedTweetId ||
parseInt(tweet.id) > this.lastCheckedTweetId
!this.client.lastCheckedTweetId ||
parseInt(tweet.id) > this.client.lastCheckedTweetId
) {
const conversationId =
tweet.conversationId + "-" + this.runtime.agentId;
elizaLogger.log("New Tweet found", tweet.permanentUrl);

const roomId = stringToUuid(conversationId);
const roomId = stringToUuid(
tweet.conversationId + "-" + this.runtime.agentId
);

const userIdUUID = stringToUuid(tweet.userId as string);
const userIdUUID =
tweet.userId === this.client.profile.id
? this.runtime.agentId
: stringToUuid(tweet.userId!);

await this.runtime.ensureConnection(
userIdUUID,
Expand All @@ -142,7 +148,10 @@ export class TwitterInteractionClient extends ClientBase {
"twitter"
);

const thread = await buildConversationThread(tweet, this);
const thread = await buildConversationThread(
tweet,
this.client
);

const message = {
content: { text: tweet.text },
Expand All @@ -158,12 +167,12 @@ export class TwitterInteractionClient extends ClientBase {
});

// Update the last checked tweet ID after processing each tweet
this.lastCheckedTweetId = parseInt(tweet.id);
this.client.lastCheckedTweetId = parseInt(tweet.id);
}
}

// Save the latest checked tweet ID to the file
await this.cacheLatestCheckedTweetId();
await this.client.cacheLatestCheckedTweetId();

elizaLogger.log("Finished checking Twitter interactions");
} catch (error) {
Expand All @@ -180,17 +189,18 @@ export class TwitterInteractionClient extends ClientBase {
message: Memory;
thread: Tweet[];
}) {
if (tweet.username === this.runtime.getSetting("TWITTER_USERNAME")) {
if (tweet.userId === this.client.profile.id) {
// console.log("skipping tweet from bot itself", tweet.id);
// Skip processing if the tweet is from the bot itself
return;
}

if (!message.content.text) {
elizaLogger.log("skipping tweet with no text", tweet.id);
elizaLogger.log("Skipping Tweet with no text", tweet.id);
return { text: "", action: "IGNORE" };
}
elizaLogger.log("handling tweet", tweet.id);

elizaLogger.log("Processing Tweet: ", tweet.id);
const formatTweet = (tweet: Tweet) => {
return ` ID: ${tweet.id}
From: ${tweet.name} (@${tweet.username})
Expand All @@ -201,12 +211,12 @@ export class TwitterInteractionClient extends ClientBase {
let homeTimeline: Tweet[] = [];
// read the file if it exists

const cachedTimeline = await this.getCachedTimeline();
const cachedTimeline = await this.client.getCachedTimeline();
if (cachedTimeline) {
homeTimeline = cachedTimeline;
} else {
homeTimeline = await this.fetchHomeTimeline(50);
await this.cacheTimeline(homeTimeline);
homeTimeline = await this.client.fetchHomeTimeline(50);
await this.client.cacheTimeline(homeTimeline);
}

elizaLogger.debug("Thread: ", thread);
Expand Down Expand Up @@ -235,7 +245,7 @@ export class TwitterInteractionClient extends ClientBase {
.join("\n");

let state = await this.runtime.composeState(message, {
twitterClient: this.twitterClient,
twitterClient: this.client.twitterClient,
twitterUserName: this.runtime.getSetting("TWITTER_USERNAME"),
currentPost,
formattedConversation,
Expand Down Expand Up @@ -270,7 +280,7 @@ export class TwitterInteractionClient extends ClientBase {
roomId,
createdAt: tweet.timestamp * 1000,
};
this.saveRequestMessage(message, state);
this.client.saveRequestMessage(message, state);
}

const shouldRespondContext = composeContext({
Expand Down Expand Up @@ -322,7 +332,7 @@ export class TwitterInteractionClient extends ClientBase {
try {
const callback: HandlerCallback = async (response: Content) => {
const memories = await sendTweet(
this,
this.client,
response,
message.roomId,
this.runtime.getSetting("TWITTER_USERNAME"),
Expand Down Expand Up @@ -380,19 +390,19 @@ export class TwitterInteractionClient extends ClientBase {
const visited: Set<string> = new Set();

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

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

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

Expand Down Expand Up @@ -456,7 +466,7 @@ export class TwitterInteractionClient extends ClientBase {
});

if (currentTweet.inReplyToStatusId) {
console.log(
elizaLogger.log(
"Fetching parent tweet:",
currentTweet.inReplyToStatusId
);
Expand All @@ -466,25 +476,28 @@ export class TwitterInteractionClient extends ClientBase {
);

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

Expand Down
Loading