Skip to content

Commit 9faad38

Browse files
authored
Merge pull request #2730 from elizaOS/tcm-improve-twitter-post
feat: improve twitter parsing
2 parents 2af84f8 + 859d959 commit 9faad38

File tree

2 files changed

+79
-30
lines changed

2 files changed

+79
-30
lines changed

packages/client-twitter/src/post.ts

+47-26
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
type UUID,
1111
truncateToCompleteSentence,
1212
parseJSONObjectFromText,
13+
extractAttributes,
1314
} from "@elizaos/core";
1415
import { elizaLogger } from "@elizaos/core";
1516
import type { ClientBase } from "./base.ts";
@@ -463,6 +464,22 @@ export class TwitterPostClient {
463464
}
464465
}
465466

467+
/**
468+
* Cleans a JSON-like response string by removing unnecessary markers, line breaks, and extra whitespace.
469+
* This is useful for handling improperly formatted JSON responses from external sources.
470+
*
471+
* @param response - The raw JSON-like string response to clean.
472+
* @returns The cleaned string, ready for parsing or further processing.
473+
*/
474+
475+
cleanJsonResponse(response: string): string {
476+
return response
477+
.replace(/```json\s*/g, "") // Remove ```json
478+
.replace(/```\s*/g, "") // Remove any remaining ```
479+
.replace(/(\r\n|\n|\r)/g, "") // Remove line breaks
480+
.trim();
481+
}
482+
466483
/**
467484
* Generates and posts a new tweet. If isDryRun is true, only logs what would have been posted.
468485
*/
@@ -512,11 +529,7 @@ export class TwitterPostClient {
512529
modelClass: ModelClass.SMALL,
513530
});
514531

515-
const newTweetContent = response
516-
.replace(/```json\s*/g, "") // Remove ```json
517-
.replace(/```\s*/g, "") // Remove any remaining ```
518-
.replace(/(\r\n|\n|\r)/g, "") // Remove line break
519-
.trim();
532+
const newTweetContent = this.cleanJsonResponse(response);
520533

521534
// First attempt to clean content
522535
let cleanedContent = "";
@@ -544,6 +557,13 @@ export class TwitterPostClient {
544557
.trim();
545558
}
546559

560+
if (!cleanedContent) {
561+
cleanedContent = truncateToCompleteSentence(
562+
extractAttributes(newTweetContent, ["text"]).text,
563+
this.client.twitterConfig.MAX_TWEET_LENGTH,
564+
);
565+
}
566+
547567
if (!cleanedContent) {
548568
elizaLogger.error(
549569
"Failed to extract valid content from response:",
@@ -634,25 +654,29 @@ export class TwitterPostClient {
634654
elizaLogger.log("generate tweet content response:\n" + response);
635655

636656
// First clean up any markdown and newlines
637-
const cleanedResponse = response
638-
.replace(/```json\s*/g, "") // Remove ```json
639-
.replace(/```\s*/g, "") // Remove any remaining ```
640-
.replace(/(\r\n|\n|\r)/g, "") // Remove line break
641-
.trim();
657+
const cleanedResponse = this.cleanJsonResponse(response);
642658

643659
// Try to parse as JSON first
644660
try {
645661
const jsonResponse = parseJSONObjectFromText(cleanedResponse);
646662
if (jsonResponse.text) {
647-
return this.trimTweetLength(jsonResponse.text);
663+
const truncateContent = truncateToCompleteSentence(
664+
jsonResponse.text,
665+
this.client.twitterConfig.MAX_TWEET_LENGTH,
666+
);
667+
return truncateContent;
648668
}
649669
if (typeof jsonResponse === "object") {
650670
const possibleContent =
651671
jsonResponse.content ||
652672
jsonResponse.message ||
653673
jsonResponse.response;
654674
if (possibleContent) {
655-
return this.trimTweetLength(possibleContent);
675+
const truncateContent = truncateToCompleteSentence(
676+
possibleContent,
677+
this.client.twitterConfig.MAX_TWEET_LENGTH,
678+
);
679+
return truncateContent;
656680
}
657681
}
658682
} catch (error) {
@@ -664,24 +688,21 @@ export class TwitterPostClient {
664688
response,
665689
);
666690
}
667-
// If not JSON or no valid content found, clean the raw text
668-
return this.trimTweetLength(cleanedResponse);
669-
}
670691

671-
// Helper method to ensure tweet length compliance
672-
private trimTweetLength(text: string, maxLength = 280): string {
673-
if (text.length <= maxLength) return text;
692+
let truncateContent = truncateToCompleteSentence(
693+
extractAttributes(cleanedResponse, ["text"]).text,
694+
this.client.twitterConfig.MAX_TWEET_LENGTH,
695+
);
674696

675-
// Try to cut at last sentence
676-
const lastSentence = text.slice(0, maxLength).lastIndexOf(".");
677-
if (lastSentence > 0) {
678-
return text.slice(0, lastSentence + 1).trim();
697+
if (!truncateContent) {
698+
// If not JSON or no valid content found, clean the raw text
699+
truncateContent = truncateToCompleteSentence(
700+
cleanedResponse,
701+
this.client.twitterConfig.MAX_TWEET_LENGTH,
702+
);
679703
}
680704

681-
// Fallback to word boundary
682-
return (
683-
text.slice(0, text.lastIndexOf(" ", maxLength - 3)).trim() + "..."
684-
);
705+
return truncateContent;
685706
}
686707

687708
/**

packages/core/src/parsing.ts

+32-4
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ If {{agentName}} is talking too much, you can choose [IGNORE]
1212
Your response must include one of the options.`;
1313

1414
export const parseShouldRespondFromText = (
15-
text: string
15+
text: string,
1616
): "RESPOND" | "IGNORE" | "STOP" | null => {
1717
const match = text
1818
.split("\n")[0]
@@ -92,6 +92,7 @@ export function parseJsonArrayFromText(text: string) {
9292
jsonData = JSON.parse(normalizedJson);
9393
} catch (e) {
9494
console.error("Error parsing JSON:", e);
95+
console.error("Text is not JSON", text);
9596
}
9697
}
9798

@@ -106,6 +107,7 @@ export function parseJsonArrayFromText(text: string) {
106107
const normalizedJson = arrayMatch[0].replace(/'/g, '"');
107108
jsonData = JSON.parse(normalizedJson);
108109
} catch (e) {
110+
console.error("Text is not JSON", text);
109111
console.error("Error parsing JSON:", e);
110112
}
111113
}
@@ -129,7 +131,7 @@ export function parseJsonArrayFromText(text: string) {
129131
* @returns An object parsed from the JSON string if successful; otherwise, null or the result of parsing an array.
130132
*/
131133
export function parseJSONObjectFromText(
132-
text: string
134+
text: string,
133135
): Record<string, any> | null {
134136
let jsonData = null;
135137

@@ -140,6 +142,7 @@ export function parseJSONObjectFromText(
140142
jsonData = JSON.parse(jsonBlockMatch[1]);
141143
} catch (e) {
142144
console.error("Error parsing JSON:", e);
145+
console.error("Text is not JSON", text);
143146
return null;
144147
}
145148
} else {
@@ -151,6 +154,7 @@ export function parseJSONObjectFromText(
151154
jsonData = JSON.parse(objectMatch[0]);
152155
} catch (e) {
153156
console.error("Error parsing JSON:", e);
157+
console.error("Text is not JSON", text);
154158
return null;
155159
}
156160
}
@@ -169,10 +173,34 @@ export function parseJSONObjectFromText(
169173
}
170174
}
171175

176+
/**
177+
* Extracts specific attributes (e.g., user, text, action) from a JSON-like string using regex.
178+
* @param response - The cleaned string response to extract attributes from.
179+
* @param attributesToExtract - An array of attribute names to extract.
180+
* @returns An object containing the extracted attributes.
181+
*/
182+
export function extractAttributes(
183+
response: string,
184+
attributesToExtract: string[],
185+
): { [key: string]: string | undefined } {
186+
const attributes: { [key: string]: string | undefined } = {};
187+
188+
attributesToExtract.forEach((attribute) => {
189+
const match = response.match(
190+
new RegExp(`"${attribute}"\\s*:\\s*"([^"]*)"`, "i"),
191+
);
192+
if (match) {
193+
attributes[attribute] = match[1];
194+
}
195+
});
196+
197+
return attributes;
198+
}
199+
172200
export const postActionResponseFooter = `Choose any combination of [LIKE], [RETWEET], [QUOTE], and [REPLY] that are appropriate. Each action must be on its own line. Your response must only include the chosen actions.`;
173201

174202
export const parseActionResponseFromText = (
175-
text: string
203+
text: string,
176204
): { actions: ActionResponse } => {
177205
const actions: ActionResponse = {
178206
like: false,
@@ -211,7 +239,7 @@ export const parseActionResponseFromText = (
211239
*/
212240
export function truncateToCompleteSentence(
213241
text: string,
214-
maxLength: number
242+
maxLength: number,
215243
): string {
216244
if (text.length <= maxLength) {
217245
return text;

0 commit comments

Comments
 (0)