Files
Neon-Desk/lib/server/task-errors.ts

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