feat: add AI-driven phone calls to patient connection
Adds an "AI Call" option alongside Call/SMS/Chat that places an outbound call where Lisa converses live with the patient via Twilio speech gather + TTS, using the same conversational AI as chat. Includes a separate, user-editable Call Template (Settings > AI Chat/Call Settings) so the call greeting can be customized independently of SMS templates. Also fixes the outbound-call webhook URL to always use the fixed public Twilio hostname (CLOUDFLARE_HOST) instead of deriving it from the triggering request, since staff-browser requests arrive over the LAN-only hostname, which Twilio can never reach. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,8 @@ import twilio from "twilio";
|
||||
import { storage } from "../storage";
|
||||
import { prisma as db } from "@repo/db/client";
|
||||
import { getHandoff, setHandoff, resetConversation, startNewPatientConversation, startRescheduleConversation, getAfterHoursHandoff, setAfterHoursHandoff } from "../ai/aiHandoffStore";
|
||||
import { resolveAiProvider } from "../ai/llm-factory";
|
||||
import { getTwilioPublicBaseUrl } from "../utils/publicUrl";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -117,6 +119,52 @@ router.post("/send-sms", async (req: Request, res: Response): Promise<any> => {
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/twilio/make-ai-call
|
||||
// Places an outbound call where Lisa (the AI assistant) conducts the whole
|
||||
// conversation live over the phone — see /webhook/ai-voice for the loop.
|
||||
router.post("/make-ai-call", async (req: Request, res: Response): Promise<any> => {
|
||||
try {
|
||||
const userId = req.user?.id;
|
||||
if (!userId) return res.status(401).json({ message: "Unauthorized" });
|
||||
|
||||
const { to, patientId } = req.body;
|
||||
if (!to || !patientId) return res.status(400).json({ message: "to and patientId are required" });
|
||||
|
||||
const settings = await storage.getTwilioSettings(userId);
|
||||
if (!settings) {
|
||||
return res.status(400).json({ message: "Twilio is not configured. Please add your Twilio credentials in Settings." });
|
||||
}
|
||||
|
||||
const aiSettings = await storage.getAiSettings(userId);
|
||||
if (!resolveAiProvider(aiSettings ?? {})) {
|
||||
return res.status(400).json({ message: "AI is not configured. Please add an AI provider key in Settings before placing an AI call." });
|
||||
}
|
||||
|
||||
const patient = await db.patient.findFirst({ where: { id: Number(patientId), userId } });
|
||||
if (!patient) return res.status(404).json({ message: "Patient not found" });
|
||||
|
||||
let publicBaseUrl: string;
|
||||
try {
|
||||
publicBaseUrl = getTwilioPublicBaseUrl();
|
||||
} catch (err: any) {
|
||||
return res.status(500).json({ message: err.message });
|
||||
}
|
||||
const webhookUrl = `${publicBaseUrl}/api/twilio/webhook/ai-voice?patientId=${patient.id}`;
|
||||
|
||||
const client = getTwilioClient(settings.accountSid, settings.authToken);
|
||||
const call = await client.calls.create({
|
||||
url: webhookUrl,
|
||||
method: "POST",
|
||||
from: settings.phoneNumber,
|
||||
to,
|
||||
});
|
||||
|
||||
return res.status(200).json({ sid: call.sid, status: call.status });
|
||||
} catch (err: any) {
|
||||
return res.status(500).json({ error: err.message || "Failed to place AI call" });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/twilio/send-reminders-batch
|
||||
router.post("/send-reminders-batch", async (req: Request, res: Response): Promise<any> => {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user