Skip to content

Commit 1824bb9

Browse files
authored
Merge pull request #323 from o-on-x/o-on-x-wip-tweet-split
utils.ts example tweet splitting
2 parents 0d1b1fb + 46a913e commit 1824bb9

File tree

1 file changed

+93
-119
lines changed

1 file changed

+93
-119
lines changed

packages/client-twitter/src/utils.ts

+93-119
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1+
// utils.ts
2+
13
import { Tweet } from "agent-twitter-client";
24
import { embeddingZeroVector } from "@ai16z/eliza/src/memory.ts";
35
import { Content, Memory, UUID } from "@ai16z/eliza/src/types.ts";
46
import { stringToUuid } from "@ai16z/eliza/src/uuid.ts";
57
import { ClientBase } from "./base.ts";
68
import { elizaLogger } from "@ai16z/eliza/src/logger.ts";
79

8-
const MAX_TWEET_LENGTH = 240;
10+
const MAX_TWEET_LENGTH = 280; // Updated to Twitter's current character limit
911

1012
export const wait = (minTime: number = 1000, maxTime: number = 3000) => {
1113
const waitTime =
@@ -17,13 +19,13 @@ export const isValidTweet = (tweet: Tweet): boolean => {
1719
// Filter out tweets with too many hashtags, @s, or $ signs, probably spam or garbage
1820
const hashtagCount = (tweet.text?.match(/#/g) || []).length;
1921
const atCount = (tweet.text?.match(/@/g) || []).length;
20-
const dollarSignCount = tweet.text?.match(/\$/g) || [];
21-
const totalCount = hashtagCount + atCount + dollarSignCount.length;
22+
const dollarSignCount = (tweet.text?.match(/\$/g) || []).length;
23+
const totalCount = hashtagCount + atCount + dollarSignCount;
2224

2325
return (
2426
hashtagCount <= 1 &&
2527
atCount <= 2 &&
26-
dollarSignCount.length <= 1 &&
28+
dollarSignCount <= 1 &&
2729
totalCount <= 3
2830
);
2931
};
@@ -40,7 +42,7 @@ export async function buildConversationThread(
4042
elizaLogger.log("No current tweet found");
4143
return;
4244
}
43-
// check if the current tweet has already been saved
45+
// Check if the current tweet has already been saved
4446
const memory = await client.runtime.messageManager.getMemoryById(
4547
stringToUuid(currentTweet.id + "-" + client.runtime.agentId)
4648
);
@@ -49,7 +51,10 @@ export async function buildConversationThread(
4951
const roomId = stringToUuid(
5052
currentTweet.conversationId + "-" + client.runtime.agentId
5153
);
52-
const userId = stringToUuid(currentTweet.userId);
54+
const userId =
55+
currentTweet.userId === client.twitterUserId
56+
? client.runtime.agentId
57+
: stringToUuid(currentTweet.userId);
5358

5459
await client.runtime.ensureConnection(
5560
userId,
@@ -59,11 +64,12 @@ export async function buildConversationThread(
5964
"twitter"
6065
);
6166

62-
client.runtime.messageManager.createMemory({
67+
await client.runtime.messageManager.createMemory({
6368
id: stringToUuid(
6469
currentTweet.id + "-" + client.runtime.agentId
6570
),
6671
agentId: client.runtime.agentId,
72+
userId: userId,
6773
content: {
6874
text: currentTweet.text,
6975
source: "twitter",
@@ -78,10 +84,6 @@ export async function buildConversationThread(
7884
},
7985
createdAt: currentTweet.timestamp * 1000,
8086
roomId,
81-
userId:
82-
currentTweet.userId === client.twitterUserId
83-
? client.runtime.agentId
84-
: stringToUuid(currentTweet.userId),
8587
embedding: embeddingZeroVector,
8688
});
8789
}
@@ -100,7 +102,7 @@ export async function buildConversationThread(
100102
await processThread(tweet);
101103
}
102104

103-
export async function sendTweetChunks(
105+
export async function sendTweet(
104106
client: ClientBase,
105107
content: Content,
106108
roomId: UUID,
@@ -109,25 +111,26 @@ export async function sendTweetChunks(
109111
): Promise<Memory[]> {
110112
const tweetChunks = splitTweetContent(content.text);
111113
const sentTweets: Tweet[] = [];
114+
let previousTweetId = inReplyTo;
112115

113116
for (const chunk of tweetChunks) {
114117
const result = await client.requestQueue.add(
115118
async () =>
116119
await client.twitterClient.sendTweet(
117-
chunk.replaceAll(/\\n/g, "\n").trim(),
118-
inReplyTo
120+
chunk.trim(),
121+
previousTweetId
119122
)
120123
);
121-
// console.log("send tweet result:\n", result);
124+
// Parse the response
122125
const body = await result.json();
123-
console.log("send tweet body:\n", body.data.create_tweet.tweet_results);
124126
const tweetResult = body.data.create_tweet.tweet_results.result;
125127

126-
const finalTweet = {
128+
const finalTweet: Tweet = {
127129
id: tweetResult.rest_id,
128130
text: tweetResult.legacy.full_text,
129131
conversationId: tweetResult.legacy.conversation_id_str,
130-
createdAt: tweetResult.legacy.created_at,
132+
//createdAt:
133+
timestamp: tweetResult.timestamp * 1000,
131134
userId: tweetResult.legacy.user_id_str,
132135
inReplyToStatusId: tweetResult.legacy.in_reply_to_status_id_str,
133136
permanentUrl: `https://twitter.com/${twitterUsername}/status/${tweetResult.rest_id}`,
@@ -137,72 +140,14 @@ export async function sendTweetChunks(
137140
thread: [],
138141
urls: [],
139142
videos: [],
140-
} as Tweet;
143+
};
141144

142145
sentTweets.push(finalTweet);
143-
}
144-
145-
const memories: Memory[] = sentTweets.map((tweet) => ({
146-
id: stringToUuid(tweet.id + "-" + client.runtime.agentId),
147-
agentId: client.runtime.agentId,
148-
userId: client.runtime.agentId,
149-
content: {
150-
text: tweet.text,
151-
source: "twitter",
152-
url: tweet.permanentUrl,
153-
inReplyTo: tweet.inReplyToStatusId
154-
? stringToUuid(
155-
tweet.inReplyToStatusId + "-" + client.runtime.agentId
156-
)
157-
: undefined,
158-
},
159-
roomId,
160-
embedding: embeddingZeroVector,
161-
createdAt: tweet.timestamp * 1000,
162-
}));
163-
164-
return memories;
165-
}
146+
previousTweetId = finalTweet.id;
166147

167-
export async function sendTweet(
168-
client: ClientBase,
169-
content: Content,
170-
roomId: UUID,
171-
twitterUsername: string,
172-
inReplyTo: string
173-
): Promise<Memory[]> {
174-
const chunk = truncateTweetContent(content.text);
175-
const sentTweets: Tweet[] = [];
176-
177-
const result = await client.requestQueue.add(
178-
async () =>
179-
await client.twitterClient.sendTweet(
180-
chunk.replaceAll(/\\n/g, "\n").trim(),
181-
inReplyTo
182-
)
183-
);
184-
// console.log("send tweet result:\n", result);
185-
const body = await result.json();
186-
console.log("send tweet body:\n", body.data.create_tweet.tweet_results);
187-
const tweetResult = body.data.create_tweet.tweet_results.result;
188-
189-
const finalTweet = {
190-
id: tweetResult.rest_id,
191-
text: tweetResult.legacy.full_text,
192-
conversationId: tweetResult.legacy.conversation_id_str,
193-
createdAt: tweetResult.legacy.created_at,
194-
userId: tweetResult.legacy.user_id_str,
195-
inReplyToStatusId: tweetResult.legacy.in_reply_to_status_id_str,
196-
permanentUrl: `https://twitter.com/${twitterUsername}/status/${tweetResult.rest_id}`,
197-
hashtags: [],
198-
mentions: [],
199-
photos: [],
200-
thread: [],
201-
urls: [],
202-
videos: [],
203-
} as Tweet;
204-
205-
sentTweets.push(finalTweet);
148+
// Wait a bit between tweets to avoid rate limiting issues
149+
await wait(1000, 2000);
150+
}
206151

207152
const memories: Memory[] = sentTweets.map((tweet) => ({
208153
id: stringToUuid(tweet.id + "-" + client.runtime.agentId),
@@ -227,56 +172,85 @@ export async function sendTweet(
227172
}
228173

229174
function splitTweetContent(content: string): string[] {
230-
const tweetChunks: string[] = [];
231-
let currentChunk = "";
175+
const maxLength = MAX_TWEET_LENGTH;
176+
const paragraphs = content.split("\n\n").map((p) => p.trim());
177+
const tweets: string[] = [];
178+
let currentTweet = "";
179+
180+
for (const paragraph of paragraphs) {
181+
if (!paragraph) continue;
232182

233-
const words = content.split(" ");
234-
for (const word of words) {
235-
if (currentChunk.length + word.length + 1 <= MAX_TWEET_LENGTH) {
236-
currentChunk += (currentChunk ? " " : "") + word;
183+
if ((currentTweet + "\n\n" + paragraph).trim().length <= maxLength) {
184+
if (currentTweet) {
185+
currentTweet += "\n\n" + paragraph;
186+
} else {
187+
currentTweet = paragraph;
188+
}
237189
} else {
238-
tweetChunks.push(currentChunk);
239-
currentChunk = word;
190+
if (currentTweet) {
191+
tweets.push(currentTweet.trim());
192+
}
193+
if (paragraph.length <= maxLength) {
194+
currentTweet = paragraph;
195+
} else {
196+
// Split long paragraph into smaller chunks
197+
const chunks = splitParagraph(paragraph, maxLength);
198+
tweets.push(...chunks.slice(0, -1));
199+
currentTweet = chunks[chunks.length - 1];
200+
}
240201
}
241202
}
242203

243-
if (currentChunk) {
244-
tweetChunks.push(currentChunk);
204+
if (currentTweet) {
205+
tweets.push(currentTweet.trim());
245206
}
246207

247-
return tweetChunks;
208+
return tweets;
248209
}
249210

250-
export function truncateTweetContent(content: string): string {
251-
// if its 240, delete the last line
252-
if (content.length === MAX_TWEET_LENGTH) {
253-
return content.slice(0, content.lastIndexOf("\n"));
254-
}
211+
function splitParagraph(paragraph: string, maxLength: number): string[] {
212+
const sentences = paragraph.match(/[^\.!\?]+[\.!\?]+|[^\.!\?]+$/g) || [paragraph];
213+
const chunks: string[] = [];
214+
let currentChunk = "";
255215

256-
// if its still bigger than 240, delete everything after the last period
257-
if (content.length > MAX_TWEET_LENGTH) {
258-
return content.slice(0, content.lastIndexOf("."));
216+
for (const sentence of sentences) {
217+
if ((currentChunk + " " + sentence).trim().length <= maxLength) {
218+
if (currentChunk) {
219+
currentChunk += " " + sentence;
220+
} else {
221+
currentChunk = sentence;
222+
}
223+
} else {
224+
if (currentChunk) {
225+
chunks.push(currentChunk.trim());
226+
}
227+
if (sentence.length <= maxLength) {
228+
currentChunk = sentence;
229+
} else {
230+
// Split long sentence into smaller pieces
231+
const words = sentence.split(" ");
232+
currentChunk = "";
233+
for (const word of words) {
234+
if ((currentChunk + " " + word).trim().length <= maxLength) {
235+
if (currentChunk) {
236+
currentChunk += " " + word;
237+
} else {
238+
currentChunk = word;
239+
}
240+
} else {
241+
if (currentChunk) {
242+
chunks.push(currentChunk.trim());
243+
}
244+
currentChunk = word;
245+
}
246+
}
247+
}
248+
}
259249
}
260250

261-
// while its STILL bigger than 240, find the second to last exclamation point or period and delete everything after it
262-
let iterations = 0;
263-
while (content.length > MAX_TWEET_LENGTH && iterations < 10) {
264-
iterations++;
265-
// second to last index of period or exclamation point
266-
const secondToLastIndexOfPeriod = content.lastIndexOf(
267-
".",
268-
content.length - 2
269-
);
270-
const secondToLastIndexOfExclamation = content.lastIndexOf(
271-
"!",
272-
content.length - 2
273-
);
274-
const secondToLastIndex = Math.max(
275-
secondToLastIndexOfPeriod,
276-
secondToLastIndexOfExclamation
277-
);
278-
content = content.slice(0, secondToLastIndex);
251+
if (currentChunk) {
252+
chunks.push(currentChunk.trim());
279253
}
280254

281-
return content;
255+
return chunks;
282256
}

0 commit comments

Comments
 (0)