79 lines
3.4 KiB
TypeScript
79 lines
3.4 KiB
TypeScript
|
|
import type { ExtractionCatalog } from './voice.providers';
|
||
|
|
|
||
|
|
/** Locale-specific guidance. Only the tooth vocabulary and numbering habits differ. */
|
||
|
|
const LOCALE_NOTES: Record<string, string> = {
|
||
|
|
fa: [
|
||
|
|
'The clinician is speaking Persian. Tooth references are usually quadrant-relative:',
|
||
|
|
'"شش بالا راست" = upper right six -> arch "upper", side "patient_right", position 6.',
|
||
|
|
'Digits may appear in Persian or Latin script. Two-digit FDI notation ("یک چهار") does',
|
||
|
|
'occur — use the "fdi" field only for that.',
|
||
|
|
].join(' '),
|
||
|
|
nl: [
|
||
|
|
'The clinician is speaking Dutch and uses FDI notation, which is standard in the',
|
||
|
|
'Netherlands. "rechtsboven zes" = upper right six. A bare two-digit number is FDI.',
|
||
|
|
].join(' '),
|
||
|
|
en: [
|
||
|
|
'The clinician is speaking English. IMPORTANT: a bare two-digit number is ambiguous,',
|
||
|
|
'because Universal numbering and FDI disagree ("tooth 14" is a different tooth in each).',
|
||
|
|
'Set "fdi" ONLY when the speaker made the notation explicit (e.g. "FDI one four").',
|
||
|
|
'Otherwise describe the tooth with arch/side/position, or leave it unresolved.',
|
||
|
|
].join(' '),
|
||
|
|
};
|
||
|
|
|
||
|
|
function codeList(entries: { code: string; label: string }[]): string {
|
||
|
|
if (entries.length === 0) return '(none available)';
|
||
|
|
return entries.map((e) => `- ${e.code} = ${e.label}`).join('\n');
|
||
|
|
}
|
||
|
|
|
||
|
|
export function buildExtractionPrompt(
|
||
|
|
transcript: string,
|
||
|
|
catalog: ExtractionCatalog,
|
||
|
|
localeHint: string,
|
||
|
|
) {
|
||
|
|
const localeNote = LOCALE_NOTES[localeHint] ?? LOCALE_NOTES.en;
|
||
|
|
|
||
|
|
const system = [
|
||
|
|
'You extract structured dental treatment data from a transcript of a clinician speaking.',
|
||
|
|
'You are a parser, not an assistant: report only what was said.',
|
||
|
|
'',
|
||
|
|
'HARD RULES',
|
||
|
|
'1. Never invent a code. treatmentType, prosthesisDefaultType and prosthesisOverrides[].type',
|
||
|
|
' must be codes from the lists below. labId must be an id from the lab list. If what you',
|
||
|
|
' heard is not in a list, use null.',
|
||
|
|
'2. Never output an FDI tooth code unless the speaker used FDI notation. Prefer',
|
||
|
|
' arch + side + position.',
|
||
|
|
'3. "side" is always the PATIENT\'s side. The patient\'s upper right is quadrant 1. Never',
|
||
|
|
" flip to the viewer's point of view.",
|
||
|
|
'4. Never do calendar arithmetic. Report the deadline as it was said, using due.kind.',
|
||
|
|
' If no deadline was mentioned, use due.kind = "none".',
|
||
|
|
'5. Copy the exact spoken words for each tooth into "spoken", so the clinician can see',
|
||
|
|
' what was heard.',
|
||
|
|
'6. If you are unsure about a value, use null. A missing field is recoverable; a wrong',
|
||
|
|
' one is not.',
|
||
|
|
'',
|
||
|
|
localeNote,
|
||
|
|
'',
|
||
|
|
'TREATMENT TYPE CODES',
|
||
|
|
codeList(catalog.treatmentTypes),
|
||
|
|
'',
|
||
|
|
'PROSTHESIS TYPE CODES',
|
||
|
|
codeList(catalog.prosthesisTypes),
|
||
|
|
'',
|
||
|
|
'LABS THIS CLINIC CAN SEND TO',
|
||
|
|
catalog.labs.length > 0
|
||
|
|
? catalog.labs.map((l) => `- ${l.id} = ${l.name}`).join('\n')
|
||
|
|
: '(none linked — labId must be null)',
|
||
|
|
'',
|
||
|
|
'OTHER FIELDS',
|
||
|
|
'- connectedSpans: only for bridges or splinted units. Endpoints inclusive.',
|
||
|
|
'- comment: clinical notes, in the language spoken. Omit the parts already captured as',
|
||
|
|
' treatment type, teeth or deadline.',
|
||
|
|
'- labMatchExact: true only when the spoken name matched a lab name exactly.',
|
||
|
|
].join('\n');
|
||
|
|
|
||
|
|
return [
|
||
|
|
{ role: 'system' as const, content: system },
|
||
|
|
{ role: 'user' as const, content: transcript },
|
||
|
|
];
|
||
|
|
}
|