- Reminder flow: send AI self-introduction as message 1 (Twilio REST API), intent response as message 2 (TwiML) so intro always arrives first - LangGraph reminder graph: classify yes/no/other from patient reply; 'no' now asks 'When would you like to reschedule?' directly - Reschedule flow: new asked_reschedule_datetime stage replaces multi-step ASAP/next-week flow - Date-only reply (e.g. '5/18'): ask for time separately, then confirm - Date+time reply (e.g. '5/18 at 10am'): go straight to confirmation - new asked_reschedule_time_for_date and asked_reschedule_confirm_datetime stages - Date/time parsing: regex handles M/D and am/pm formats first; falls back to Gemini for natural language - Day-level office hours check: if requested day is closed (e.g. Sunday), reply 'Our office is closed on [date]. Choose another day?' - Time-level office hours check: if requested time is outside working hours (e.g. 12pm during lunch), reply with actual hours (e.g. '9:00 am – 12:00 pm and 1:00 pm – 5:00 pm') - Slot availability check: verifies no conflicting appointment for same staff member - After appointment confirmed: patient thank-you reply triggers warm closing with upcoming appointment time - Schedule page: office hours summary bar above grid showing today's configured hours with link to settings Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
103 lines
3.8 KiB
TypeScript
103 lines
3.8 KiB
TypeScript
import { prisma as db } from "@repo/db/client";
|
|
|
|
export type ConversationStage =
|
|
| "initial"
|
|
| "reminder_initial"
|
|
| "greeted"
|
|
| "done"
|
|
| "new_patient_greeted"
|
|
| "asked_new_or_existing"
|
|
| "asked_new_patient_insurance"
|
|
| "asked_existing_insurance"
|
|
| "asked_appointment_time"
|
|
| "awaiting_masshealth_info"
|
|
| "asked_appointment_preference"
|
|
| "asked_self_pay"
|
|
| "asked_reschedule_confirm"
|
|
| "asked_reschedule_preference"
|
|
| "asked_reschedule_asap"
|
|
| "asked_reschedule_next_week"
|
|
| "asked_reschedule_time"
|
|
| "asked_reschedule_datetime"
|
|
| "asked_reschedule_time_for_date"
|
|
| "asked_reschedule_confirm_datetime";
|
|
|
|
// ── Conversation stage + AI handoff per patient (DB-persisted) ────────────────
|
|
|
|
export async function getStage(userId: number, patientId: number): Promise<ConversationStage> {
|
|
const row = await db.patientConversation.findUnique({ where: { patientId } });
|
|
return (row?.stage as ConversationStage) ?? "initial";
|
|
}
|
|
|
|
export async function setStage(userId: number, patientId: number, stage: ConversationStage): Promise<void> {
|
|
await db.patientConversation.upsert({
|
|
where: { patientId },
|
|
update: { stage },
|
|
create: { patientId, userId, stage, aiHandoff: true },
|
|
});
|
|
}
|
|
|
|
export async function getHandoff(userId: number, patientId: number): Promise<boolean> {
|
|
const row = await db.patientConversation.findUnique({ where: { patientId } });
|
|
return row?.aiHandoff ?? true;
|
|
}
|
|
|
|
export async function setHandoff(userId: number, patientId: number, enabled: boolean): Promise<void> {
|
|
await db.patientConversation.upsert({
|
|
where: { patientId },
|
|
update: { aiHandoff: enabled },
|
|
create: { patientId, userId, aiHandoff: enabled, stage: "initial" },
|
|
});
|
|
}
|
|
|
|
// ── After-hours handoff per user (persisted in ai_settings) ──────────────────
|
|
|
|
export async function getAfterHoursHandoff(userId: number): Promise<boolean> {
|
|
const row = await db.aiSettings.findUnique({ where: { userId } });
|
|
return row?.afterHoursEnabled ?? true;
|
|
}
|
|
|
|
export async function setAfterHoursHandoff(userId: number, enabled: boolean): Promise<void> {
|
|
await db.aiSettings.update({ where: { userId }, data: { afterHoursEnabled: enabled } });
|
|
}
|
|
|
|
// ── Conversation starters ─────────────────────────────────────────────────────
|
|
|
|
export async function resetConversation(userId: number, patientId: number): Promise<void> {
|
|
await setStage(userId, patientId, "reminder_initial");
|
|
}
|
|
|
|
export async function startNewPatientConversation(userId: number, patientId: number): Promise<void> {
|
|
await setStage(userId, patientId, "new_patient_greeted");
|
|
}
|
|
|
|
export async function startRescheduleConversation(userId: number, patientId: number): Promise<void> {
|
|
await setStage(userId, patientId, "asked_reschedule_confirm");
|
|
}
|
|
|
|
// ── Pending reschedule (in-memory — seconds-lived within a single exchange) ───
|
|
|
|
interface PendingReschedule {
|
|
newDate: Date;
|
|
dayLabel: string;
|
|
startTime?: string; // "HH:MM" — set when full datetime was parsed together
|
|
}
|
|
|
|
const pendingRescheduleStore = new Map<string, PendingReschedule>();
|
|
|
|
function convKey(userId: number, patientId: number): string {
|
|
return `${userId}:${patientId}`;
|
|
}
|
|
|
|
export function setPendingReschedule(userId: number, patientId: number, data: PendingReschedule): void {
|
|
pendingRescheduleStore.set(convKey(userId, patientId), data);
|
|
}
|
|
|
|
export function getPendingReschedule(userId: number, patientId: number): PendingReschedule | undefined {
|
|
return pendingRescheduleStore.get(convKey(userId, patientId));
|
|
}
|
|
|
|
export function clearPendingReschedule(userId: number, patientId: number): void {
|
|
pendingRescheduleStore.delete(convKey(userId, patientId));
|
|
}
|