Enhance error handling in ask command and improve message chunk sending
This commit is contained in:
@@ -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),
|
headers: {
|
||||||
expect.any(Object),
|
Authorization: 'Bearer test-api-key',
|
||||||
{
|
'HTTP-Referer': 'https://github.com/hllywluis/kekbot.js',
|
||||||
headers: {
|
'Content-Type': 'application/json',
|
||||||
Authorization: 'Bearer test-api-key',
|
},
|
||||||
'HTTP-Referer': 'https://github.com/hllywluis/kekbot.js',
|
});
|
||||||
'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
@@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user