fix: Handle AI errors better in builder (no-changelog) (#18406)

This commit is contained in:
Mutasem Aldmour
2025-08-20 13:50:53 +02:00
committed by GitHub
parent 51c867fb66
commit afaa0bec71
7 changed files with 742 additions and 40 deletions

View File

@@ -1453,6 +1453,244 @@ describe('useBuilderMessages', () => {
});
});
describe('error message handling with retry', () => {
it('should pass retry function to error messages from processAssistantMessages', () => {
const retryFn = vi.fn(async () => {});
const currentMessages: ChatUI.AssistantMessage[] = [];
const newMessages: ChatRequest.MessageResponse[] = [
{
type: 'error',
role: 'assistant',
content: 'Something went wrong',
},
];
const result = builderMessages.processAssistantMessages(
currentMessages,
newMessages,
'test-id',
retryFn,
);
expect(result.messages).toHaveLength(1);
const errorMessage = result.messages[0] as ChatUI.ErrorMessage;
expect(errorMessage).toMatchObject({
id: 'test-id-0',
role: 'assistant',
type: 'error',
content: 'Something went wrong',
read: false,
});
expect(errorMessage.retry).toBe(retryFn);
});
it('should not pass retry function to non-error messages', () => {
const retryFn = vi.fn(async () => {});
const currentMessages: ChatUI.AssistantMessage[] = [];
const newMessages: ChatRequest.MessageResponse[] = [
{
type: 'message',
role: 'assistant',
text: 'This is a normal text message',
},
{
role: 'assistant',
type: 'tool',
toolName: 'add_nodes',
toolCallId: 'call-1',
status: 'running',
updates: [],
},
];
const result = builderMessages.processAssistantMessages(
currentMessages,
newMessages,
'test-id',
retryFn,
);
expect(result.messages).toHaveLength(2);
const textMessage = result.messages[0] as ChatUI.TextMessage;
expect(textMessage.type).toBe('text');
expect('retry' in textMessage).toBe(false);
const toolMessage = result.messages[1] as ChatUI.ToolMessage;
expect(toolMessage.type).toBe('tool');
expect('retry' in toolMessage).toBe(false);
});
it('should clear retry from previous error messages when processing new messages', () => {
const oldRetryFn = vi.fn(async () => {});
const newRetryFn = vi.fn(async () => {});
const currentMessages: ChatUI.AssistantMessage[] = [
{
id: 'error-1',
role: 'assistant',
type: 'error',
content: 'First error',
retry: oldRetryFn,
read: false,
} as ChatUI.ErrorMessage,
{
id: 'error-2',
role: 'assistant',
type: 'error',
content: 'Second error',
retry: oldRetryFn,
read: false,
} as ChatUI.ErrorMessage,
];
const newMessages: ChatRequest.MessageResponse[] = [
{
type: 'error',
role: 'assistant',
content: 'New error',
},
];
const result = builderMessages.processAssistantMessages(
currentMessages,
newMessages,
'test-id',
newRetryFn,
);
expect(result.messages).toHaveLength(3);
// First error should have retry removed
const firstError = result.messages[0] as ChatUI.ErrorMessage;
expect(firstError.content).toBe('First error');
expect('retry' in firstError).toBe(false);
// Second error should have retry removed
const secondError = result.messages[1] as ChatUI.ErrorMessage;
expect(secondError.content).toBe('Second error');
expect('retry' in secondError).toBe(false);
// New error should have the new retry function
const newError = result.messages[2] as ChatUI.ErrorMessage;
expect(newError.content).toBe('New error');
expect(newError.retry).toBe(newRetryFn);
});
it('should only keep retry on the last error message when multiple errors exist', () => {
const retryFn = vi.fn(async () => {});
const currentMessages: ChatUI.AssistantMessage[] = [];
const newMessages: ChatRequest.MessageResponse[] = [
{
type: 'error',
role: 'assistant',
content: 'First error in batch',
},
{
type: 'error',
role: 'assistant',
content: 'Second error in batch',
},
{
type: 'error',
role: 'assistant',
content: 'Third error in batch',
},
];
const result = builderMessages.processAssistantMessages(
currentMessages,
newMessages,
'test-id',
retryFn,
);
expect(result.messages).toHaveLength(3);
// First error should not have retry
const firstError = result.messages[0] as ChatUI.ErrorMessage;
expect(firstError.content).toBe('First error in batch');
expect('retry' in firstError).toBe(false);
// Second error should not have retry
const secondError = result.messages[1] as ChatUI.ErrorMessage;
expect(secondError.content).toBe('Second error in batch');
expect('retry' in secondError).toBe(false);
// Only the last error should have retry
const lastError = result.messages[2] as ChatUI.ErrorMessage;
expect(lastError.content).toBe('Third error in batch');
expect(lastError.retry).toBe(retryFn);
});
it('should handle mixed message types and only affect error messages with retry logic', () => {
const retryFn = vi.fn(async () => {});
const currentMessages: ChatUI.AssistantMessage[] = [
{
id: 'msg-1',
role: 'assistant',
type: 'text',
content: 'Normal message',
read: false,
},
{
id: 'error-1',
role: 'assistant',
type: 'error',
content: 'Old error',
retry: retryFn,
read: false,
} as ChatUI.ErrorMessage,
];
const newMessages: ChatRequest.MessageResponse[] = [
{
type: 'message',
role: 'assistant',
text: 'New text message',
},
{
type: 'error',
role: 'assistant',
content: 'New error message',
},
];
const result = builderMessages.processAssistantMessages(
currentMessages,
newMessages,
'test-id',
retryFn,
);
expect(result.messages).toHaveLength(4);
// Normal text message should be unchanged
expect(result.messages[0]).toMatchObject({
type: 'text',
content: 'Normal message',
});
expect('retry' in result.messages[0]).toBe(false);
// Old error should have retry removed
const oldError = result.messages[1] as ChatUI.ErrorMessage;
expect(oldError.content).toBe('Old error');
expect('retry' in oldError).toBe(false);
// New text message should not have retry
expect(result.messages[2]).toMatchObject({
type: 'text',
content: 'New text message',
});
expect('retry' in result.messages[2]).toBe(false);
// Only the new error should have retry
const newError = result.messages[3] as ChatUI.ErrorMessage;
expect(newError.content).toBe('New error message');
expect(newError.retry).toBe(retryFn);
});
});
describe('applyRatingLogic', () => {
it('should apply rating to the last assistant text message after workflow-updated when no tools are running', () => {
const messages: ChatUI.AssistantMessage[] = [

View File

@@ -97,6 +97,7 @@ export function useBuilderMessages() {
messages: ChatUI.AssistantMessage[],
msg: ChatRequest.MessageResponse,
messageId: string,
retry?: () => Promise<void>,
): boolean {
let shouldClearThinking = false;
@@ -127,6 +128,7 @@ export function useBuilderMessages() {
type: 'error',
content: msg.content,
read: false,
retry,
});
shouldClearThinking = true;
}
@@ -248,6 +250,7 @@ export function useBuilderMessages() {
currentMessages: ChatUI.AssistantMessage[],
newMessages: ChatRequest.MessageResponse[],
baseId: string,
retry?: () => Promise<void>,
): MessageProcessingResult {
const mutableMessages = [...currentMessages];
let shouldClearThinking = false;
@@ -255,22 +258,36 @@ export function useBuilderMessages() {
newMessages.forEach((msg, index) => {
// Generate unique ID for each message in the batch
const messageId = `${baseId}-${index}`;
const clearThinking = processSingleMessage(mutableMessages, msg, messageId);
const clearThinking = processSingleMessage(mutableMessages, msg, messageId, retry);
shouldClearThinking = shouldClearThinking || clearThinking;
});
const thinkingMessage = determineThinkingMessage(mutableMessages);
// Apply rating logic only to messages after workflow-updated
const finalMessages = applyRatingLogic(mutableMessages);
const messagesWithRatingLogic = applyRatingLogic(mutableMessages);
// Remove retry from all error messages except the last one
const messagesWithRetryLogic = removeRetryFromOldErrorMessages(messagesWithRatingLogic);
return {
messages: finalMessages,
messages: messagesWithRetryLogic,
thinkingMessage,
shouldClearThinking: shouldClearThinking && mutableMessages.length > currentMessages.length,
};
}
function removeRetryFromOldErrorMessages(messages: ChatUI.AssistantMessage[]) {
// Remove retry from all error messages except the last one
return messages.map((message, index) => {
if (message.type === 'error' && message.retry && index !== messages.length - 1) {
const { retry, ...messageWithoutRetry } = message;
return messageWithoutRetry;
}
return message;
});
}
function createUserMessage(content: string, id: string): ChatUI.AssistantMessage {
return {
id,

View File

@@ -286,6 +286,7 @@ export const useBuilderStore = defineStore(STORES.BUILDER, () => {
chatMessages.value,
response.messages,
generateMessageId(),
retry,
);
chatMessages.value = result.messages;