diff --git a/src/pages/nearle/assistant/intents.js b/src/pages/nearle/assistant/intents.js index 28d9125..bcabeb7 100644 --- a/src/pages/nearle/assistant/intents.js +++ b/src/pages/nearle/assistant/intents.js @@ -109,11 +109,37 @@ const KEYWORD_VOCAB = [ 'available' ]; -const correctTypos = (text) => - text +// Words that form a NAMED ENTITY must never be "corrected". A tenant called +// "Partnerz" is one edit from "partners" and a rider called "Delivara" is two +// from "delivered"; rewriting either makes the name unresolvable, and +// orderQuery extracts tenant/rider names from the corrected text, so the +// failure is silent — the filter just never matches. +// +// The original comment here claimed order IDs and tenant names were never +// touched. Nothing enforced that; this does. +const ENTITY_NAME_SPAN = /\b(?:rider|tenant|hub|vehicle|customer|partner|named|called|for)\s+((?:[A-Za-z][A-Za-z0-9.'-]*\s*){1,4})/gi; + +const protectedNameWords = (text) => { + const keep = new Set(); + const re = new RegExp(ENTITY_NAME_SPAN.source, 'gi'); + let m = re.exec(text); + while (m !== null) { + m[1] + .split(/\s+/) + .filter(Boolean) + .forEach((w) => keep.add(w.toLowerCase())); + m = re.exec(text); + } + return keep; +}; + +const correctTypos = (text) => { + const keep = protectedNameWords(text); + return text .split(/\b/) .map((token) => { const word = token.toLowerCase(); + if (keep.has(word)) return token; if (!/^[a-z]+$/.test(word) || word.length < 5 || KEYWORD_VOCAB.includes(word)) return token; // >=6 chars allows distance 2, which is what a single adjacent-letter // transposition ("riedrs" for "riders") costs in plain Levenshtein @@ -132,6 +158,7 @@ const correctTypos = (text) => return best && bestDist <= maxDist ? best : token; }) .join(''); +}; const TODAY = () => dayjs().format('YYYY-MM-DD'); @@ -789,9 +816,18 @@ const INTENTS = [ ); if (!found) return null; return { - headline: `${found.displayname || found.name} — ${found.status || 'status unknown'}.`, + // Field names matter here: a miler has NO `status`, `phonenumber` or + // `vehicletype` field. The real ones are `availabilitystatus`, + // `phone` and `defaultvehicletype` (api.js documents the confirmed + // live shape). Reading the wrong names meant this answer always said + // "status unknown" and never showed a phone or vehicle. + headline: `${found.displayname || found.name} — ${found.availabilitystatus || 'availability unknown'}.`, detail: - [found.phonenumber ? `Phone: ${found.phonenumber}` : null, found.vehicletype ? `Vehicle: ${found.vehicletype}` : null] + [ + found.phone ? `Phone: ${found.phone}` : null, + found.defaultvehicletype ? `Vehicle: ${found.defaultvehicletype}` : null, + found.hubname ? `Hub: ${found.hubname}` : null + ] .filter(Boolean) .join('\n') || undefined, sourceCalls: [{ name: 'getMilers', target: '/admin/milers', status: 'complete', stats: `matched "${name}"` }]