Files
DentalManagementMH06/apps/Backend/src/ai/reschedule-graph.ts
Gitead 112529155c feat: persist AI conversation state in DB and fix LangGraph flow bugs
- Replace in-memory Maps in aiHandoffStore with DB-backed async functions
  using new patient_conversation table (stage + aiHandoff per patient)
- Add afterHoursEnabled to ai_settings table (persists across restarts)
- Fix runtime crash in reschedule-graph: mon/tue/wed variables were out
  of scope in the next-week fallback branch (ReferenceError)
- Wire rescheduleGreeting and generalFallback chat templates through to
  LangGraph nodes so user-configured messages take effect
- Add otherNode to reminder-graph to handle unclassified patient replies
  (e.g. "I want another appointment") and route to booking flow
- Fetch chatTemplates once per webhook request instead of per stage

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 15:23:55 -04:00

472 lines
25 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
import { prisma as db } from "@repo/db/client";
import { storage } from "../storage";
import {
type ConversationStage,
setPendingReschedule,
getPendingReschedule,
clearPendingReschedule,
} from "./aiHandoffStore";
// ── Date helpers ──────────────────────────────────────────────────────────────
const MONTHS = [
"January","February","March","April","May","June",
"July","August","September","October","November","December",
];
const DAYS = ["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"];
function formattedDate(d: Date): string {
return `${DAYS[d.getDay()]}, ${MONTHS[d.getMonth()]} ${d.getDate()}`;
}
/** Get the day-of-week (0=Sun…6=Sat) of the patient's next scheduled appointment. */
async function getAppointmentDow(patientId: number): Promise<number> {
const today = new Date(); today.setHours(0, 0, 0, 0);
const appt = await db.appointment.findFirst({
where: { patientId, status: "scheduled", date: { gte: today } },
orderBy: { date: "asc" },
select: { date: true },
});
if (!appt) return -1;
return new Date(appt.date).getUTCDay();
}
function getTomorrow(): string {
const t = new Date();
t.setDate(t.getDate() + 1);
return formattedDate(t);
}
function getNextWeekDays(): { mon: string; tue: string; wed: string } {
const today = new Date();
const dow = today.getDay();
// Days until next Monday (always at least 1 day away, never 0)
const daysToMon = (8 - dow) % 7 || 7;
const mon = new Date(today); mon.setDate(today.getDate() + daysToMon);
const tue = new Date(mon); tue.setDate(mon.getDate() + 1);
const wed = new Date(mon); wed.setDate(mon.getDate() + 2);
return { mon: formattedDate(mon), tue: formattedDate(tue), wed: formattedDate(wed) };
}
// ── Date objects for rescheduling ─────────────────────────────────────────────
function getTomorrowDate(): { date: Date; label: string } {
const d = new Date();
d.setDate(d.getDate() + 1);
d.setUTCHours(0, 0, 0, 0);
return { date: d, label: formattedDate(new Date(d.getTime() + d.getTimezoneOffset() * 60000)) };
}
function getNextWeekDateObjects(): { mon: { date: Date; label: string }; tue: { date: Date; label: string }; wed: { date: Date; label: string } } {
const today = new Date();
const dow = today.getDay();
const daysToMon = (8 - dow) % 7 || 7;
const mkDate = (offset: number) => {
const d = new Date(today);
d.setDate(today.getDate() + offset);
d.setUTCHours(0, 0, 0, 0);
const label = formattedDate(new Date(today.getFullYear(), today.getMonth(), today.getDate() + offset));
return { date: d, label };
};
return { mon: mkDate(daysToMon), tue: mkDate(daysToMon + 1), wed: mkDate(daysToMon + 2) };
}
// ── Time parsing ──────────────────────────────────────────────────────────────
/** Parse patient's time preference into 24-h "HH:MM" string or null. */
async function parseTime(message: string, apiKey: string): Promise<string | null> {
const t = message.toLowerCase();
// Keyword shortcuts
if (/\bmorning\b|mañana|manhã|上午|صباح|maten/i.test(t)) return "09:00";
if (/\bafternoon\b|tarde|après-midi|下午|مساء|aprèmidi/i.test(t)) return "13:00";
// Numeric patterns: "10am", "10:30", "2pm", "14:00"
const ampm = t.match(/\b(\d{1,2})(?::(\d{2}))?\s*(am|pm)\b/);
const clock = t.match(/\b([01]?\d|2[0-3]):([0-5]\d)\b/);
if (ampm) {
let h = parseInt(ampm[1]!);
const m = ampm[2] ? parseInt(ampm[2]) : 0;
if (ampm[3] === "pm" && h < 12) h += 12;
if (ampm[3] === "am" && h === 12) h = 0;
return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`;
}
if (clock) return clock[0]!;
// LLM fallback
try {
const llm = new ChatGoogleGenerativeAI({ model: "gemini-1.5-flash", apiKey });
const res = await llm.invoke([
{ role: "system", content: 'Extract the time from the message. Return ONLY a 24-hour time in "HH:MM" format (e.g., "10:00", "14:30"). If no time is mentioned, return "null".' },
{ role: "user", content: message },
]);
const raw = String(res.content).trim();
if (/^([01]?\d|2[0-3]):[0-5]\d$/.test(raw)) return raw;
} catch { /* fall through */ }
return null;
}
// ── Appointment update ────────────────────────────────────────────────────────
async function moveAppointment(
patientId: number,
newDate: Date,
newStartTime: string, // "HH:MM"
): Promise<string> {
const today = new Date(); today.setHours(0, 0, 0, 0);
const appt = await db.appointment.findFirst({
where: { patientId, status: "scheduled", date: { gte: today } },
orderBy: { date: "asc" },
});
if (!appt) return "no_appointment";
// Preserve original duration
const [sh, sm] = appt.startTime.split(":").map(Number);
const [eh, em] = appt.endTime.split(":").map(Number);
const durationMin = (eh! * 60 + em!) - (sh! * 60 + sm!);
const [nh, nm] = newStartTime.split(":").map(Number);
const endTotalMin = nh! * 60 + nm! + durationMin;
const newEndTime = `${String(Math.floor(endTotalMin / 60)).padStart(2, "0")}:${String(endTotalMin % 60).padStart(2, "0")}`;
await storage.updateAppointment(appt.id, {
date: newDate,
startTime: newStartTime,
endTime: newEndTime,
status: "scheduled",
} as any);
return "ok";
}
// ── Intent classifiers ────────────────────────────────────────────────────────
function yes(t: string) {
return /\b(yes|yeah|yep|sure|ok|okay|please|absolutely|definitely|sí|si|sim|好的|نعم|wi|oke)\b/i.test(t);
}
function no(t: string) {
return /\b(no|nope|not|don't|won't|can't|لا|pa ka|não)\b/i.test(t);
}
function wantsAsap(t: string) {
return /\b(asap|soon|soonest|next day|tomorrow|quick|fastest|earliest|lo antes|mañana|amanhã|明天|明日|غدًا|demen)\b/i.test(t);
}
function wantsNextWeek(t: string) {
return /\b(next week|semana|prochain|prochaine|下周|下週|الأسبوع القادم|semèn pwochèn)\b/i.test(t);
}
function prefersMonday(t: string) { return /monday|lunes|segunda|周一|月曜|الاثنين|lendi/i.test(t); }
function prefersTuesday(t: string) { return /tuesday|martes|terça|周二|火曜|الثلاثاء|madi/i.test(t); }
function prefersWednesday(t: string) { return /wednesday|miércoles|quarta|周三|水曜|الأربعاء|mèkredi/i.test(t); }
// ── LLM helper ────────────────────────────────────────────────────────────────
async function llmReply(system: string, user: string, fallback: string, apiKey: string): Promise<string> {
try {
const llm = new ChatGoogleGenerativeAI({ model: "gemini-1.5-flash", apiKey });
const res = await llm.invoke([
{ role: "system", content: system },
{ role: "user", content: user },
]);
return String(res.content).trim() || fallback;
} catch {
return fallback;
}
}
// ── Transfer fallback ─────────────────────────────────────────────────────────
const TRANSFER: Record<string, string> = {
English: "Our office staff will contact you shortly to help with rescheduling.",
Spanish: "El personal de nuestra oficina le contactará pronto para ayudarle a reprogramar.",
Portuguese: "Nossa equipe entrará em contato em breve para ajudá-lo a reagendar.",
Mandarin: "我们的工作人员将很快与您联系以帮助重新安排预约。",
Cantonese: "我們的工作人員將很快聯絡您以協助重新安排預約。",
Arabic: "سيتصل بك موظفو مكتبنا قريباً لمساعدتك في إعادة الجدولة.",
"Haitian Creole": "Anplwaye biwo nou yo pral kontakte ou byento pou ede ou repwograme.",
};
// ── Main step function ────────────────────────────────────────────────────────
export async function runRescheduleStep(
message: string,
stage: ConversationStage,
language: string,
patientId: number,
apiKey: string,
userId: number = 0,
): Promise<{ reply: string; nextStage: ConversationStage }> {
const lang = language || "English";
const t = message.toLowerCase();
const tx = TRANSFER[lang] ?? TRANSFER["English"]!;
// ── asked_reschedule_confirm: patient answered "Would you like to reschedule?" ──
if (stage === "asked_reschedule_confirm") {
if (no(t)) {
const fallbacks: Record<string, string> = {
English: "No problem! Feel free to reach out whenever you're ready. Have a great day!",
Spanish: "¡Sin problema! No dude en contactarnos cuando esté listo. ¡Que tenga un buen día!",
Portuguese: "Sem problema! Entre em contato quando estiver pronto. Tenha um ótimo dia!",
Mandarin: "没关系!随时联系我们。祝您有美好的一天!",
Cantonese: "沒問題!隨時聯繫我們。祝您有美好的一天!",
Arabic: "لا بأس! لا تتردد في التواصل معنا متى كنت مستعداً. أتمنى لك يوماً رائعاً!",
"Haitian Creole": "Pa gen pwoblèm! Kontakte nou nenpòt ki lè ou prèt. Pase yon bèl jounen!",
};
const fallback = fallbacks[lang] ?? fallbacks["English"]!;
const reply = await llmReply(
`You are a friendly dental office assistant. The patient does not want to reschedule. Write a warm, brief closing message in ${lang}. 1 sentence, no formatting.`,
`Patient said: "${message}"`, fallback, apiKey
);
return { reply, nextStage: "done" };
}
if (yes(t)) {
// Check if original appointment was MonThu (days 14)
const dow = await getAppointmentDow(patientId);
const isMonToThu = dow >= 1 && dow <= 4;
if (isMonToThu) {
// Offer ASAP or next week
const fallbacks: Record<string, string> = {
English: "Would you like to reschedule as soon as possible, or would you prefer next week?",
Spanish: "¿Le gustaría reprogramar lo antes posible, o prefiere la semana que viene?",
Portuguese: "Gostaria de reagendar o mais rápido possível, ou prefere a semana que vem?",
Mandarin: "您想尽快重新安排预约,还是下周更方便?",
Cantonese: "您想盡快重新安排預約,還是下週更方便?",
Arabic: "هل تفضل إعادة الجدولة في أقرب وقت ممكن، أم تفضل الأسبوع القادم؟",
"Haitian Creole": "Èske ou ta renmen repwograme pi vit posib, oswa ou prefere semèn pwochèn?",
};
const fallback = fallbacks[lang] ?? fallbacks["English"]!;
const reply = await llmReply(
`You are a friendly dental office assistant. The patient wants to reschedule. Their original appointment was on a weekday. Ask in ${lang} whether they prefer to reschedule as soon as possible or next week. 1 sentence, no formatting.`,
`Patient said: "${message}"`, fallback, apiKey
);
return { reply, nextStage: "asked_reschedule_preference" };
} else {
// Original appointment was Fri/Sat/Sun — go straight to next week
const { mon, tue, wed } = getNextWeekDays();
const fallbacks: Record<string, string> = {
English: `I can schedule you for next week. Would ${mon}, ${tue}, or ${wed} work for you?`,
Spanish: `Puedo programarle para la semana que viene. ¿Le vendría bien el ${mon}, ${tue} o el ${wed}?`,
Portuguese: `Posso agendá-lo para a semana que vem. ${mon}, ${tue} ou ${wed} seria bom para você?`,
Mandarin: `我可以安排您在下周预约。${mon}${tue}${wed} 方便吗?`,
Cantonese: `我可以安排您在下週預約。${mon}${tue}${wed} 方便嗎?`,
Arabic: `يمكنني جدولتك للأسبوع القادم. هل ${mon} أو ${tue} أو ${wed} مناسب لك؟`,
"Haitian Creole": `Mwen ka pwograme ou pou semèn pwochèn. ${mon}, ${tue}, oswa ${wed} ka travay pou ou?`,
};
const fallback = fallbacks[lang] ?? fallbacks["English"]!;
const reply = await llmReply(
`You are a friendly dental office assistant. Offer the patient next week's Monday (${mon}), Tuesday (${tue}), or Wednesday (${wed}) in ${lang}. 1-2 sentences, no formatting.`,
`Patient wants to reschedule.`, fallback, apiKey
);
return { reply, nextStage: "asked_reschedule_next_week" };
}
}
return { reply: tx, nextStage: "done" };
}
// ── asked_reschedule_preference: patient chose ASAP or next week ──────────
if (stage === "asked_reschedule_preference") {
if (wantsAsap(t) || yes(t)) {
const tomorrow = getTomorrow();
const fallbacks: Record<string, string> = {
English: `Can you come in tomorrow, ${tomorrow}?`,
Spanish: `¿Podría venir mañana, ${tomorrow}?`,
Portuguese: `Você pode vir amanhã, ${tomorrow}?`,
Mandarin: `您明天(${tomorrow})能来吗?`,
Cantonese: `您明天(${tomorrow})能來嗎?`,
Arabic: `هل يمكنك الحضور غداً، ${tomorrow}؟`,
"Haitian Creole": `Èske ou ka vini demen, ${tomorrow}?`,
};
const fallback = fallbacks[lang] ?? fallbacks["English"]!;
const reply = await llmReply(
`You are a friendly dental office assistant. Ask the patient in ${lang} if they can come in tomorrow, ${tomorrow}. 1 sentence, no formatting.`,
`Patient wants to reschedule ASAP.`, fallback, apiKey
);
return { reply, nextStage: "asked_reschedule_asap" };
}
if (wantsNextWeek(t)) {
const { mon, tue, wed } = getNextWeekDays();
const fallbacks: Record<string, string> = {
English: `I can schedule you for next week. Would ${mon}, ${tue}, or ${wed} work for you?`,
Spanish: `Puedo programarle para la semana que viene. ¿Le vendría bien el ${mon}, ${tue} o el ${wed}?`,
Portuguese: `Posso agendá-lo para a semana que vem. ${mon}, ${tue} ou ${wed} seria bom para você?`,
Mandarin: `我可以安排您在下周预约。${mon}${tue}${wed} 方便吗?`,
Cantonese: `我可以安排您在下週預約。${mon}${tue}${wed} 方便嗎?`,
Arabic: `يمكنني جدولتك للأسبوع القادم. هل ${mon} أو ${tue} أو ${wed} مناسب لك؟`,
"Haitian Creole": `Mwen ka pwograme ou pou semèn pwochèn. ${mon}, ${tue}, oswa ${wed} ka travay pou ou?`,
};
const fallback = fallbacks[lang] ?? fallbacks["English"]!;
const reply = await llmReply(
`You are a friendly dental office assistant. Offer next week's Monday (${mon}), Tuesday (${tue}), or Wednesday (${wed}) in ${lang}. 1-2 sentences, no formatting.`,
`Patient prefers next week.`, fallback, apiKey
);
return { reply, nextStage: "asked_reschedule_next_week" };
}
return { reply: tx, nextStage: "done" };
}
// ── asked_reschedule_asap: patient answered "Can you come tomorrow?" ───────
if (stage === "asked_reschedule_asap") {
if (yes(t)) {
const { date, label } = getTomorrowDate();
setPendingReschedule(userId, patientId, { newDate: date, dayLabel: label });
const fallbacks: Record<string, string> = {
English: `${label} it is! Would you prefer morning (9am12pm) or afternoon (1pm5pm)?`,
Spanish: `¡${label} perfecto! ¿Prefiere la mañana (9am12pm) o la tarde (1pm5pm)?`,
Portuguese: `${label} ótimo! Você prefere manhã (9h12h) ou tarde (13h17h)?`,
Mandarin: `${label}太好了您想预约上午9点12点还是下午1点5点`,
Cantonese: `${label}太好了您想預約上午9點12點還是下午1點5點`,
Arabic: `${label} ممتاز! هل تفضل الصباح (9ص12م) أم بعد الظهر (1م5م)؟`,
"Haitian Creole": `${label} pafè! Èske ou prefere maten (9am12pm) oswa apremidi (1pm5pm)?`,
};
const fallback = fallbacks[lang] ?? fallbacks["English"]!;
const reply = await llmReply(
`You are a friendly dental office assistant. The patient confirmed ${label}. Ask in ${lang} whether they prefer morning (9am-12pm) or afternoon (1pm-5pm). 1 sentence, no formatting.`,
`Patient confirmed tomorrow.`, fallback, apiKey
);
return { reply, nextStage: "asked_reschedule_time" };
}
if (no(t)) {
// Can't make tomorrow — offer next week instead
const { mon, tue, wed } = getNextWeekDays();
const fallbacks: Record<string, string> = {
English: `No problem! What about next week? Would ${mon}, ${tue}, or ${wed} work for you?`,
Spanish: `¡Sin problema! ¿Qué le parece la semana que viene? ¿Le vendría bien el ${mon}, ${tue} o el ${wed}?`,
Portuguese: `Sem problema! E na semana que vem? ${mon}, ${tue} ou ${wed} seria bom?`,
Mandarin: `没关系!下周怎么样?${mon}${tue}${wed} 方便吗?`,
Cantonese: `沒問題!下週怎麼樣?${mon}${tue}${wed} 方便嗎?`,
Arabic: `لا بأس! ماذا عن الأسبوع القادم؟ هل ${mon} أو ${tue} أو ${wed} يناسبك؟`,
"Haitian Creole": `Pa gen pwoblèm! Ki sa ki dire semèn pwochèn? ${mon}, ${tue}, oswa ${wed} ka travay?`,
};
const fallback = fallbacks[lang] ?? fallbacks["English"]!;
const reply = await llmReply(
`You are a friendly dental office assistant. The patient cannot come tomorrow. Offer next week: ${mon}, ${tue}, or ${wed} in ${lang}. 1-2 sentences, no formatting.`,
`Patient can't come tomorrow.`, fallback, apiKey
);
return { reply, nextStage: "asked_reschedule_next_week" };
}
return { reply: tx, nextStage: "done" };
}
// ── asked_reschedule_next_week: patient choosing Mon/Tue/Wed ─────────────
if (stage === "asked_reschedule_next_week") {
const nwd = getNextWeekDateObjects();
let chosen: { date: Date; label: string } | null = null;
if (prefersMonday(t)) chosen = nwd.mon;
if (prefersTuesday(t)) chosen = nwd.tue;
if (prefersWednesday(t)) chosen = nwd.wed;
if (chosen) {
setPendingReschedule(userId, patientId, { newDate: chosen.date, dayLabel: chosen.label });
const day = chosen.label;
const fallbacks: Record<string, string> = {
English: `${day} works! Would you prefer morning (9am12pm) or afternoon (1pm5pm)?`,
Spanish: `¡${day} perfecto! ¿Prefiere la mañana (9am12pm) o la tarde (1pm5pm)?`,
Portuguese: `${day} ótimo! Você prefere manhã (9h12h) ou tarde (13h17h)?`,
Mandarin: `${day}好的您想预约上午9点12点还是下午1点5点`,
Cantonese: `${day}好的您想預約上午9點12點還是下午1點5點`,
Arabic: `${day} رائع! هل تفضل الصباح (9ص12م) أم بعد الظهر (1م5م)؟`,
"Haitian Creole": `${day} pafè! Èske ou prefere maten (9am12pm) oswa apremidi (1pm5pm)?`,
};
const fallback = fallbacks[lang] ?? fallbacks["English"]!;
const reply = await llmReply(
`You are a friendly dental office assistant. The patient chose ${day}. Ask in ${lang} whether they prefer morning (9am-12pm) or afternoon (1pm-5pm). 1 sentence, no formatting.`,
`Patient chose ${day}.`, fallback, apiKey
);
return { reply, nextStage: "asked_reschedule_time" };
}
// Day not clearly detected — ask again with the specific options
const { mon, tue, wed } = getNextWeekDays();
const fallbacks: Record<string, string> = {
English: `Which day works best — ${mon}, ${tue}, or ${wed}?`,
Spanish: `¿Qué día le viene mejor — el ${mon}, ${tue} o el ${wed}?`,
Portuguese: `Qual dia é melhor — ${mon}, ${tue} ou ${wed}?`,
Mandarin: `哪天最方便——${mon}${tue} 还是 ${wed}`,
Cantonese: `哪天最方便——${mon}${tue} 還是 ${wed}`,
Arabic: `أي يوم هو الأفضل لك — ${mon} أو ${tue} أو ${wed}؟`,
"Haitian Creole": `Ki jou ki pi bon — ${mon}, ${tue}, oswa ${wed}?`,
};
const fallback = fallbacks[lang] ?? fallbacks["English"]!;
return { reply: fallback, nextStage: "asked_reschedule_next_week" };
}
// ── asked_reschedule_time: patient picked morning / afternoon / specific time ──
if (stage === "asked_reschedule_time") {
const pending = getPendingReschedule(userId, patientId);
if (!pending) {
// Edge case: lost state — fall back gracefully
return { reply: tx, nextStage: "done" };
}
const startTime = await parseTime(message, apiKey);
if (!startTime) {
// Couldn't parse time — ask again
const fallbacks: Record<string, string> = {
English: "I didn't catch the time. Would you prefer morning (9am12pm) or afternoon (1pm5pm)?",
Spanish: "No entendí la hora. ¿Prefiere la mañana (9am12pm) o la tarde (1pm5pm)?",
Portuguese: "Não entendi o horário. Você prefere manhã (9h12h) ou tarde (13h17h)?",
Mandarin: "我没听清时间。您想预约上午9点12点还是下午1点5点",
Cantonese: "我沒聽清時間。您想預約上午9點12點還是下午1點5點",
Arabic: "لم أفهم الوقت. هل تفضل الصباح (9ص12م) أم بعد الظهر (1م5م)؟",
"Haitian Creole": "Mwen pa konprann lè a. Èske ou prefere maten (9am12pm) oswa apremidi (1pm5pm)?",
};
return { reply: fallbacks[lang] ?? fallbacks["English"]!, nextStage: "asked_reschedule_time" };
}
// Update the appointment in the database
const updateResult = await moveAppointment(patientId, pending.newDate, startTime);
clearPendingReschedule(userId, patientId);
const [h, m] = startTime.split(":").map(Number);
const h12 = h! % 12 || 12;
const ampm = h! >= 12 ? "pm" : "am";
const timeLabel = `${h12}:${String(m!).padStart(2, "0")} ${ampm}`;
const apptLabel = `${pending.dayLabel} at ${timeLabel}`;
if (updateResult === "no_appointment") {
const fallbacks: Record<string, string> = {
English: `I couldn't find your appointment to update. Our staff will contact you to confirm ${apptLabel}.`,
Spanish: `No encontré su cita para actualizar. El personal le contactará para confirmar el ${apptLabel}.`,
Portuguese: `Não encontrei sua consulta para atualizar. Nossa equipe entrará em contato para confirmar ${apptLabel}.`,
Mandarin: `我找不到您的预约进行更新。我们的工作人员将联系您确认${apptLabel}`,
Cantonese: `我找不到您的預約進行更新。我們的工作人員將聯絡您確認${apptLabel}`,
Arabic: `لم أجد موعدك لتحديثه. سيتصل بك موظفونا لتأكيد ${apptLabel}.`,
"Haitian Creole": `Mwen pa jwenn randevou ou pou mete ajou. Anplwaye nou yo pral kontakte ou pou konfime ${apptLabel}.`,
};
return { reply: fallbacks[lang] ?? fallbacks["English"]!, nextStage: "done" };
}
// Success
const fallbacks: Record<string, string> = {
English: `Your appointment has been moved to ${apptLabel}. See you then!`,
Spanish: `Su cita ha sido cambiada al ${apptLabel}. ¡Hasta entonces!`,
Portuguese: `Sua consulta foi remarcada para ${apptLabel}. Até lá!`,
Mandarin: `您的预约已更改为${apptLabel}。到时见!`,
Cantonese: `您的預約已更改為${apptLabel}。到時見!`,
Arabic: `تم تغيير موعدك إلى ${apptLabel}. نراك قريباً!`,
"Haitian Creole": `Randevou ou a deplase ale nan ${apptLabel}. N'ap wè ou lè sa a!`,
};
const fallback = fallbacks[lang] ?? fallbacks["English"]!;
const reply = await llmReply(
`You are a friendly dental office assistant. The patient's appointment has been successfully rescheduled to ${apptLabel}. Confirm in ${lang} with enthusiasm. 1 sentence, no formatting.`,
`Appointment moved to ${apptLabel}.`, fallback, apiKey
);
return { reply, nextStage: "done" };
}
return { reply: tx, nextStage: "done" };
}