From 52a7359b9a397aed7f197e8c3b40efa2a522820d Mon Sep 17 00:00:00 2001 From: Amin Mousavi Date: Fri, 21 Aug 2026 04:43:19 +0800 Subject: [PATCH] feat(backend): read a spoken tooth number as its FDI code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../src/modules/voice/extraction.prompt.ts | 49 ++++++++++++------- backend/src/modules/voice/extraction.wire.ts | 8 +-- 2 files changed, 35 insertions(+), 22 deletions(-) diff --git a/backend/src/modules/voice/extraction.prompt.ts b/backend/src/modules/voice/extraction.prompt.ts index 86a415f..0df6c22 100644 --- a/backend/src/modules/voice/extraction.prompt.ts +++ b/backend/src/modules/voice/extraction.prompt.ts @@ -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 = { 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, '', diff --git a/backend/src/modules/voice/extraction.wire.ts b/backend/src/modules/voice/extraction.wire.ts index b91aed1..554b99d 100644 --- a/backend/src/modules/voice/extraction.wire.ts +++ b/backend/src/modules/voice/extraction.wire.ts @@ -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;