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 | null; }; 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); }