1
+ // utils.ts
2
+
1
3
import { Tweet } from "agent-twitter-client" ;
2
4
import { embeddingZeroVector } from "@ai16z/eliza/src/memory.ts" ;
3
5
import { Content , Memory , UUID } from "@ai16z/eliza/src/types.ts" ;
4
6
import { stringToUuid } from "@ai16z/eliza/src/uuid.ts" ;
5
7
import { ClientBase } from "./base.ts" ;
6
8
import { elizaLogger } from "@ai16z/eliza/src/logger.ts" ;
7
9
8
- const MAX_TWEET_LENGTH = 240 ;
10
+ const MAX_TWEET_LENGTH = 280 ; // Updated to Twitter's current character limit
9
11
10
12
export const wait = ( minTime : number = 1000 , maxTime : number = 3000 ) => {
11
13
const waitTime =
@@ -17,13 +19,13 @@ export const isValidTweet = (tweet: Tweet): boolean => {
17
19
// Filter out tweets with too many hashtags, @s, or $ signs, probably spam or garbage
18
20
const hashtagCount = ( tweet . text ?. match ( / # / g) || [ ] ) . length ;
19
21
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 ;
22
24
23
25
return (
24
26
hashtagCount <= 1 &&
25
27
atCount <= 2 &&
26
- dollarSignCount . length <= 1 &&
28
+ dollarSignCount <= 1 &&
27
29
totalCount <= 3
28
30
) ;
29
31
} ;
@@ -40,7 +42,7 @@ export async function buildConversationThread(
40
42
elizaLogger . log ( "No current tweet found" ) ;
41
43
return ;
42
44
}
43
- // check if the current tweet has already been saved
45
+ // Check if the current tweet has already been saved
44
46
const memory = await client . runtime . messageManager . getMemoryById (
45
47
stringToUuid ( currentTweet . id + "-" + client . runtime . agentId )
46
48
) ;
@@ -49,7 +51,10 @@ export async function buildConversationThread(
49
51
const roomId = stringToUuid (
50
52
currentTweet . conversationId + "-" + client . runtime . agentId
51
53
) ;
52
- const userId = stringToUuid ( currentTweet . userId ) ;
54
+ const userId =
55
+ currentTweet . userId === client . twitterUserId
56
+ ? client . runtime . agentId
57
+ : stringToUuid ( currentTweet . userId ) ;
53
58
54
59
await client . runtime . ensureConnection (
55
60
userId ,
@@ -59,11 +64,12 @@ export async function buildConversationThread(
59
64
"twitter"
60
65
) ;
61
66
62
- client . runtime . messageManager . createMemory ( {
67
+ await client . runtime . messageManager . createMemory ( {
63
68
id : stringToUuid (
64
69
currentTweet . id + "-" + client . runtime . agentId
65
70
) ,
66
71
agentId : client . runtime . agentId ,
72
+ userId : userId ,
67
73
content : {
68
74
text : currentTweet . text ,
69
75
source : "twitter" ,
@@ -78,10 +84,6 @@ export async function buildConversationThread(
78
84
} ,
79
85
createdAt : currentTweet . timestamp * 1000 ,
80
86
roomId,
81
- userId :
82
- currentTweet . userId === client . twitterUserId
83
- ? client . runtime . agentId
84
- : stringToUuid ( currentTweet . userId ) ,
85
87
embedding : embeddingZeroVector ,
86
88
} ) ;
87
89
}
@@ -100,7 +102,7 @@ export async function buildConversationThread(
100
102
await processThread ( tweet ) ;
101
103
}
102
104
103
- export async function sendTweetChunks (
105
+ export async function sendTweet (
104
106
client : ClientBase ,
105
107
content : Content ,
106
108
roomId : UUID ,
@@ -109,25 +111,26 @@ export async function sendTweetChunks(
109
111
) : Promise < Memory [ ] > {
110
112
const tweetChunks = splitTweetContent ( content . text ) ;
111
113
const sentTweets : Tweet [ ] = [ ] ;
114
+ let previousTweetId = inReplyTo ;
112
115
113
116
for ( const chunk of tweetChunks ) {
114
117
const result = await client . requestQueue . add (
115
118
async ( ) =>
116
119
await client . twitterClient . sendTweet (
117
- chunk . replaceAll ( / \\ n / g , "\n" ) . trim ( ) ,
118
- inReplyTo
120
+ chunk . trim ( ) ,
121
+ previousTweetId
119
122
)
120
123
) ;
121
- // console.log("send tweet result:\n", result);
124
+ // Parse the response
122
125
const body = await result . json ( ) ;
123
- console . log ( "send tweet body:\n" , body . data . create_tweet . tweet_results ) ;
124
126
const tweetResult = body . data . create_tweet . tweet_results . result ;
125
127
126
- const finalTweet = {
128
+ const finalTweet : Tweet = {
127
129
id : tweetResult . rest_id ,
128
130
text : tweetResult . legacy . full_text ,
129
131
conversationId : tweetResult . legacy . conversation_id_str ,
130
- createdAt : tweetResult . legacy . created_at ,
132
+ //createdAt:
133
+ timestamp : tweetResult . timestamp * 1000 ,
131
134
userId : tweetResult . legacy . user_id_str ,
132
135
inReplyToStatusId : tweetResult . legacy . in_reply_to_status_id_str ,
133
136
permanentUrl : `https://twitter.com/${ twitterUsername } /status/${ tweetResult . rest_id } ` ,
@@ -137,72 +140,14 @@ export async function sendTweetChunks(
137
140
thread : [ ] ,
138
141
urls : [ ] ,
139
142
videos : [ ] ,
140
- } as Tweet ;
143
+ } ;
141
144
142
145
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 ;
166
147
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
+ }
206
151
207
152
const memories : Memory [ ] = sentTweets . map ( ( tweet ) => ( {
208
153
id : stringToUuid ( tweet . id + "-" + client . runtime . agentId ) ,
@@ -227,56 +172,85 @@ export async function sendTweet(
227
172
}
228
173
229
174
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 ;
232
182
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
+ }
237
189
} 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
+ }
240
201
}
241
202
}
242
203
243
- if ( currentChunk ) {
244
- tweetChunks . push ( currentChunk ) ;
204
+ if ( currentTweet ) {
205
+ tweets . push ( currentTweet . trim ( ) ) ;
245
206
}
246
207
247
- return tweetChunks ;
208
+ return tweets ;
248
209
}
249
210
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 = "" ;
255
215
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
+ }
259
249
}
260
250
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 ( ) ) ;
279
253
}
280
254
281
- return content ;
255
+ return chunks ;
282
256
}
0 commit comments