feat(backend): read a spoken tooth number as its FDI code

The prompt had this backwards. "Never output an FDI tooth code unless the
speaker used FDI notation. Prefer arch + side + position" pushed the model
to decompose speech into "upper / patient_right / six", so the clinician
effectively had to *describe* every tooth. Saying "دندون بیست و شش" — the
way a dentist actually dictates — was the unsupported path.

FDI is what clinicians speak, so the prompt now teaches the notation
instead of forbidding it: first digit = quadrant from the patient's own
point of view, second digit = position from the midline. arch/side/position
stays as the reading of a *described* tooth, where a single digit is a
position and the quadrant comes from words.

Two guards come with it, because bare numbers are now teeth: a single digit
alone still refuses to guess a quadrant, and dates, counts and quantities
are explicitly not teeth.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-21 04:43:19 +08:00
parent c09698aea2
commit 52a7359b9a
2 changed files with 35 additions and 22 deletions

View File

@@ -3,21 +3,19 @@ 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:',
'The clinician is speaking Persian. A tooth number can be said as a whole number',
'("بیست و شش" = 26), digit by digit ("دو شش" = 26), or with a lead-in',
'("دندون شماره ۲۶"). Digits may arrive in Persian or Latin script — either way, copy',
'the number into "fdi" as two Latin digits. The descriptive form is 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.',
'A bare "دندون دو" carries no quadrant: report position 2 with arch and side null.',
].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.',
'The clinician is speaking Dutch, where FDI is standard. "zesentwintig" and "26" are',
'tooth 26. The descriptive form is "rechtsboven zes" = upper right six.',
].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.',
'The clinician is speaking English and uses FDI. "twenty-six", "two six" and "26" are',
'all tooth 26. The descriptive form is "upper right six".',
].join(' '),
};
@@ -41,19 +39,32 @@ export function buildExtractionPrompt(
'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',
'2. "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.',
'3. 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',
'4. 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',
'5. If you are unsure about a value, use null. A missing field is recoverable; a wrong',
' one is not.',
'7. A tooth number spoken WITHOUT a quadrant ("دندون دو", "tooth two") does not identify',
' a tooth — four teeth carry that position. Still report it: set "position" and leave',
' "arch" and "side" null. Never pick a quadrant that was not said.',
'',
'TOOTH NUMBERS',
'A number the clinician says for a tooth IS that tooth\'s FDI code. Put it in "fdi" as',
'two digits. FDI is built from the two digits:',
" first digit = quadrant, from the PATIENT's own point of view —",
' 1 upper right, 2 upper left, 3 lower left, 4 lower right.',
' (5-8 are those same four quadrants in primary/deciduous teeth.)',
' second digit = position from the midline — 1 central incisor ... 8 third molar.',
'So 26 is the upper left first molar, and 47 is the lower right second molar.',
'',
'- Use "arch" + "side" + "position" only when the tooth is DESCRIBED rather than',
' numbered ("upper right six" -> arch "upper", side "patient_right", position 6).',
'- A single digit is a position, never an FDI code. If a single digit is said with no',
' quadrant words at all, set "position" and leave "arch" and "side" null. Never pick a',
' quadrant that was not said.',
'- If a number is given AND the quadrant is spelled out as well, still use "fdi".',
'- Not every number is a tooth. Dates, counts and quantities ("two teeth", "the 26th")',
' are not teeth, and must never appear in the teeth list.',
'',
localeNote,
'',

View File

@@ -21,7 +21,7 @@ import { WEEKDAYS } from './voice.types';
export type WireToothIntent = {
spoken: string;
/** Two-digit FDI code, only when the speaker genuinely used FDI notation. */
/** The two-digit FDI code the clinician spoke; null when the tooth was described. */
fdi: string | null;
arch: 'upper' | 'lower' | null;
side: 'patient_right' | 'patient_left' | null;
@@ -66,7 +66,8 @@ const TOOTH_SCHEMA = {
fdi: {
type: ['string', 'null'],
description:
'Two-digit FDI code ONLY if the speaker used FDI notation. Otherwise null.',
'The two-digit FDI code the clinician said for this tooth, e.g. "26". Null only ' +
'when the tooth was described in words instead of numbered.',
},
arch: { type: ['string', 'null'], enum: ['upper', 'lower', null] },
side: {
@@ -76,7 +77,8 @@ const TOOTH_SCHEMA = {
},
position: {
type: ['integer', 'null'],
description: '1 = central incisor … 8 = third molar.',
description:
'Position from the midline: 1 = central incisor … 8 = third molar. Never an FDI code.',
},
},
} as const;