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: handle long tweet #1339

Merged
merged 8 commits into from
Dec 27, 2024
Merged
Changes from 2 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
308 changes: 206 additions & 102 deletions packages/client-twitter/src/post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
ModelClass,
stringToUuid,
parseBooleanFromText,
UUID,
} from "@ai16z/eliza";
import { elizaLogger } from "@ai16z/eliza";
import { ClientBase } from "./base.ts";
Expand Down Expand Up @@ -71,25 +72,26 @@ function truncateToCompleteSentence(
}

// Attempt to truncate at the last period within the limit
const truncatedAtPeriod = text.slice(
0,
text.lastIndexOf(".", maxTweetLength) + 1
);
if (truncatedAtPeriod.trim().length > 0) {
return truncatedAtPeriod.trim();
const lastPeriodIndex = text.lastIndexOf(".", maxTweetLength - 1);
if (lastPeriodIndex !== -1) {
const truncatedAtPeriod = text.slice(0, lastPeriodIndex + 1).trim();
if (truncatedAtPeriod.length > 0) {
return truncatedAtPeriod;
}
}

// If no period is found, truncate to the nearest whitespace
const truncatedAtSpace = text.slice(
0,
text.lastIndexOf(" ", maxTweetLength)
);
if (truncatedAtSpace.trim().length > 0) {
return truncatedAtSpace.trim() + "...";
// If no period, truncate to the nearest whitespace within the limit
const lastSpaceIndex = text.lastIndexOf(" ", maxTweetLength - 1);
if (lastSpaceIndex !== -1) {
const truncatedAtSpace = text.slice(0, lastSpaceIndex).trim();
if (truncatedAtSpace.length > 0) {
return truncatedAtSpace + "...";
}
}

// Fallback: Hard truncate and add ellipsis
return text.slice(0, maxTweetLength - 3).trim() + "...";
const hardTruncated = text.slice(0, maxTweetLength - 3).trim();
return hardTruncated + "...";
}

export class TwitterPostClient {
Expand Down Expand Up @@ -174,7 +176,8 @@ export class TwitterPostClient {
generateNewTweetLoop();

// Add check for ENABLE_ACTION_PROCESSING before starting the loop
const enableActionProcessing = this.runtime.getSetting("ENABLE_ACTION_PROCESSING") ?? false;
const enableActionProcessing =
this.runtime.getSetting("ENABLE_ACTION_PROCESSING") ?? false;

if (enableActionProcessing) {
processActionsLoop().catch((error) => {
Expand All @@ -186,7 +189,6 @@ export class TwitterPostClient {
} else {
elizaLogger.log("Action processing loop disabled by configuration");
}
generateNewTweetLoop();
}

constructor(client: ClientBase, runtime: IAgentRuntime) {
Expand All @@ -195,6 +197,169 @@ export class TwitterPostClient {
this.twitterUsername = runtime.getSetting("TWITTER_USERNAME");
}

createTweetObject(
tweetResult: any,
client: any,
twitterUsername: string
): Tweet {
return {
id: tweetResult.rest_id,
name: client.profile.screenName,
username: client.profile.username,
text: tweetResult.legacy.full_text,
conversationId: tweetResult.legacy.conversation_id_str,
createdAt: tweetResult.legacy.created_at,
timestamp: new Date(tweetResult.legacy.created_at).getTime(),
userId: client.profile.id,
inReplyToStatusId: tweetResult.legacy.in_reply_to_status_id_str,
permanentUrl: `https://twitter.com/${twitterUsername}/status/${tweetResult.rest_id}`,
hashtags: [],
mentions: [],
photos: [],
thread: [],
urls: [],
videos: [],
} as Tweet;
}

async processAndCacheTweet(
runtime: IAgentRuntime,
client: ClientBase,
tweet: Tweet,
roomId: UUID,
newTweetContent: string
) {
// Cache the last post details
await runtime.cacheManager.set(
`twitter/${client.profile.username}/lastPost`,
{
id: tweet.id,
timestamp: Date.now(),
}
);

// Cache the tweet
await client.cacheTweet(tweet);

// Log the posted tweet
elizaLogger.log(`Tweet posted:\n ${tweet.permanentUrl}`);

// Ensure the room and participant exist
await runtime.ensureRoomExists(roomId);
await runtime.ensureParticipantInRoom(runtime.agentId, roomId);

// Create a memory for the tweet
await runtime.messageManager.createMemory({
id: stringToUuid(tweet.id + "-" + runtime.agentId),
userId: runtime.agentId,
agentId: runtime.agentId,
content: {
text: newTweetContent.trim(),
url: tweet.permanentUrl,
source: "twitter",
},
roomId,
embedding: getEmbeddingZeroVector(),
createdAt: tweet.timestamp,
});
}

async handleNoteTweet(
client: ClientBase,
runtime: IAgentRuntime,
content: string,
tweetId?: string
) {
try {
const noteTweetResult = await client.requestQueue.add(
async () =>
await client.twitterClient.sendNoteTweet(content, tweetId)
);

if (noteTweetResult.errors && noteTweetResult.errors.length > 0) {
// Note Tweet failed due to authorization. Falling back to standard Tweet.
const truncateContent = truncateToCompleteSentence(
content,
parseInt(runtime.getSetting("MAX_TWEET_LENGTH")) ||
DEFAULT_MAX_TWEET_LENGTH
);
return await this.sendStandardTweet(
client,
truncateContent,
tweetId
);
} else {
return noteTweetResult.data.notetweet_create.tweet_results
.result;
}
} catch (error) {
throw new Error(`Note Tweet failed: ${error}`);
}
}

async sendStandardTweet(
client: ClientBase,
content: string,
tweetId?: string
) {
try {
const standardTweetResult = await client.requestQueue.add(
async () =>
await client.twitterClient.sendTweet(content, tweetId)
);
const body = await standardTweetResult.json();
if (!body?.data?.create_tweet?.tweet_results?.result) {
console.error("Error sending tweet; Bad response:", body);
return;
}
return body.data.create_tweet.tweet_results.result;
} catch (error) {
elizaLogger.error("Error sending standard Tweet:", error);
throw error;
}
}

async postTweet(
runtime: IAgentRuntime,
client: ClientBase,
cleanedContent: string,
roomId: UUID,
newTweetContent: string,
twitterUsername: string
) {
try {
elizaLogger.log(`Posting new tweet:\n`);

let result;

if (cleanedContent.length > DEFAULT_MAX_TWEET_LENGTH) {
result = await this.handleNoteTweet(
client,
runtime,
cleanedContent
);
} else {
result = await this.sendStandardTweet(client, cleanedContent);
}

const tweet = this.createTweetObject(
result,
client,
twitterUsername
);

await this.processAndCacheTweet(
runtime,
client,
tweet,
roomId,
newTweetContent
);
} catch (error) {
elizaLogger.error("Error sending tweet111111111:", error);
}
}

private async generateNewTweet() {
elizaLogger.log("Generating new tweet");

Expand Down Expand Up @@ -233,8 +398,6 @@ export class TwitterPostClient {
twitterPostTemplate,
});

console.log("twitter context:\n" + context);

elizaLogger.debug("generate post prompt:\n" + context);

const newTweetContent = await generateText({
Expand Down Expand Up @@ -276,19 +439,13 @@ export class TwitterPostClient {
return;
}

// Use the helper function to truncate to complete sentence
const content = truncateToCompleteSentence(
cleanedContent,
parseInt(this.runtime.getSetting("MAX_TWEET_LENGTH")) || DEFAULT_MAX_TWEET_LENGTH
);

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

const fixNewLines = (str: string) => str.replaceAll(/\\n/g, "\n");

// Final cleaning
cleanedContent = removeQuotes(fixNewLines(content));
cleanedContent = removeQuotes(fixNewLines(cleanedContent));

if (this.runtime.getSetting("TWITTER_DRY_RUN") === "true") {
elizaLogger.info(
Expand All @@ -299,73 +456,14 @@ export class TwitterPostClient {

try {
elizaLogger.log(`Posting new tweet:\n ${cleanedContent}`);

const result = await this.client.requestQueue.add(
async () =>
await this.client.twitterClient.sendTweet(
cleanedContent
)
);
const body = await result.json();
if (!body?.data?.create_tweet?.tweet_results?.result) {
console.error("Error sending tweet; Bad response:", body);
return;
}
const tweetResult = body.data.create_tweet.tweet_results.result;

const tweet = {
id: tweetResult.rest_id,
name: this.client.profile.screenName,
username: this.client.profile.username,
text: tweetResult.legacy.full_text,
conversationId: tweetResult.legacy.conversation_id_str,
createdAt: tweetResult.legacy.created_at,
timestamp: new Date(
tweetResult.legacy.created_at
).getTime(),
userId: this.client.profile.id,
inReplyToStatusId:
tweetResult.legacy.in_reply_to_status_id_str,
permanentUrl: `https://twitter.com/${this.twitterUsername}/status/${tweetResult.rest_id}`,
hashtags: [],
mentions: [],
photos: [],
thread: [],
urls: [],
videos: [],
} as Tweet;

await this.runtime.cacheManager.set(
`twitter/${this.client.profile.username}/lastPost`,
{
id: tweet.id,
timestamp: Date.now(),
}
);

await this.client.cacheTweet(tweet);

elizaLogger.log(`Tweet posted:\n ${tweet.permanentUrl}`);

await this.runtime.ensureRoomExists(roomId);
await this.runtime.ensureParticipantInRoom(
this.runtime.agentId,
roomId
);

await this.runtime.messageManager.createMemory({
id: stringToUuid(tweet.id + "-" + this.runtime.agentId),
userId: this.runtime.agentId,
agentId: this.runtime.agentId,
content: {
text: newTweetContent.trim(),
url: tweet.permanentUrl,
source: "twitter",
},
this.postTweet(
this.runtime,
this.client,
cleanedContent,
roomId,
embedding: getEmbeddingZeroVector(),
createdAt: tweet.timestamp,
});
newTweetContent,
this.twitterUsername
);
} catch (error) {
elizaLogger.error("Error sending tweet:", error);
}
Expand Down Expand Up @@ -832,18 +930,24 @@ export class TwitterPostClient {

elizaLogger.debug("Final reply text to be sent:", replyText);

// Send the tweet through request queue
const result = await this.client.requestQueue.add(
async () =>
await this.client.twitterClient.sendTweet(
replyText,
tweet.id
)
);
let result;

const body = await result.json();
if (replyText.length > DEFAULT_MAX_TWEET_LENGTH) {
result = await this.handleNoteTweet(
this.client,
this.runtime,
replyText,
tweet.id
);
} else {
result = await this.sendStandardTweet(
this.client,
replyText,
tweet.id
);
}

if (body?.data?.create_tweet?.tweet_results?.result) {
if (result) {
elizaLogger.log("Successfully posted reply tweet");
executedActions.push("reply");

Expand All @@ -853,7 +957,7 @@ export class TwitterPostClient {
`Context:\n${enrichedState}\n\nGenerated Reply:\n${replyText}`
);
} else {
elizaLogger.error("Tweet reply creation failed:", body);
elizaLogger.error("Tweet reply creation failed");
}
} catch (error) {
elizaLogger.error("Error in handleTextOnlyReply:", error);
Expand Down
Loading