Improve workflow error messaging
This commit is contained in:
154
lib/server/task-errors.ts
Normal file
154
lib/server/task-errors.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import { stageLabel, taskTypeLabel } from '@/lib/task-workflow';
|
||||
import type { TaskStage, TaskStageContext, TaskType } from '@/lib/types';
|
||||
|
||||
type TaskErrorSource = {
|
||||
task_type: TaskType;
|
||||
stage?: TaskStage | null;
|
||||
stage_context?: TaskStageContext | null;
|
||||
payload?: Record<string, unknown> | null;
|
||||
};
|
||||
|
||||
export type TaskFailureMessage = {
|
||||
summary: string;
|
||||
detail: string;
|
||||
};
|
||||
|
||||
function rawMessage(error: unknown) {
|
||||
if (error instanceof Error && error.message.trim().length > 0) {
|
||||
return error.message.trim();
|
||||
}
|
||||
|
||||
if (typeof error === 'string' && error.trim().length > 0) {
|
||||
return error.trim();
|
||||
}
|
||||
|
||||
return 'Task failed unexpectedly';
|
||||
}
|
||||
|
||||
function normalizeSentence(value: string) {
|
||||
const collapsed = value
|
||||
.split('\n')[0]!
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
|
||||
if (!collapsed) {
|
||||
return 'Task failed unexpectedly.';
|
||||
}
|
||||
|
||||
const sentence = collapsed[0]!.toUpperCase() + collapsed.slice(1);
|
||||
return /[.!?]$/.test(sentence) ? sentence : `${sentence}.`;
|
||||
}
|
||||
|
||||
function asString(value: unknown) {
|
||||
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
|
||||
}
|
||||
|
||||
function subjectLabel(task: TaskErrorSource) {
|
||||
const subject = task.stage_context?.subject;
|
||||
const ticker = subject?.ticker ?? asString(task.payload?.ticker);
|
||||
const accessionNumber = subject?.accessionNumber ?? asString(task.payload?.accessionNumber);
|
||||
const label = subject?.label ?? asString(task.payload?.label);
|
||||
const parts = [ticker, accessionNumber, label].filter((part): part is string => Boolean(part));
|
||||
|
||||
if (parts.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return parts.join(' · ');
|
||||
}
|
||||
|
||||
function stagePhrase(stage?: TaskStage | null) {
|
||||
if (!stage || stage === 'queued' || stage === 'running' || stage === 'completed' || stage === 'failed') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return stageLabel(stage).toLowerCase();
|
||||
}
|
||||
|
||||
function genericFailure(task: TaskErrorSource, message: string): TaskFailureMessage {
|
||||
const failedStage = stagePhrase(task.stage);
|
||||
const subject = subjectLabel(task);
|
||||
const summary = failedStage
|
||||
? `Failed during ${failedStage}.`
|
||||
: `${taskTypeLabel(task.task_type)} failed.`;
|
||||
const detail = [
|
||||
failedStage
|
||||
? `${taskTypeLabel(task.task_type)} failed during ${failedStage}`
|
||||
: `${taskTypeLabel(task.task_type)} failed`,
|
||||
subject ? `for ${subject}` : null,
|
||||
'. ',
|
||||
normalizeSentence(message)
|
||||
].filter(Boolean).join('');
|
||||
|
||||
return {
|
||||
summary,
|
||||
detail
|
||||
};
|
||||
}
|
||||
|
||||
export function describeTaskFailure(task: TaskErrorSource, error: unknown): TaskFailureMessage {
|
||||
const message = rawMessage(error);
|
||||
const normalized = message.toLowerCase();
|
||||
const failedStage = stagePhrase(task.stage);
|
||||
const subject = subjectLabel(task);
|
||||
|
||||
if (normalized.includes('zhipu_api_key is required')) {
|
||||
if (task.task_type === 'index_search') {
|
||||
return {
|
||||
summary: 'Search indexing could not generate embeddings.',
|
||||
detail: `Search indexing could not generate embeddings${subject ? ` for ${subject}` : ''} because ZHIPU_API_KEY is not configured. Add the API key and retry the job.`
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
summary: 'AI configuration is incomplete.',
|
||||
detail: `${taskTypeLabel(task.task_type)} could not continue${subject ? ` for ${subject}` : ''} because ZHIPU_API_KEY is not configured. Add the API key and retry the job.`
|
||||
};
|
||||
}
|
||||
|
||||
if (normalized.includes('ai sdk returned an empty response')) {
|
||||
return {
|
||||
summary: failedStage
|
||||
? `No usable AI response during ${failedStage}.`
|
||||
: 'The AI provider returned an empty response.',
|
||||
detail: `The AI provider returned an empty response${failedStage ? ` during ${failedStage}` : ''}${subject ? ` for ${subject}` : ''}. Retry the job. If this keeps happening, verify the model configuration and provider health.`
|
||||
};
|
||||
}
|
||||
|
||||
if (normalized.includes('extraction output invalid json schema')) {
|
||||
return {
|
||||
summary: 'The extraction response was not valid JSON.',
|
||||
detail: `The AI model returned extraction data in an unexpected format${subject ? ` for ${subject}` : ''}, so filing analysis could not continue. Retry the job. If it repeats, inspect the model output or prompt.`
|
||||
};
|
||||
}
|
||||
|
||||
if (normalized.includes('workflow run cancelled')) {
|
||||
return {
|
||||
summary: 'The background workflow was cancelled.',
|
||||
detail: `The background workflow was cancelled before ${taskTypeLabel(task.task_type).toLowerCase()} could finish${subject ? ` for ${subject}` : ''}. Retry the job if you still need the result.`
|
||||
};
|
||||
}
|
||||
|
||||
if (normalized.includes('workflow run failed')) {
|
||||
return {
|
||||
summary: 'The background workflow stopped unexpectedly.',
|
||||
detail: `The background workflow stopped unexpectedly before ${taskTypeLabel(task.task_type).toLowerCase()} could finish${subject ? ` for ${subject}` : ''}. Retry the job. If it fails again, inspect the task details for the last completed step.`
|
||||
};
|
||||
}
|
||||
|
||||
if (normalized.includes('failed to start workflow')) {
|
||||
return {
|
||||
summary: 'The background worker could not start this job.',
|
||||
detail: `The background worker could not start ${taskTypeLabel(task.task_type).toLowerCase()}${subject ? ` for ${subject}` : ''}. The workflow backend may be unavailable. Retry the job once the workflow service is healthy.`
|
||||
};
|
||||
}
|
||||
|
||||
if (normalized.includes('embedding')) {
|
||||
return {
|
||||
summary: 'Embedding generation failed.',
|
||||
detail: `Search indexing could not generate embeddings${subject ? ` for ${subject}` : ''}. ${normalizeSentence(message)}`
|
||||
};
|
||||
}
|
||||
|
||||
return genericFailure(task, message);
|
||||
}
|
||||
Reference in New Issue
Block a user