const express = require('express'); const path = require('path'); const botLogic = require('./botLogic'); const configManager = require('./configManager'); const { getTemporalClient } = require('./temporalClient'); const crypto = require('crypto'); const app = express(); const port = 3000; app.use(express.json()); // Serve static files app.use(express.static(path.join(__dirname, '../'), { setHeaders: (res, path) => { if (path.endsWith('.html')) { res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate'); res.setHeader('Pragma', 'no-cache'); res.setHeader('Expires', '0'); } } })); // Primary Routes app.get('/', (req, res) => { res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate'); res.setHeader('Pragma', 'no-cache'); res.setHeader('Expires', '0'); res.sendFile(path.join(__dirname, '../login.html')); }); app.get('/admin', (req, res) => { res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate'); res.setHeader('Pragma', 'no-cache'); res.setHeader('Expires', '0'); res.sendFile(path.join(__dirname, '../admin.html')); }); app.get('/dashboard', (req, res) => { res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate'); res.setHeader('Pragma', 'no-cache'); res.setHeader('Expires', '0'); res.sendFile(path.join(__dirname, '../dashboard.html')); }); // Multi-tenant Config API app.get('/api/config/:tenantId', (req, res) => { const config = configManager.getTenantConfig(req.params.tenantId); res.json(config); }); // Official Twilio Webhook parsing middleware app.use(express.urlencoded({ extended: true })); // WhatsApp Webhook (Twilio) app.post('/whatsapp/webhook', async (req, res) => { // Twilio sends urlencoded fields: From, Body // Or our mock sends json: from, body const from = req.body.From || req.body.from; const body = req.body.Body || req.body.body; console.log(`[WhatsApp Webhook] Received message from ${from}: ${body}`); if (!from || !body) { return res.status(400).send("Missing from or body"); } try { const temporalClient = await getTemporalClient(); // Use the phone number as the unique Workflow ID const workflowId = `booking-${from.replace(/[^a-zA-Z0-9]/g, '')}`; let handle; try { handle = temporalClient.workflow.getHandle(workflowId); const desc = await handle.describe(); if (desc.status.name === 'RUNNING') { // Ongoing conversation! Send a signal with the new message console.log(`[WhatsApp Webhook] Signaling existing workflow ${workflowId}`); await handle.signal('receive_message', body); // Twilio expects a valid TwiML response. Empty response means no immediate automated reply. // Our Temporal worker will asynchronously send a reply back via Twilio API. res.type('text/xml').send(''); return; } } catch (err) { // Workflow does not exist. We will start a new one below. } console.log(`[WhatsApp Webhook] Starting new AppointmentBookingWorkflow for ${from}`); await temporalClient.workflow.start('AppointmentBookingWorkflow', { args: [body, from], taskQueue: 'curaflow-tasks', workflowId: workflowId, }); res.type('text/xml').send(''); } catch (err) { console.error('[WhatsApp Webhook] Error processing message:', err); res.status(500).send('Internal Server Error'); } }); // AI Scribe Endpoints app.post('/api/ai/scribe/start', async (req, res) => { try { const { dictation } = req.body; if (!dictation) return res.status(400).json({ error: 'Dictation is required' }); const temporalClient = await getTemporalClient(); const jobId = `scribe-${crypto.randomUUID()}`; console.log(`[AI Scribe] Starting job ${jobId} for dictation of length ${dictation.length}`); // Start the workflow asynchronously await temporalClient.workflow.start('ClinicalIntakeWorkflow', { args: [dictation], taskQueue: 'curaflow-tasks', workflowId: jobId, }); res.json({ success: true, jobId }); } catch (err) { console.error('[AI Scribe] Start Error:', err); res.status(500).json({ error: 'Failed to start AI Scribe job' }); } }); app.get('/api/ai/scribe/status/:jobId', async (req, res) => { try { const { jobId } = req.params; const temporalClient = await getTemporalClient(); const handle = temporalClient.workflow.getHandle(jobId); const description = await handle.describe(); // Check if completed if (description.status.name === 'COMPLETED') { const result = await handle.result(); return res.json({ status: 'COMPLETED', result }); } else if (description.status.name === 'FAILED') { return res.json({ status: 'FAILED' }); } return res.json({ status: description.status.name }); } catch (err) { // If not found or error console.error(`[AI Scribe] Status Error for ${req.params.jobId}:`, err); res.status(500).json({ error: 'Failed to get job status' }); } }); // Lab Watcher Endpoints app.post('/api/ai/lab/watch/start', async (req, res) => { try { const { patientId, baselineData } = req.body; if (!patientId) return res.status(400).json({ error: 'Patient ID is required' }); const temporalClient = await getTemporalClient(); const workflowId = `lab-watcher-${patientId}`; // Ensure we don't start it twice if it's already running try { const handle = temporalClient.workflow.getHandle(workflowId); const desc = await handle.describe(); if (desc.status.name === 'RUNNING') { return res.json({ success: true, message: 'Watcher already running' }); } } catch (e) { // Not found, so we proceed to start it } await temporalClient.workflow.start('LabResultWatcherWorkflow', { args: [baselineData || { baseline: "Patient is healthy", medications: [] }], taskQueue: 'curaflow-tasks', workflowId: workflowId, }); res.json({ success: true }); } catch (err) { console.error('[AI Lab Watcher] Start Error:', err); res.status(500).json({ error: 'Failed to start AI Lab Watcher' }); } }); app.post('/api/ai/lab/result/:patientId', async (req, res) => { try { const { patientId } = req.params; const { result } = req.body; if (!result) return res.status(400).json({ error: 'Lab result string is required' }); const temporalClient = await getTemporalClient(); const workflowId = `lab-watcher-${patientId}`; const handle = temporalClient.workflow.getHandle(workflowId); await handle.signal('add_lab_result', result); res.json({ success: true }); } catch (err) { console.error(`[AI Lab Watcher] Signal Error for ${req.params.patientId}:`, err); res.status(500).json({ error: 'Failed to send lab result to AI Watcher' }); } }); app.get('/api/ai/lab/alerts/:patientId', async (req, res) => { try { const { patientId } = req.params; const temporalClient = await getTemporalClient(); const workflowId = `lab-watcher-${patientId}`; try { const handle = temporalClient.workflow.getHandle(workflowId); const alerts = await handle.query('get_alerts'); res.json({ success: true, alerts }); } catch (e) { // Workflow might not be running res.json({ success: true, alerts: [] }); } } catch (err) { console.error(`[AI Lab Watcher] Query Error for ${req.params.patientId}:`, err); res.status(500).json({ error: 'Failed to fetch alerts' }); } }); app.listen(port, '0.0.0.0', () => { console.log(`Curio WhatsApp Mock Server running at http://0.0.0.0:${port}`); });