Skip to content

Commit b766d08

Browse files
committed
refactor: Implement retry logic with exponential backoff for AI message processing and filter out GIFs from image attachments
1 parent cf12f1f commit b766d08

File tree

2 files changed

+91
-53
lines changed

2 files changed

+91
-53
lines changed

src/events/ai/ai.ts

Lines changed: 77 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ import {
1818

1919
@Discord()
2020
export class AiChat {
21+
private static readonly MAX_RETRIES = 5;
22+
private static readonly BASE_DELAY = 1000; // 1 second
23+
2124
@On()
2225
async messageCreate(
2326
[message]: ArgsOf<"messageCreate">,
@@ -63,60 +66,82 @@ export class AiChat {
6366
const userContext = await this.getUserContext(message.author.id, message);
6467
const fullMessage = `${userMsg}${replyContext}${userContext}`;
6568

66-
try {
67-
const messageImages = await makeImageParts(message);
68-
const allImages = [...messageImages, ...repliedImages];
69-
70-
// Create user message with context
71-
const userMessage: ModelMessage =
72-
allImages.length > 0
73-
? {
74-
role: "user",
75-
content: [
76-
{ type: "text", text: fullMessage },
77-
...allImages.map((url) => ({
78-
type: "image" as const,
79-
image: url,
80-
})),
81-
],
82-
}
83-
: {
84-
role: "user",
85-
content: fullMessage,
86-
};
87-
88-
// Add user message to history
89-
messages.push(userMessage);
90-
91-
const { text, steps } = await generateText({
92-
model: google("gemini-2.5-flash"),
93-
system: AI_SYSTEM_PROMPT,
94-
messages: [...messages],
95-
tools: TOOLS,
96-
});
97-
98-
messages.push({ role: "assistant", content: text?.trim() });
99-
100-
// Trim history if too long
101-
if (messages.length > MAX_MESSAGES_PER_CHANNEL) {
102-
messages.splice(0, messages.length - MAX_MESSAGES_PER_CHANNEL);
103-
}
69+
// Retry logic with exponential backoff
70+
let lastError: Error | null = null;
71+
72+
for (let attempt = 0; attempt < AiChat.MAX_RETRIES; attempt++) {
73+
try {
74+
const messageImages = await makeImageParts(message);
75+
const allImages = [...messageImages, ...repliedImages];
76+
77+
// Create user message with context
78+
const userMessage: ModelMessage =
79+
allImages.length > 0
80+
? {
81+
role: "user",
82+
content: [
83+
{ type: "text", text: fullMessage },
84+
...allImages.map((url) => ({
85+
type: "image" as const,
86+
image: url,
87+
})),
88+
],
89+
}
90+
: {
91+
role: "user",
92+
content: fullMessage,
93+
};
94+
95+
// Add user message to history
96+
messages.push(userMessage);
97+
98+
const { text, steps } = await generateText({
99+
model: google("gemini-2.5-flash"),
100+
system: AI_SYSTEM_PROMPT,
101+
messages: [...messages],
102+
tools: TOOLS,
103+
});
104+
105+
messages.push({ role: "assistant", content: text?.trim() });
106+
107+
// Trim history if too long
108+
if (messages.length > MAX_MESSAGES_PER_CHANNEL) {
109+
messages.splice(0, messages.length - MAX_MESSAGES_PER_CHANNEL);
110+
}
104111

105-
channelMessages.set(message.channel.id, messages);
106-
107-
const gifUrl = this.extractGifFromSteps(steps);
108-
await message.reply({
109-
content: text?.trim(),
110-
files: gifUrl
111-
? [{ attachment: gifUrl, name: "reaction.gif" }]
112-
: undefined,
113-
});
114-
} catch (err) {
115-
error("AI error:", err);
116-
await message.reply(
117-
"Something went wrong while thinking. Try again later!"
118-
);
112+
channelMessages.set(message.channel.id, messages);
113+
114+
const gifUrl = this.extractGifFromSteps(steps);
115+
await message.reply({
116+
content: text?.trim(),
117+
files: gifUrl
118+
? [{ attachment: gifUrl, name: "reaction.gif" }]
119+
: undefined,
120+
});
121+
122+
// Success - exit retry loop
123+
return;
124+
} catch (err) {
125+
lastError = err as Error;
126+
error(`AI error (attempt ${attempt + 1}/${AiChat.MAX_RETRIES}):`, err);
127+
128+
// Don't wait after the last attempt
129+
if (attempt < AiChat.MAX_RETRIES - 1) {
130+
const delay = AiChat.BASE_DELAY * Math.pow(2, attempt);
131+
console.log(`Retrying in ${delay}ms...`);
132+
await new Promise((resolve) => setTimeout(resolve, delay));
133+
}
134+
}
119135
}
136+
137+
// All retries failed
138+
error(
139+
`All ${AiChat.MAX_RETRIES} AI attempts failed. Last error:`,
140+
lastError
141+
);
142+
await message.reply(
143+
"Something went wrong while thinking. Try again later!"
144+
);
120145
}
121146

122147
private async getUserContext(

src/events/ai/utils.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,26 @@ export async function makeImageParts(message: Message): Promise<string[]> {
7676

7777
for (const attachment of message.attachments.values()) {
7878
if (attachment.contentType?.startsWith("image/")) {
79+
// Filter out GIFs - Google Gemini doesn't support them
80+
if (attachment.contentType === "image/gif") {
81+
console.log(`Skipping GIF attachment: ${attachment.url}`);
82+
continue;
83+
}
7984
images.push(attachment.url);
8085
}
8186
}
8287

8388
for (const sticker of message.stickers.values()) {
8489
if (sticker.format !== StickerFormatType.Lottie) {
85-
images.push(sticker.url);
90+
// Only include non-GIF stickers (PNG/APNG format stickers)
91+
if (
92+
sticker.format === StickerFormatType.PNG ||
93+
sticker.format === StickerFormatType.APNG
94+
) {
95+
images.push(sticker.url);
96+
} else {
97+
console.log(`Skipping GIF sticker: ${sticker.url}`);
98+
}
8699
}
87100
}
88101

0 commit comments

Comments
 (0)