import type { ExtractionCatalog } from './voice.providers'; /** Locale-specific guidance. Only the tooth vocabulary and numbering habits differ. */ const LOCALE_NOTES: Record = { 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 }, ]; }