Enhance error handling in ask command and improve message chunk sending

This commit is contained in:
2025-02-02 16:22:33 -05:00
parent bd4f42d46d
commit a6eec68753
2 changed files with 123 additions and 46 deletions
+71 -37
View File
@@ -37,8 +37,7 @@ jest.mock('discord.js', () => ({
}, },
{ {
name: 'websearch', name: 'websearch',
description: description: 'Enable web search for more up-to-date information',
'Enable web search for more up-to-date information',
type: 5, type: 5,
required: false, required: false,
}, },
@@ -77,9 +76,7 @@ describe('Ask Command', () => {
expect(promptOption.required).toBe(true); expect(promptOption.required).toBe(true);
expect(websearchOption.name).toBe('websearch'); expect(websearchOption.name).toBe('websearch');
expect(websearchOption.description).toBe( expect(websearchOption.description).toBe('Enable web search for more up-to-date information');
'Enable web search for more up-to-date information'
);
expect(websearchOption.required).toBe(false); expect(websearchOption.required).toBe(false);
}); });
}); });
@@ -126,7 +123,7 @@ describe('Ask Command', () => {
model: 'google/gemini-2.0-flash-exp:free', model: 'google/gemini-2.0-flash-exp:free',
plugins: undefined, plugins: undefined,
}), }),
expect.any(Object) expect.any(Object),
); );
expect(interaction.followUp).toHaveBeenCalledWith({ expect(interaction.followUp).toHaveBeenCalledWith({
@@ -162,7 +159,7 @@ describe('Ask Command', () => {
}), }),
]), ]),
}), }),
expect.any(Object) expect.any(Object),
); );
expect(interaction.followUp).toHaveBeenCalledWith({ expect(interaction.followUp).toHaveBeenCalledWith({
@@ -191,7 +188,7 @@ describe('Ask Command', () => {
model: 'google/gemini-2.0-flash-exp:free', model: 'google/gemini-2.0-flash-exp:free',
plugins: undefined, plugins: undefined,
}), }),
expect.any(Object) expect.any(Object),
); );
}); });
@@ -222,12 +219,8 @@ describe('Ask Command', () => {
await askCommand.execute(interaction); await askCommand.execute(interaction);
expect(interaction.followUp).toHaveBeenCalledTimes(2); expect(interaction.followUp).toHaveBeenCalledTimes(2);
expect(interaction.followUp.mock.calls[0][0].content).toContain( expect(interaction.followUp.mock.calls[0][0].content).toContain('Web search enabled');
'Web search enabled' expect(interaction.followUp.mock.calls[1][0].content).toContain('(continued)');
);
expect(interaction.followUp.mock.calls[1][0].content).toContain(
'(continued)'
);
}); });
it('should handle code blocks in chunked responses with websearch', async () => { it('should handle code blocks in chunked responses with websearch', async () => {
@@ -241,8 +234,9 @@ describe('Ask Command', () => {
}, },
}); });
const responseWithCodeBlock = const responseWithCodeBlock = "Here's a code example:\n```python\nprint('hello')\n```".repeat(
"Here's a code example:\n```python\nprint('hello')\n```".repeat(20); 20,
);
axios.post.mockResolvedValueOnce({ axios.post.mockResolvedValueOnce({
data: { data: {
choices: [ choices: [
@@ -265,20 +259,14 @@ describe('Ask Command', () => {
}); });
// First chunk should contain websearch indicator // First chunk should contain websearch indicator
expect(interaction.followUp.mock.calls[0][0].content).toContain( expect(interaction.followUp.mock.calls[0][0].content).toContain('Web search enabled');
'Web search enabled'
);
}); });
it('should handle API errors with websearch enabled', async () => { it('should handle API errors with websearch enabled', async () => {
interaction = createMockInteraction({ interaction = createMockInteraction({
commandName: 'ask', commandName: 'ask',
stringOptions: { stringOptions: { prompt: mockPrompt },
prompt: mockPrompt, booleanOptions: { websearch: true },
},
booleanOptions: {
websearch: true,
},
}); });
const error = new Error('API Error'); const error = new Error('API Error');
@@ -293,6 +281,60 @@ describe('Ask Command', () => {
}); });
}); });
it('should handle rate limit errors', async () => {
interaction = createMockInteraction({
commandName: 'ask',
stringOptions: { prompt: mockPrompt },
});
const error = new Error('Rate Limit Exceeded');
error.response = { status: 429, data: 'Too Many Requests' };
axios.post.mockRejectedValueOnce(error);
await askCommand.execute(interaction);
expect(interaction.followUp).toHaveBeenCalledWith({
content: 'The AI service is currently busy. Please try again in a few moments.',
ephemeral: true,
});
});
it('should handle timeout errors', async () => {
interaction = createMockInteraction({
commandName: 'ask',
stringOptions: { prompt: mockPrompt },
});
const error = new Error('Timeout');
error.code = 'ETIMEDOUT';
axios.post.mockRejectedValueOnce(error);
await askCommand.execute(interaction);
expect(interaction.followUp).toHaveBeenCalledWith({
content: 'The request timed out. Please try again.',
ephemeral: true,
});
});
it('should handle invalid request errors', async () => {
interaction = createMockInteraction({
commandName: 'ask',
stringOptions: { prompt: mockPrompt },
});
const error = new Error('Bad Request');
error.response = { status: 400, data: 'Invalid Request' };
axios.post.mockRejectedValueOnce(error);
await askCommand.execute(interaction);
expect(interaction.followUp).toHaveBeenCalledWith({
content: 'Invalid request. Please try rephrasing your question.',
ephemeral: true,
});
});
it('should send API request with correct headers regardless of websearch', async () => { it('should send API request with correct headers regardless of websearch', async () => {
interaction = createMockInteraction({ interaction = createMockInteraction({
commandName: 'ask', commandName: 'ask',
@@ -308,17 +350,13 @@ describe('Ask Command', () => {
await askCommand.execute(interaction); await askCommand.execute(interaction);
expect(axios.post).toHaveBeenCalledWith( expect(axios.post).toHaveBeenCalledWith(expect.any(String), expect.any(Object), {
expect.any(String),
expect.any(Object),
{
headers: { headers: {
Authorization: 'Bearer test-api-key', Authorization: 'Bearer test-api-key',
'HTTP-Referer': 'https://github.com/hllywluis/kekbot.js', 'HTTP-Referer': 'https://github.com/hllywluis/kekbot.js',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
} });
);
}); });
it('should handle defer reply failure with websearch', async () => { it('should handle defer reply failure with websearch', async () => {
@@ -333,9 +371,7 @@ describe('Ask Command', () => {
deferFails: true, deferFails: true,
}); });
await expect(askCommand.execute(interaction)).rejects.toThrow( await expect(askCommand.execute(interaction)).rejects.toThrow('Failed to defer reply');
'Failed to defer reply'
);
}); });
it('should handle follow up failure with websearch', async () => { it('should handle follow up failure with websearch', async () => {
@@ -352,9 +388,7 @@ describe('Ask Command', () => {
axios.post.mockResolvedValueOnce(mockApiResponse); axios.post.mockResolvedValueOnce(mockApiResponse);
await expect(askCommand.execute(interaction)).rejects.toThrow( await expect(askCommand.execute(interaction)).rejects.toThrow('Failed to follow up');
'Failed to follow up'
);
}); });
}); });
}); });
+47 -4
View File
@@ -130,13 +130,56 @@ module.exports = {
isFirstChunk = false; isFirstChunk = false;
} }
// Send all chunks in sequence // Send chunks sequentially using reduce
await Promise.all(chunks.map(chunk => interaction.followUp(chunk))); await chunks.reduce(
(promise, chunk) =>
promise.then(async () => {
try {
await interaction.followUp(chunk);
return undefined;
} catch (error) {
console.error('Error sending message chunk:', {
chunkLength: chunk.content.length,
error: error.message,
});
// Attempt to send error notification
try {
await interaction.followUp({
content: 'Failed to send complete response. Please try again.',
ephemeral: true,
});
} catch (e) {
// If even the error notification fails, log it
console.error('Failed to send error notification:', e.message);
}
// Reject to stop processing remaining chunks
return Promise.reject(error);
}
}),
Promise.resolve(),
);
} }
} catch (error) { } catch (error) {
console.error('Error:', error.response?.data || error.message); // Log detailed error information
console.error('Error in ask command:', {
message: error.message,
response: error.response?.data,
status: error.response?.status,
stack: error.stack,
});
// Provide more specific error messages to users
let errorMessage = 'Sorry, there was an error processing your request.';
if (error.response?.status === 429) {
errorMessage = 'The AI service is currently busy. Please try again in a few moments.';
} else if (error.code === 'ECONNABORTED' || error.code === 'ETIMEDOUT') {
errorMessage = 'The request timed out. Please try again.';
} else if (error.response?.status === 400) {
errorMessage = 'Invalid request. Please try rephrasing your question.';
}
await interaction.followUp({ await interaction.followUp({
content: 'Sorry, there was an error processing your request.', content: errorMessage,
ephemeral: true, ephemeral: true,
}); });
} }