155 lines
5.4 KiB
TypeScript
155 lines
5.4 KiB
TypeScript
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;
|
|
};
|
|
|
|
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);
|
|
}
|