feat: enhance new-patient AI chat flow with full scheduling and eligibility
- Add 3-message intro (self-intro → empathetic ack → new/existing question) via single TwiML response to guarantee delivery order - Detect reschedule intent from first message; look up existing appointment date - New patient flow: ask insurance type → MassHealth consent → member ID + DOB → Selenium eligibility check - Post-eligibility: active → ask appointment date/time with office-hours validation; inactive → ask other insurance or collect contact info - Date/time collection mirrors reschedule flow: check office day open, ask time, validate against office hours - Auto-create appointment in schedule for known patients on confirmation; use first available staff member - Add openPhoneReply toggle (Settings → AI Chat) to respond to any number at any time - Add 5-minute inactivity timeout: reset conversation to initial stage and clear pending state - Normalize MassHealth DOB to zero-padded MM/DD/YYYY before Selenium submission - Expand isExistingPatient classifier to recognize "old patient", "old", "previous", "prior" - Existing patient confirmation message now acknowledges patient type before asking about insurance Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,11 +8,17 @@ export type ConversationStage =
|
||||
| "new_patient_greeted"
|
||||
| "asked_new_or_existing"
|
||||
| "asked_new_patient_insurance"
|
||||
| "asked_new_or_reschedule"
|
||||
| "asked_insurance_type"
|
||||
| "asked_masshealth_check_consent"
|
||||
| "asked_existing_insurance"
|
||||
| "asked_appointment_time"
|
||||
| "awaiting_masshealth_info"
|
||||
| "asked_appointment_preference"
|
||||
| "asked_self_pay"
|
||||
| "asked_other_insurance_after_inactive"
|
||||
| "collecting_contact_info"
|
||||
| "asked_new_appt_time_for_date"
|
||||
| "asked_reschedule_confirm"
|
||||
| "asked_reschedule_preference"
|
||||
| "asked_reschedule_asap"
|
||||
|
||||
@@ -43,19 +43,19 @@ function isNewPatient(text: string): boolean {
|
||||
}
|
||||
|
||||
function isExistingPatient(text: string): boolean {
|
||||
return /existing|been there|have been|already|before|i have been|returning|came before|i was there/i.test(text);
|
||||
return /existing|old patient|old\b|been there|have been|already|before|i have been|returning|came before|i was there|previous|prior|past patient/i.test(text);
|
||||
}
|
||||
|
||||
function hasMassHealth(text: string): boolean {
|
||||
return /masshealth|mass health|medicaid|masscare/i.test(text);
|
||||
}
|
||||
|
||||
function hasOtherInsurance(text: string): boolean {
|
||||
return /blue cross|delta dental|cigna|aetna|united|metlife|guardian|humana|tufts|harvard pilgrim|bmchp|yes|i have|my insurance|i do/i.test(text);
|
||||
function hasInsurance(text: string): boolean {
|
||||
return /yes|i have|my insurance|i do|have insurance|insured/i.test(text);
|
||||
}
|
||||
|
||||
function hasNoInsurance(text: string): boolean {
|
||||
return /no insurance|uninsured|self.pay|self pay|i don't|don't have|no i don't|i have no/i.test(text);
|
||||
return /no insurance|uninsured|self.pay|self pay|i don't|don't have|no i don't|i have no|no,? i|not insured/i.test(text);
|
||||
}
|
||||
|
||||
function sameInsurance(text: string): boolean {
|
||||
@@ -66,6 +66,14 @@ function changedInsurance(text: string): boolean {
|
||||
return /no|changed|different|new insurance|switched|lost|expired/i.test(text);
|
||||
}
|
||||
|
||||
function saysYes(text: string): boolean {
|
||||
return /\byes\b|yeah|yep|sure|ok\b|okay|of course|absolutely|please|si\b|sí|oui|wi\b|نعم|好的/i.test(text);
|
||||
}
|
||||
|
||||
function saysNo(text: string): boolean {
|
||||
return /\bno\b|nope|nah|not really|i don't|don't|no i|لا|非|no,/i.test(text);
|
||||
}
|
||||
|
||||
// ── LLM reply helper ──────────────────────────────────────────────────────────
|
||||
|
||||
async function llmReply(
|
||||
@@ -88,7 +96,6 @@ async function llmReply(
|
||||
|
||||
// ── Graph nodes ───────────────────────────────────────────────────────────────
|
||||
|
||||
// Classify intent based on current stage
|
||||
function classifyNode(state: GraphStateType) {
|
||||
const text = state.message.toLowerCase();
|
||||
const stage = state.stage as ConversationStage;
|
||||
@@ -97,22 +104,43 @@ function classifyNode(state: GraphStateType) {
|
||||
|
||||
if (stage === "new_patient_greeted") {
|
||||
intent = wantsAppointment(text) ? "wants_appointment" : "other";
|
||||
|
||||
} else if (stage === "asked_new_or_existing") {
|
||||
if (isNewPatient(text)) intent = "new_patient";
|
||||
if (isNewPatient(text)) intent = "new_patient";
|
||||
else if (isExistingPatient(text)) intent = "existing_patient";
|
||||
|
||||
} else if (stage === "asked_new_patient_insurance") {
|
||||
if (hasMassHealth(text)) intent = "masshealth";
|
||||
else if (hasNoInsurance(text)) intent = "no_insurance";
|
||||
else if (hasOtherInsurance(text)) intent = "other_insurance";
|
||||
else if (hasInsurance(text)) intent = "has_insurance";
|
||||
|
||||
} else if (stage === "asked_insurance_type") {
|
||||
if (hasMassHealth(text)) intent = "masshealth";
|
||||
else intent = "other_insurance";
|
||||
|
||||
} else if (stage === "asked_masshealth_check_consent") {
|
||||
if (saysYes(text)) intent = "yes";
|
||||
else intent = "no";
|
||||
|
||||
} else if (stage === "asked_existing_insurance") {
|
||||
if (sameInsurance(text)) intent = "same_insurance";
|
||||
else if (changedInsurance(text)) intent = "changed_insurance";
|
||||
|
||||
} else if (stage === "asked_appointment_time") {
|
||||
intent = "appointment_time";
|
||||
|
||||
} else if (stage === "asked_appointment_preference") {
|
||||
intent = "appointment_preference_reply";
|
||||
|
||||
} else if (stage === "asked_self_pay") {
|
||||
intent = "self_pay_reply";
|
||||
|
||||
} else if (stage === "asked_other_insurance_after_inactive") {
|
||||
if (saysYes(text) && !saysNo(text)) intent = "yes";
|
||||
else if (saysNo(text)) intent = "no";
|
||||
|
||||
} else if (stage === "collecting_contact_info") {
|
||||
intent = "contact_info_received";
|
||||
}
|
||||
|
||||
return { intent };
|
||||
@@ -122,14 +150,40 @@ function routeNode(state: GraphStateType): string {
|
||||
const stage = state.stage as ConversationStage;
|
||||
const intent = state.intent;
|
||||
|
||||
if (stage === "new_patient_greeted") return intent === "wants_appointment" ? "ask_new_or_existing" : "transfer";
|
||||
if (stage === "asked_new_or_existing") return intent === "new_patient" ? "ask_new_patient_insurance" : intent === "existing_patient" ? "ask_existing_insurance" : "transfer";
|
||||
if (stage === "asked_new_patient_insurance") return intent === "masshealth" ? "ask_masshealth_info" : intent === "no_insurance" ? "ask_appointment_time" : "transfer";
|
||||
if (stage === "asked_existing_insurance") return intent === "same_insurance" ? "ask_appointment_time" : "transfer";
|
||||
if (stage === "new_patient_greeted")
|
||||
return intent === "wants_appointment" ? "ask_new_or_existing" : "transfer";
|
||||
|
||||
if (stage === "asked_new_or_existing")
|
||||
return intent === "new_patient" ? "ask_new_patient_insurance"
|
||||
: intent === "existing_patient" ? "ask_existing_insurance"
|
||||
: "transfer";
|
||||
|
||||
if (stage === "asked_new_patient_insurance")
|
||||
return intent === "masshealth" ? "ask_masshealth_check_consent"
|
||||
: intent === "no_insurance" ? "ask_appointment_time"
|
||||
: intent === "has_insurance" ? "ask_insurance_type"
|
||||
: "transfer";
|
||||
|
||||
if (stage === "asked_insurance_type")
|
||||
return intent === "masshealth" ? "ask_masshealth_check_consent" : "ask_appointment_time";
|
||||
|
||||
if (stage === "asked_masshealth_check_consent")
|
||||
return intent === "yes" ? "ask_masshealth_info" : "ask_appointment_time";
|
||||
|
||||
if (stage === "asked_existing_insurance")
|
||||
return intent === "same_insurance" ? "ask_appointment_time" : "transfer";
|
||||
|
||||
if (stage === "asked_appointment_time") return "acknowledge_appointment_time";
|
||||
if (stage === "asked_appointment_preference") return "handle_appointment_preference";
|
||||
if (stage === "asked_self_pay") return "handle_self_pay";
|
||||
|
||||
if (stage === "asked_other_insurance_after_inactive")
|
||||
return intent === "yes" ? "transfer"
|
||||
: intent === "no" ? "ask_contact_info"
|
||||
: "transfer";
|
||||
|
||||
if (stage === "collecting_contact_info") return "acknowledge_contact_info";
|
||||
|
||||
return "transfer";
|
||||
}
|
||||
|
||||
@@ -187,30 +241,56 @@ async function askNewPatientInsuranceNode(state: GraphStateType, config: any) {
|
||||
return { reply, nextStage: "asked_new_patient_insurance" };
|
||||
}
|
||||
|
||||
async function askExistingInsuranceNode(state: GraphStateType, config: any) {
|
||||
async function askInsuranceTypeNode(state: GraphStateType, config: any) {
|
||||
const lang = state.language || "English";
|
||||
const apiKey: string | undefined = config?.configurable?.apiKey;
|
||||
|
||||
const fallbacks: Record<string, string> = {
|
||||
English: "Do you still have the same insurance?",
|
||||
Spanish: "¿Sigue teniendo el mismo seguro?",
|
||||
Portuguese: "Você ainda tem o mesmo plano?",
|
||||
Mandarin: "您还有相同的保险吗?",
|
||||
Cantonese: "您仍然有相同的保險嗎?",
|
||||
Arabic: "هل لا تزال تمتلك نفس التأمين؟",
|
||||
"Haitian Creole": "Èske ou toujou gen menm asirans lan?",
|
||||
English: "What kind of insurance do you have?",
|
||||
Spanish: "¿Qué tipo de seguro tiene?",
|
||||
Portuguese: "Que tipo de plano de saúde você tem?",
|
||||
Mandarin: "您有什么类型的保险?",
|
||||
Cantonese: "您有什麼類型的保險?",
|
||||
Arabic: "ما نوع التأمين الذي لديك؟",
|
||||
"Haitian Creole": "Ki kalite asirans ou genyen?",
|
||||
};
|
||||
const fallback = fallbacks[lang] ?? fallbacks["English"]!;
|
||||
|
||||
const reply = apiKey
|
||||
? await llmReply(
|
||||
`You are a friendly dental office AI assistant. Ask the existing patient in ${lang} if they still have the same dental insurance on file. One sentence, no formatting.`,
|
||||
`Existing patient confirmed. Ask about insurance.`,
|
||||
`You are a friendly dental office AI assistant. The patient confirmed they have insurance. Ask them in ${lang} what kind of insurance they have. One sentence, no formatting.`,
|
||||
`Patient has insurance. Ask what kind.`,
|
||||
fallback, apiKey
|
||||
)
|
||||
: fallback;
|
||||
|
||||
return { reply, nextStage: "asked_existing_insurance" };
|
||||
return { reply, nextStage: "asked_insurance_type" };
|
||||
}
|
||||
|
||||
async function askMassHealthCheckConsentNode(state: GraphStateType, config: any) {
|
||||
const lang = state.language || "English";
|
||||
const apiKey: string | undefined = config?.configurable?.apiKey;
|
||||
|
||||
const fallbacks: Record<string, string> = {
|
||||
English: "Do you want to check your MassHealth insurance now?",
|
||||
Spanish: "¿Desea verificar su seguro MassHealth ahora?",
|
||||
Portuguese: "Você gostaria de verificar sua cobertura MassHealth agora?",
|
||||
Mandarin: "您想现在查询您的MassHealth保险吗?",
|
||||
Cantonese: "您想現在查詢您的MassHealth保險嗎?",
|
||||
Arabic: "هل تريد التحقق من تأمين MassHealth الخاص بك الآن؟",
|
||||
"Haitian Creole": "Èske ou vle verifye asirans MassHealth ou kounye a?",
|
||||
};
|
||||
const fallback = fallbacks[lang] ?? fallbacks["English"]!;
|
||||
|
||||
const reply = apiKey
|
||||
? await llmReply(
|
||||
`You are a friendly dental office AI assistant. The patient has MassHealth insurance. Ask them in ${lang} if they would like to check their MassHealth coverage right now. One sentence, no formatting.`,
|
||||
`Patient has MassHealth. Ask if they want to check it now.`,
|
||||
fallback, apiKey
|
||||
)
|
||||
: fallback;
|
||||
|
||||
return { reply, nextStage: "asked_masshealth_check_consent" };
|
||||
}
|
||||
|
||||
async function askMassHealthInfoNode(state: GraphStateType, config: any) {
|
||||
@@ -218,20 +298,20 @@ async function askMassHealthInfoNode(state: GraphStateType, config: any) {
|
||||
const apiKey: string | undefined = config?.configurable?.apiKey;
|
||||
|
||||
const fallbacks: Record<string, string> = {
|
||||
English: "I can check your MassHealth coverage! Please text me your Member ID and date of birth.",
|
||||
Spanish: "¡Puedo verificar su cobertura de MassHealth! Por favor envíeme su número de miembro y fecha de nacimiento.",
|
||||
Portuguese: "Posso verificar sua cobertura MassHealth! Por favor envie seu número de membro e data de nascimento.",
|
||||
Mandarin: "我可以查看您的MassHealth保险!请发送您的会员ID和出生日期。",
|
||||
Cantonese: "我可以查核您的MassHealth保險!請傳送您的會員ID和出生日期。",
|
||||
Arabic: "يمكنني التحقق من تغطية MassHealth الخاصة بك! من فضلك أرسل لي رقم العضوية وتاريخ الميلاد.",
|
||||
"Haitian Creole": "Mwen ka verifye asirans MassHealth ou! Tanpri voye ID manm ou ak dat nesans ou.",
|
||||
English: "Please send me your MassHealth Member ID and date of birth so I can check your coverage.",
|
||||
Spanish: "Por favor envíeme su número de miembro de MassHealth y su fecha de nacimiento para verificar su cobertura.",
|
||||
Portuguese: "Por favor envie seu número de membro MassHealth e data de nascimento para verificar sua cobertura.",
|
||||
Mandarin: "请发送您的MassHealth会员ID和出生日期,以便我查询您的保险。",
|
||||
Cantonese: "請傳送您的MassHealth會員ID和出生日期,以便我查詢您的保險。",
|
||||
Arabic: "من فضلك أرسل لي رقم عضوية MassHealth وتاريخ ميلادك حتى أتحقق من تغطيتك.",
|
||||
"Haitian Creole": "Tanpri voye ID manm MassHealth ou ak dat nesans ou pou mwen ka verifye kouvèti ou.",
|
||||
};
|
||||
const fallback = fallbacks[lang] ?? fallbacks["English"]!;
|
||||
|
||||
const reply = apiKey
|
||||
? await llmReply(
|
||||
`You are a friendly dental office AI assistant. The patient has MassHealth. Tell them in ${lang} that you can check their coverage and ask them to send their Member ID and date of birth. 1-2 sentences, no formatting.`,
|
||||
`Patient has MassHealth. Ask for member ID and DOB.`,
|
||||
`You are a friendly dental office AI assistant. The patient wants to check their MassHealth coverage. Ask them in ${lang} to provide their MassHealth Member ID and date of birth. 1-2 sentences, no formatting.`,
|
||||
`Patient agreed to MassHealth check. Ask for member ID and DOB.`,
|
||||
fallback, apiKey
|
||||
)
|
||||
: fallback;
|
||||
@@ -239,16 +319,42 @@ async function askMassHealthInfoNode(state: GraphStateType, config: any) {
|
||||
return { reply, nextStage: "awaiting_masshealth_info" };
|
||||
}
|
||||
|
||||
async function askExistingInsuranceNode(state: GraphStateType, config: any) {
|
||||
const lang = state.language || "English";
|
||||
const apiKey: string | undefined = config?.configurable?.apiKey;
|
||||
|
||||
const fallbacks: Record<string, string> = {
|
||||
English: "Got it — you are an existing patient! Do you still have the same insurance on file?",
|
||||
Spanish: "¡Entendido — usted es un paciente existente! ¿Sigue teniendo el mismo seguro registrado?",
|
||||
Portuguese: "Entendido — você é um paciente existente! Você ainda tem o mesmo plano registrado?",
|
||||
Mandarin: "好的——您是现有患者!您还有相同的保险记录吗?",
|
||||
Cantonese: "好的——您是現有病人!您仍然有相同的保險記錄嗎?",
|
||||
Arabic: "حسناً — أنت مريض حالي! هل لا تزال تمتلك نفس التأمين المسجل لدينا؟",
|
||||
"Haitian Creole": "Konprann — ou se yon pasyan egzistan! Èske ou toujou gen menm asirans nou gen anrejistreman?",
|
||||
};
|
||||
const fallback = fallbacks[lang] ?? fallbacks["English"]!;
|
||||
|
||||
const reply = apiKey
|
||||
? await llmReply(
|
||||
`You are a friendly dental office AI assistant. The patient confirmed they are an existing patient. In ${lang}, acknowledge that they are an existing patient and then ask if they still have the same dental insurance on file. 1-2 sentences, no formatting.`,
|
||||
`Existing patient confirmed. Acknowledge and ask about insurance.`,
|
||||
fallback, apiKey
|
||||
)
|
||||
: fallback;
|
||||
|
||||
return { reply, nextStage: "asked_existing_insurance" };
|
||||
}
|
||||
|
||||
async function askAppointmentTimeNode(state: GraphStateType, config: any) {
|
||||
const lang = state.language || "English";
|
||||
const apiKey: string | undefined = config?.configurable?.apiKey;
|
||||
|
||||
const fallbacks: Record<string, string> = {
|
||||
English: "When would you like to make an appointment?",
|
||||
Spanish: "¿Cuándo le gustaría hacer una cita?",
|
||||
Portuguese: "Quando você gostaria de agendar uma consulta?",
|
||||
Mandarin: "您想什么时候预约?",
|
||||
Cantonese: "您想幾時預約?",
|
||||
English: "When would you like to come in for your appointment?",
|
||||
Spanish: "¿Cuándo le gustaría venir para su cita?",
|
||||
Portuguese: "Quando você gostaria de agendar sua consulta?",
|
||||
Mandarin: "您想什么时候来预约?",
|
||||
Cantonese: "您想幾時來預約?",
|
||||
Arabic: "متى تريد تحديد موعد؟",
|
||||
"Haitian Creole": "Ki lè ou ta renmen fè yon randevou?",
|
||||
};
|
||||
@@ -256,7 +362,7 @@ async function askAppointmentTimeNode(state: GraphStateType, config: any) {
|
||||
|
||||
const reply = apiKey
|
||||
? await llmReply(
|
||||
`You are a friendly dental office AI assistant. Ask the patient in ${lang} when they would like to schedule their appointment. One sentence, no formatting.`,
|
||||
`You are a friendly dental office AI assistant. Ask the patient in ${lang} what date and time they would prefer for their appointment. One sentence, no formatting.`,
|
||||
`Ask when to schedule.`,
|
||||
fallback, apiKey
|
||||
)
|
||||
@@ -291,8 +397,6 @@ async function acknowledgeAppointmentTimeNode(state: GraphStateType, config: any
|
||||
return { reply, nextStage: "done" };
|
||||
}
|
||||
|
||||
// ── Post-Selenium: appointment preference (MassHealth ACTIVE) ─────────────────
|
||||
|
||||
async function handleAppointmentPreferenceNode(state: GraphStateType, config: any) {
|
||||
const lang = state.language || "English";
|
||||
const apiKey: string | undefined = config?.configurable?.apiKey;
|
||||
@@ -319,14 +423,11 @@ async function handleAppointmentPreferenceNode(state: GraphStateType, config: an
|
||||
return { reply, nextStage: "asked_appointment_time" };
|
||||
}
|
||||
|
||||
// ── Post-Selenium: self-pay offer (MassHealth INACTIVE) ───────────────────────
|
||||
|
||||
async function handleSelfPayNode(state: GraphStateType, config: any) {
|
||||
const lang = state.language || "English";
|
||||
const apiKey: string | undefined = config?.configurable?.apiKey;
|
||||
const text = state.message.toLowerCase();
|
||||
|
||||
// Classify yes/no for self-pay
|
||||
const acceptsSelfPay = /yes|sure|ok|okay|yep|yeah|sí|si|claro|sim|confirmado|好的|نعم|wi|oke/i.test(text);
|
||||
const declinesSelfPay = /no|nope|can't|won't|not interested|no puedo|não|لا|pa ka/i.test(text);
|
||||
|
||||
@@ -372,10 +473,61 @@ async function handleSelfPayNode(state: GraphStateType, config: any) {
|
||||
return { reply, nextStage: "asked_appointment_time" };
|
||||
}
|
||||
|
||||
// Ambiguous — transfer to staff
|
||||
return { reply: transferMsg(lang), nextStage: "done" };
|
||||
}
|
||||
|
||||
async function askContactInfoNode(state: GraphStateType, config: any) {
|
||||
const lang = state.language || "English";
|
||||
const apiKey: string | undefined = config?.configurable?.apiKey;
|
||||
|
||||
const fallbacks: Record<string, string> = {
|
||||
English: "Please leave your name and phone number. Our receptionist will contact you as soon as possible.",
|
||||
Spanish: "Por favor déjenos su nombre y número de teléfono. Nuestra recepcionista se comunicará con usted lo antes posible.",
|
||||
Portuguese: "Por favor deixe seu nome e número de telefone. Nossa recepcionista entrará em contato o mais breve possível.",
|
||||
Mandarin: "请留下您的姓名和电话号码,我们的前台将尽快与您联系。",
|
||||
Cantonese: "請留下您的姓名和電話號碼,我們的接待員將盡快與您聯絡。",
|
||||
Arabic: "يرجى ترك اسمك ورقم هاتفك. ستتواصل معك موظفة الاستقبال في أقرب وقت ممكن.",
|
||||
"Haitian Creole": "Tanpri kite non ou ak nimewo telefòn ou. Resepsyonis nou an pral kontakte ou pi vit posib.",
|
||||
};
|
||||
const fallback = fallbacks[lang] ?? fallbacks["English"]!;
|
||||
|
||||
const reply = apiKey
|
||||
? await llmReply(
|
||||
`You are a friendly dental office AI assistant. The patient's MassHealth is inactive and they have no other insurance. In ${lang}, politely ask them to leave their name and phone number so the receptionist can contact them. 1-2 sentences, no formatting.`,
|
||||
`MassHealth inactive, no other insurance. Ask for name and phone.`,
|
||||
fallback, apiKey
|
||||
)
|
||||
: fallback;
|
||||
|
||||
return { reply, nextStage: "collecting_contact_info" };
|
||||
}
|
||||
|
||||
async function acknowledgeContactInfoNode(state: GraphStateType, config: any) {
|
||||
const lang = state.language || "English";
|
||||
const apiKey: string | undefined = config?.configurable?.apiKey;
|
||||
|
||||
const fallbacks: Record<string, string> = {
|
||||
English: "Thank you! Our receptionist will reach out to you shortly.",
|
||||
Spanish: "¡Gracias! Nuestra recepcionista se pondrá en contacto con usted en breve.",
|
||||
Portuguese: "Obrigado! Nossa recepcionista entrará em contato com você em breve.",
|
||||
Mandarin: "谢谢!我们的前台将很快与您联系。",
|
||||
Cantonese: "多謝!我們的接待員將很快與您聯絡。",
|
||||
Arabic: "شكراً! ستتواصل معك موظفة الاستقبال قريباً.",
|
||||
"Haitian Creole": "Mèsi! Resepsyonis nou an pral kontakte ou byento.",
|
||||
};
|
||||
const fallback = fallbacks[lang] ?? fallbacks["English"]!;
|
||||
|
||||
const reply = apiKey
|
||||
? await llmReply(
|
||||
`You are a friendly dental office AI assistant. The patient has left their contact information. Thank them in ${lang} and let them know the receptionist will reach out soon. 1-2 sentences, no formatting.`,
|
||||
`Patient provided contact info. Acknowledge.`,
|
||||
fallback, apiKey
|
||||
)
|
||||
: fallback;
|
||||
|
||||
return { reply, nextStage: "done" };
|
||||
}
|
||||
|
||||
function transferNode(state: GraphStateType) {
|
||||
const lang = state.language || "English";
|
||||
return { reply: state.generalFallback || transferMsg(lang), nextStage: "done" };
|
||||
@@ -387,33 +539,45 @@ const graph = new StateGraph(GraphState)
|
||||
.addNode("classify", classifyNode)
|
||||
.addNode("ask_new_or_existing", askNewOrExistingNode)
|
||||
.addNode("ask_new_patient_insurance", askNewPatientInsuranceNode)
|
||||
.addNode("ask_existing_insurance", askExistingInsuranceNode)
|
||||
.addNode("ask_insurance_type", askInsuranceTypeNode)
|
||||
.addNode("ask_masshealth_check_consent", askMassHealthCheckConsentNode)
|
||||
.addNode("ask_masshealth_info", askMassHealthInfoNode)
|
||||
.addNode("ask_existing_insurance", askExistingInsuranceNode)
|
||||
.addNode("ask_appointment_time", askAppointmentTimeNode)
|
||||
.addNode("acknowledge_appointment_time", acknowledgeAppointmentTimeNode)
|
||||
.addNode("handle_appointment_preference",handleAppointmentPreferenceNode)
|
||||
.addNode("handle_self_pay", handleSelfPayNode)
|
||||
.addNode("ask_contact_info", askContactInfoNode)
|
||||
.addNode("acknowledge_contact_info", acknowledgeContactInfoNode)
|
||||
.addNode("transfer", transferNode)
|
||||
.addEdge(START, "classify")
|
||||
.addConditionalEdges("classify", routeNode, {
|
||||
ask_new_or_existing: "ask_new_or_existing",
|
||||
ask_new_patient_insurance: "ask_new_patient_insurance",
|
||||
ask_existing_insurance: "ask_existing_insurance",
|
||||
ask_insurance_type: "ask_insurance_type",
|
||||
ask_masshealth_check_consent: "ask_masshealth_check_consent",
|
||||
ask_masshealth_info: "ask_masshealth_info",
|
||||
ask_existing_insurance: "ask_existing_insurance",
|
||||
ask_appointment_time: "ask_appointment_time",
|
||||
acknowledge_appointment_time: "acknowledge_appointment_time",
|
||||
handle_appointment_preference: "handle_appointment_preference",
|
||||
handle_self_pay: "handle_self_pay",
|
||||
ask_contact_info: "ask_contact_info",
|
||||
acknowledge_contact_info: "acknowledge_contact_info",
|
||||
transfer: "transfer",
|
||||
})
|
||||
.addEdge("ask_new_or_existing", END)
|
||||
.addEdge("ask_new_patient_insurance", END)
|
||||
.addEdge("ask_existing_insurance", END)
|
||||
.addEdge("ask_insurance_type", END)
|
||||
.addEdge("ask_masshealth_check_consent", END)
|
||||
.addEdge("ask_masshealth_info", END)
|
||||
.addEdge("ask_existing_insurance", END)
|
||||
.addEdge("ask_appointment_time", END)
|
||||
.addEdge("acknowledge_appointment_time", END)
|
||||
.addEdge("handle_appointment_preference", END)
|
||||
.addEdge("handle_self_pay", END)
|
||||
.addEdge("ask_contact_info", END)
|
||||
.addEdge("acknowledge_contact_info", END)
|
||||
.addEdge("transfer", END)
|
||||
.compile();
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ function getNextWeekDateObjects() {
|
||||
}
|
||||
|
||||
/** Format "HH:MM" (24-h) → "H:MM am/pm" */
|
||||
function timeLabel(hhmm: string): string {
|
||||
export function timeLabel(hhmm: string): string {
|
||||
const [h, m] = hhmm.split(":").map(Number);
|
||||
const h12 = (h! % 12) || 12;
|
||||
const ampm = (h! >= 12) ? "pm" : "am";
|
||||
@@ -80,7 +80,7 @@ function messageHasTime(msg: string): boolean {
|
||||
* Parse only a date (no time) from a message like "5/18" or "next Monday".
|
||||
* Returns a UTC-midnight Date and a date-only display label, or null.
|
||||
*/
|
||||
async function parseDateOnlyFromMessage(
|
||||
export async function parseDateOnlyFromMessage(
|
||||
message: string,
|
||||
apiKey: string,
|
||||
): Promise<{ date: Date; dateLabel: string } | null> {
|
||||
@@ -249,7 +249,7 @@ Rules:
|
||||
* Returns true if the given date+time is within the office's configured hours.
|
||||
* Falls back to true (unrestricted) if no hours are configured.
|
||||
*/
|
||||
async function isWithinOfficeHours(
|
||||
export async function isWithinOfficeHours(
|
||||
date: Date,
|
||||
time: string,
|
||||
userId: number,
|
||||
@@ -285,7 +285,7 @@ async function isWithinOfficeHours(
|
||||
* Returns whether the office is open at all on a given date (day-level check).
|
||||
* Also returns the display day name (e.g. "Sunday") for use in error messages.
|
||||
*/
|
||||
async function isOfficeDayOpen(
|
||||
export async function isOfficeDayOpen(
|
||||
date: Date,
|
||||
userId: number,
|
||||
): Promise<{ open: boolean; displayDay: string }> {
|
||||
@@ -319,7 +319,7 @@ async function isOfficeDayOpen(
|
||||
* e.g. "9:00 am – 12:00 pm and 1:00 pm – 5:00 pm".
|
||||
* Returns null if no hours are configured or the day is closed.
|
||||
*/
|
||||
async function getOfficeHoursDisplay(date: Date, userId: number): Promise<string | null> {
|
||||
export async function getOfficeHoursDisplay(date: Date, userId: number): Promise<string | null> {
|
||||
try {
|
||||
const record = await storage.getOfficeHours(userId);
|
||||
if (!record?.data) return null;
|
||||
@@ -387,7 +387,7 @@ async function isSlotAvailable(
|
||||
|
||||
// ── Time parsing (legacy) ─────────────────────────────────────────────────────
|
||||
|
||||
async function parseTime(message: string, apiKey: string): Promise<string | null> {
|
||||
export async function parseTime(message: string, apiKey: string): Promise<string | null> {
|
||||
const t = message.toLowerCase();
|
||||
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";
|
||||
|
||||
Reference in New Issue
Block a user