fix: recognize limited exam, 1 PA (#tooth), composite # NN surfaces, and check [patient name]

- Add limited exam / emergency exam → D0140 to DIRECT_CODE_MAP for reliable lookup
- Fix parseCompositeCode regex to allow space between # and digit (# 10 DL, # 11 ML)
- Strip empty parens from strippedCleaned so "1 pa (#13)" → "1 pa" hits DIRECT_CODE_MAP
- LLM prompt: add single-PA-with-tooth examples (1 PA (#13) → ["1 pa, #13"])
- LLM prompt: clarify check_eligibility vs navigate_eligibility — "check Qu" is patient name, not insurance

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-24 09:05:21 -04:00
parent 8154e904b9
commit 528a30efc6
2 changed files with 14 additions and 5 deletions

View File

@@ -147,6 +147,9 @@ const ALIAS_MAP: Record<string, string> = {
// Direct phrase → CDT code for terms not in the MH fee schedule JSON // Direct phrase → CDT code for terms not in the MH fee schedule JSON
// or short abbreviations that need precise mapping (checked before keyword search) // or short abbreviations that need precise mapping (checked before keyword search)
const DIRECT_CODE_MAP: Record<string, string> = { const DIRECT_CODE_MAP: Record<string, string> = {
// Exams
"limited exam": "D0140",
"emergency exam": "D0140",
// X-rays // X-rays
"2bw": "D0272", "2bw": "D0272",
"2 bw": "D0272", "2 bw": "D0272",
@@ -265,7 +268,7 @@ const RCT_MOLAR = new Set([1, 2, 3, 14, 15, 16, 17, 18, 19, 30, 31, 32]); /
function parseCompositeCode(input: string): CdtMatch | null { function parseCompositeCode(input: string): CdtMatch | null {
const s = input.trim(); const s = input.trim();
// Match #NN SURFACES or tooth NN SURFACES (surfaces = 1-5 letters, valid dental surface chars) // Match #NN SURFACES or tooth NN SURFACES (surfaces = 1-5 letters, valid dental surface chars)
const m = s.match(/#(\d{1,2})\s+([A-Za-z]{1,5})\b/) ?? s.match(/\btooth\s+(\d{1,2})\s+([A-Za-z]{1,5})\b/i); const m = s.match(/#\s*(\d{1,2})\s+([A-Za-z]{1,5})\b/) ?? s.match(/\btooth\s+(\d{1,2})\s+([A-Za-z]{1,5})\b/i);
if (!m) return null; if (!m) return null;
const toothNum = parseInt(m[1]!, 10); const toothNum = parseInt(m[1]!, 10);
@@ -467,6 +470,7 @@ export function lookupCdtCodes(
const strippedCleaned = cleaned const strippedCleaned = cleaned
.replace(/,?\s*#\s*\d{1,2}\b/g, "") .replace(/,?\s*#\s*\d{1,2}\b/g, "")
.replace(/,?\s*#\s*[a-t]\b/gi, "") .replace(/,?\s*#\s*[a-t]\b/gi, "")
.replace(/\(\s*\)/g, "")
.replace(/\s+/g, " ") .replace(/\s+/g, " ")
.trim(); .trim();

View File

@@ -71,6 +71,8 @@ Omit any field that is not present in the message or history.
Intents: Intents:
- check_eligibility : user wants to check insurance for a patient identified by NAME only - check_eligibility : user wants to check insurance for a patient identified by NAME only
e.g. "check Maria Jesus", "verify insurance for John Smith" e.g. "check Maria Jesus", "verify insurance for John Smith"
e.g. "check Qu", "check Li", "check Kim" — a short word after "check" is a patient last name, NOT an insurance abbreviation
Insurance abbreviations are: mh, masshealth, bcbs, cca, dentaquest — anything else is a patient name
- eligibility_by_id : user provides a SINGLE member ID and date of birth (no patient name) - eligibility_by_id : user provides a SINGLE member ID and date of birth (no patient name)
e.g. "check masshealth for 100xxxx, 10/10/1988" e.g. "check masshealth for 100xxxx, 10/10/1988"
ALSO use this when user wants to check eligibility AND schedule/add an appointment on a date ALSO use this when user wants to check eligibility AND schedule/add an appointment on a date
@@ -121,8 +123,9 @@ Intents:
Use this when the user says "preauth", "pre auth", "pre-auth", or "prior auth" Use this when the user says "preauth", "pre auth", "pre-auth", or "prior auth"
- navigate_claims : open the claims page - navigate_claims : open the claims page
- navigate_schedule : open the appointments/schedule page - navigate_schedule : open the appointments/schedule page
- navigate_eligibility : open the insurance eligibility page - navigate_eligibility : open the insurance eligibility page — ONLY when user says a known insurance keyword with no patient name
e.g. "check mh", "check masshealth", "open eligibility", "go to eligibility", "check insurance" e.g. "check mh", "check masshealth", "open eligibility", "go to eligibility", "check insurance"
Do NOT use this for "check [name]" — that is check_eligibility
- general : anything else - general : anything else
Rules: Rules:
@@ -134,7 +137,8 @@ Rules:
do NOT include it in procedureNames. It refers to a file attachment, not a billable procedure. do NOT include it in procedureNames. It refers to a file attachment, not a billable procedure.
Only include actual clinical procedures in procedureNames. Only include actual clinical procedures in procedureNames.
- For composite fillings with a tooth number, preserve the EXACT notation including tooth# and surfaces: - For composite fillings with a tooth number, preserve the EXACT notation including tooth# and surfaces:
e.g. "composite #29 O", "#8 MO", "composite #11 MOD" — keep the #number and surface letters together as one entry e.g. "composite #29 O", "#8 MO", "composite #11 MOD", "# 10 DL", "# 11 ML" — keep the #number and surface letters together as one entry
Note: "# 10 DL" and "composite on # 10 DL" are the same — preserve the space-after-# as-is
- #number always means a TOOTH number (never a case or pre-auth reference). When a single #number appears before a comma-separated list of procedures, apply it to EVERY procedure in the list. - #number always means a TOOTH number (never a case or pre-auth reference). When a single #number appears before a comma-separated list of procedures, apply it to EVERY procedure in the list.
e.g. "#20 rct, post, crown" → ["#20 rct", "#20 post", "#20 crown"] e.g. "#20 rct, post, crown" → ["#20 rct", "#20 post", "#20 crown"]
e.g. "preauth #20 rct, pos, crown" → ["#20 rct", "#20 pos", "#20 crown"] e.g. "preauth #20 rct, pos, crown" → ["#20 rct", "#20 pos", "#20 crown"]
@@ -147,8 +151,9 @@ Rules:
The tooth# after a CDT code belongs only to that procedure. The tooth# after a CDT code belongs only to that procedure.
- For SRP with a quadrant abbreviation (UL, UR, LL, LR), keep the code and quadrant together as one entry: - For SRP with a quadrant abbreviation (UL, UR, LL, LR), keep the code and quadrant together as one entry:
e.g. "D4341 UL", "4341 LR", "D4342 UR" — the quadrant always travels with the SRP code e.g. "D4341 UL", "4341 LR", "D4342 UR" — the quadrant always travels with the SRP code
- For multiple PA X-rays with tooth numbers, expand each PA into its own entry: - For PA X-rays with tooth numbers, always use "1 pa, #T" format for the first/only PA and "2nd pa, #T" for every additional:
ALWAYS use "1 pa, #T" for the first tooth and "2nd pa, #T" for EVERY additional tooth (never "3rd pa", "4th pa", etc.) e.g. "1 PA (#13)" → ["1 pa, #13"]
e.g. "PA #13" → ["1 pa, #13"]
e.g. "2 PA (#30, 15)" → ["1 pa, #30", "2nd pa, #15"] e.g. "2 PA (#30, 15)" → ["1 pa, #30", "2nd pa, #15"]
e.g. "3 PA (#3, 14, 30)" → ["1 pa, #3", "2nd pa, #14", "2nd pa, #30"] e.g. "3 PA (#3, 14, 30)" → ["1 pa, #3", "2nd pa, #14", "2nd pa, #30"]
e.g. "4 PA (#3, 14, 19, 30)" → ["1 pa, #3", "2nd pa, #14", "2nd pa, #19", "2nd pa, #30"] e.g. "4 PA (#3, 14, 19, 30)" → ["1 pa, #3", "2nd pa, #14", "2nd pa, #19", "2nd pa, #30"]