209 lines
7.5 KiB
JavaScript
209 lines
7.5 KiB
JavaScript
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, '../')));
|
|
|
|
// Primary Routes
|
|
app.get('/', (req, res) => {
|
|
res.sendFile(path.join(__dirname, '../login.html'));
|
|
});
|
|
|
|
app.get('/admin', (req, res) => {
|
|
res.sendFile(path.join(__dirname, '../admin.html'));
|
|
});
|
|
|
|
app.get('/dashboard', (req, res) => {
|
|
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('<Response></Response>');
|
|
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('<Response></Response>');
|
|
} 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}`);
|
|
});
|