Files
DentalManagement09/apps/Backend/src/storage/twilio-storage.ts
T
ffandClaude Sonnet 5 0cd095330a fix: AI recall-call flow, speech-to-text time parsing, and stale conversation state
- Reset conversation stage to a fresh start on every new voice call
  (recall and reminder alike), and on every hangup/timeout/goodbye,
  so an abandoned or completed call never leaves a patient stuck
  mid-flow for their next call or text.
- Add a staleness check to the SMS flow: after a long idle gap,
  check whether the patient's reply still reads as a continuation
  before resuming the stored stage, falling back to a friendly
  "it's been a while" re-greeting when unsure.
- Fix parseTime() to handle Twilio's speech-to-text output, which
  renders "pm"/"am" with periods and often drops the colon in a
  spoken time (e.g. "100 p.m." -> 1:00 PM) — this was silently
  failing and causing the AI to loop asking for a time it had
  already been given.
- Extend the voice Gather timeout so the AI doesn't hang up on a
  patient who just needs a few seconds to respond.
- Reword the recall-call opening line and rework the post-"yes"
  prompt to ask for day and time together.
- Fix rclone WebDAV server to serve only the backups folder instead
  of the entire backend app directory (was exposing .env and source).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 09:35:48 -04:00

211 lines
10 KiB
TypeScript

import { prisma as db } from "@repo/db/client";
// Exported so twilio-webhooks.ts can detect an admin-customized template
// (vs. this untouched default) and swap in a translated greeting instead.
export const DEFAULT_GREETING =
"Hi {firstName}, this is Lisa, the AI assistant at {officeName}. How can I help you today?";
export const DEFAULT_RECALL_GREETING =
"Hi {firstName}, this is Lisa from {officeName}. We wanted to check in — you're due for a routine exam and cleaning. Would you like to make an appointment because your insurance covers it?";
export type TwilioSettingsData = {
accountSid: string;
authToken: string;
phoneNumber: string;
greetingMessage?: string | null;
twimlAppSid?: string | null;
};
export type CommunicationCreateData = {
patientId: number;
userId?: number;
channel: "sms" | "voice";
direction: "outbound" | "inbound";
status: "queued" | "sent" | "delivered" | "failed" | "completed" | "busy" | "no_answer";
body?: string;
callDuration?: number;
twilioSid?: string;
};
export const twilioStorage = {
async getTwilioSettings(userId: number) {
return db.twilioSettings.findUnique({ where: { userId } });
},
async upsertTwilioSettings(userId: number, data: TwilioSettingsData) {
const { twimlAppSid, ...rest } = data;
const existing = await db.twilioSettings.findUnique({ where: { userId } });
const existingTemplates = (existing?.templates as Record<string, string>) || {};
const templates: Record<string, string> = { ...existingTemplates };
if (twimlAppSid !== undefined) {
if (twimlAppSid) templates["_twiml_app_sid"] = twimlAppSid;
else delete templates["_twiml_app_sid"];
}
return db.twilioSettings.upsert({
where: { userId },
update: { ...rest, templates },
create: { userId, ...rest, templates },
});
},
async createCommunication(data: CommunicationCreateData) {
return db.communication.create({ data: data as any });
},
async getCommunicationsByPatient(patientId: number) {
return db.communication.findMany({
where: { patientId },
orderBy: { createdAt: "asc" },
});
},
async getTemplates(userId: number): Promise<Record<string, string>> {
const settings = await db.twilioSettings.findUnique({ where: { userId } });
if (!settings?.templates) return {};
return settings.templates as Record<string, string>;
},
async saveTemplate(userId: number, key: string, body: string) {
const settings = await db.twilioSettings.findUnique({ where: { userId } });
const existing = (settings?.templates as Record<string, string>) || {};
const updated = { ...existing, [key]: body };
return db.twilioSettings.upsert({
where: { userId },
update: { templates: updated },
create: { userId, accountSid: "", authToken: "", phoneNumber: "", templates: updated },
});
},
async getAiChatTemplates(userId: number): Promise<Record<string, string>> {
const settings = await db.twilioSettings.findUnique({ where: { userId } });
const all = (settings?.templates as Record<string, string>) || {};
return {
reminderGreeting: all["_ai_chat_reminder_greeting"] ?? "",
newPatientGreeting: all["_ai_chat_new_patient_greeting"] ?? "",
generalFallback: all["_ai_chat_general_fallback"] ?? "",
rescheduleGreeting: all["_ai_chat_reschedule_greeting"] ?? "",
reminderSms: all["_ai_chat_reminder_sms"] ?? "",
};
},
async saveAiChatTemplates(userId: number, templates: { reminderGreeting?: string; newPatientGreeting?: string; generalFallback?: string; rescheduleGreeting?: string; reminderSms?: string }) {
const settings = await db.twilioSettings.findUnique({ where: { userId } });
const existing = (settings?.templates as Record<string, string>) || {};
const updated: Record<string, string> = { ...existing };
if (templates.reminderGreeting !== undefined) updated["_ai_chat_reminder_greeting"] = templates.reminderGreeting;
if (templates.newPatientGreeting !== undefined) updated["_ai_chat_new_patient_greeting"] = templates.newPatientGreeting;
if (templates.generalFallback !== undefined) updated["_ai_chat_general_fallback"] = templates.generalFallback;
if (templates.rescheduleGreeting !== undefined) updated["_ai_chat_reschedule_greeting"] = templates.rescheduleGreeting;
if (templates.reminderSms !== undefined) updated["_ai_chat_reminder_sms"] = templates.reminderSms;
return db.twilioSettings.upsert({
where: { userId },
update: { templates: updated },
create: { userId, accountSid: "", authToken: "", phoneNumber: "", templates: updated },
});
},
// ── Call Templates — wording spoken during live AI phone calls, kept
// separate from the SMS wording above (routePatientTurn() consumes
// whichever set the caller passes in). ────────────────────────────────────
async getAiCallTemplates(userId: number): Promise<Record<string, string>> {
const settings = await db.twilioSettings.findUnique({ where: { userId } });
const all = (settings?.templates as Record<string, string>) || {};
return {
greeting: all["_ai_call_greeting"] ?? all["_ai_chat_call_template"] ?? DEFAULT_GREETING,
reminderGreeting: all["_ai_call_reminder_greeting"] ?? "Sure, I can help with that.",
newPatientGreeting: all["_ai_call_new_patient_greeting"] ?? "No problem, let's get you set up.",
generalFallback: all["_ai_call_general_fallback"] ?? "Sure, how can I help?",
recallGreeting: all["_ai_call_recall_greeting"] ?? DEFAULT_RECALL_GREETING,
};
},
async saveAiCallTemplates(userId: number, templates: { greeting?: string; reminderGreeting?: string; newPatientGreeting?: string; generalFallback?: string; recallGreeting?: string }) {
const settings = await db.twilioSettings.findUnique({ where: { userId } });
const existing = (settings?.templates as Record<string, string>) || {};
const updated: Record<string, string> = { ...existing };
if (templates.greeting !== undefined) updated["_ai_call_greeting"] = templates.greeting;
if (templates.reminderGreeting !== undefined) updated["_ai_call_reminder_greeting"] = templates.reminderGreeting;
if (templates.newPatientGreeting !== undefined) updated["_ai_call_new_patient_greeting"] = templates.newPatientGreeting;
if (templates.generalFallback !== undefined) updated["_ai_call_general_fallback"] = templates.generalFallback;
if (templates.recallGreeting !== undefined) updated["_ai_call_recall_greeting"] = templates.recallGreeting;
return db.twilioSettings.upsert({
where: { userId },
update: { templates: updated },
create: { userId, accountSid: "", authToken: "", phoneNumber: "", templates: updated },
});
},
async getSmsTemplateList(userId: number): Promise<{ id: string; name: string; body: string }[]> {
const settings = await db.twilioSettings.findUnique({ where: { userId } });
const all = (settings?.templates as Record<string, string>) || {};
const raw = all["_sms_template_list"];
if (!raw) return [];
try { return JSON.parse(raw); } catch { return []; }
},
async saveSmsTemplateList(userId: number, templates: { id: string; name: string; body: string }[]) {
const settings = await db.twilioSettings.findUnique({ where: { userId } });
const existing = (settings?.templates as Record<string, string>) || {};
const updated: Record<string, string> = {
...existing,
"_sms_template_list": JSON.stringify(templates),
};
// Keep _ai_chat_reminder_sms in sync with the first template for batch sends
if (templates.length > 0) updated["_ai_chat_reminder_sms"] = templates[0]!.body;
return db.twilioSettings.upsert({
where: { userId },
update: { templates: updated },
create: { userId, accountSid: "", authToken: "", phoneNumber: "", templates: updated },
});
},
async getInternalChatSystemPrompt(userId: number): Promise<string> {
const settings = await db.twilioSettings.findUnique({ where: { userId } });
const all = (settings?.templates as Record<string, string>) || {};
return all["_internal_chat_system_prompt"] ?? "";
},
async saveInternalChatSystemPrompt(userId: number, prompt: string): Promise<void> {
const settings = await db.twilioSettings.findUnique({ where: { userId } });
const existing = (settings?.templates as Record<string, string>) || {};
const updated = { ...existing, "_internal_chat_system_prompt": prompt };
await db.twilioSettings.upsert({
where: { userId },
update: { templates: updated },
create: { userId, accountSid: "", authToken: "", phoneNumber: "", templates: updated },
});
},
async getCdtAliases(userId: number): Promise<{ phrase: string; cdtCode: string }[]> {
const settings = await db.twilioSettings.findUnique({ where: { userId } });
const all = (settings?.templates as Record<string, any>) || {};
const raw = all["_cdt_aliases"];
if (!Array.isArray(raw)) return [];
return raw.filter(
(r: any) => typeof r?.phrase === "string" && typeof r?.cdtCode === "string"
);
},
async saveCdtAliases(userId: number, aliases: { phrase: string; cdtCode: string }[]): Promise<void> {
const settings = await db.twilioSettings.findUnique({ where: { userId } });
const existing = (settings?.templates as Record<string, any>) || {};
const updated = { ...existing, "_cdt_aliases": aliases };
await db.twilioSettings.upsert({
where: { userId },
update: { templates: updated },
create: { userId, accountSid: "", authToken: "", phoneNumber: "", templates: updated },
});
},
async getRecentCommunicationsByUser(userId: number, limit = 20) {
return db.communication.findMany({
where: { patient: { userId } },
orderBy: { createdAt: "desc" },
take: limit,
include: {
patient: { select: { id: true, firstName: true, lastName: true, phone: true } },
},
});
},
};