Applied the <UNTRUSTED_*> delimiter + INJECTION_GUARD pattern to:
- src/routes/sickVisit.js (chief complaint, transcript, dictation,
ROS, physical exam, diagnoses, style hints)
- src/routes/wellVisit.js (SSHADESS answers + full well-visit context)
- src/routes/chartReview.js (PMH + all visit content + labs)
- src/routes/hospitalCourse.js (all notes/H&P/ED + clarification
& update endpoints)
- src/routes/milestones.js (narrative + summary)
Each wraps patient-derived text in <UNTRUSTED_*>…</UNTRUSTED_*>
tags and appends the INJECTION_GUARD system instruction that tells
the model to treat wrapped content strictly as data. Operator-
supplied `additionalInstructions` stays unwrapped (trusted).
139 lines
5.6 KiB
JavaScript
139 lines
5.6 KiB
JavaScript
const express = require('express');
|
|
const router = express.Router();
|
|
const { callAI } = require('../utils/ai');
|
|
const PROMPTS = require('../utils/prompts');
|
|
const { authMiddleware } = require('../middleware/auth');
|
|
var logger = require('../utils/logger');
|
|
var { wrapUserText, INJECTION_GUARD } = require('../utils/promptSafe');
|
|
|
|
// Generate hospital course
|
|
router.post('/generate-hospital-course', authMiddleware, async (req, res) => {
|
|
try {
|
|
const {
|
|
notes, // Array of { date, type, content }
|
|
edNote, // { date, content, labs }
|
|
hAndP, // { date, content }
|
|
labs, // Array of { date, values }
|
|
patientAge, patientGender, pmh,
|
|
setting, // 'floor' | 'picu' | 'nicu' | 'psych'
|
|
los, // length of stay
|
|
model,
|
|
formatPreference, // 'auto' | 'prose' | 'dayByDay' | 'organSystem'
|
|
additionalInstructions,
|
|
physicianMemories
|
|
} = req.body;
|
|
|
|
if (!notes || notes.length === 0) {
|
|
return res.status(400).json({ error: 'No notes provided' });
|
|
}
|
|
|
|
// Determine format
|
|
let prompt;
|
|
let format = formatPreference || 'auto';
|
|
|
|
if (format === 'auto') {
|
|
if (setting === 'picu' || setting === 'nicu') {
|
|
format = 'organSystem';
|
|
} else if (setting === 'psych') {
|
|
format = 'psych';
|
|
} else if (los && parseInt(los) > 3) {
|
|
format = 'dayByDay';
|
|
} else {
|
|
format = 'prose';
|
|
}
|
|
}
|
|
|
|
switch (format) {
|
|
case 'organSystem': prompt = PROMPTS.hospitalCourseICU; break;
|
|
case 'dayByDay': prompt = PROMPTS.hospitalCourseLong; break;
|
|
case 'psych': prompt = PROMPTS.hospitalCoursePsych; break;
|
|
default: prompt = PROMPTS.hospitalCourseShort;
|
|
}
|
|
|
|
// Build the complete clinical data
|
|
let clinicalData = `Patient: ${patientAge || 'Unknown'}, ${patientGender || 'Unknown'}`;
|
|
if (pmh) clinicalData += `\nPMH: ` + wrapUserText('pmh', pmh);
|
|
if (setting) clinicalData += `\nSetting: ${setting}`;
|
|
if (los) clinicalData += `\nLength of Stay: ${los} days`;
|
|
clinicalData += `\nFormat: ${format}`;
|
|
|
|
if (edNote) {
|
|
clinicalData += `\n\n=== ED NOTE (${edNote.date || 'date unknown'}) ===\n` + wrapUserText('ed_note', edNote.content || '');
|
|
if (edNote.labs) clinicalData += `\nED Labs: ` + wrapUserText('labs', edNote.labs);
|
|
}
|
|
if (hAndP) clinicalData += `\n\n=== H&P (${hAndP.date || 'date unknown'}) ===\n` + wrapUserText('h_and_p', hAndP.content || '');
|
|
|
|
const sortedNotes = [...notes].sort((a, b) => new Date(a.date) - new Date(b.date));
|
|
sortedNotes.forEach((note, i) => {
|
|
clinicalData += `\n\n=== ${(note.type || 'Progress Note').toUpperCase()} - ${note.date || `Note ${i + 1}`} ===\n` + wrapUserText('note', note.content || '');
|
|
});
|
|
|
|
if (labs && labs.length > 0) {
|
|
clinicalData += '\n\n=== LABS ===';
|
|
labs.forEach(lab => { clinicalData += `\n${lab.date}: ` + wrapUserText('labs', lab.values || ''); });
|
|
}
|
|
|
|
if (additionalInstructions) {
|
|
prompt += `\n\nADDITIONAL INSTRUCTIONS FROM PHYSICIAN (operator-supplied, trusted):\n${additionalInstructions}`;
|
|
}
|
|
if (physicianMemories) clinicalData += '\n\n[STYLE HINTS (low priority — only apply if relevant to the current note, never copy content from these examples)]\n'
|
|
+ wrapUserText('style_hints', physicianMemories) + '\n[END STYLE HINTS]';
|
|
|
|
const result = await callAI([
|
|
{ role: 'system', content: prompt + INJECTION_GUARD },
|
|
{ role: 'user', content: clinicalData }
|
|
], { model, maxTokens: 6000 });
|
|
|
|
res.json({
|
|
success: true,
|
|
hospitalCourse: result.content,
|
|
format: format,
|
|
model: result.model
|
|
});
|
|
logger.audit(req.user.id, 'generate_hospital_course', 'Generated hospital course', req, { category: 'clinical' });
|
|
} catch (err) {
|
|
res.status(500).json({ error: 'Request failed' });
|
|
}
|
|
});
|
|
|
|
// Ask for clarification/missing info
|
|
router.post('/hospital-course-clarify', authMiddleware, async (req, res) => {
|
|
try {
|
|
const { currentDraft, notes } = req.body;
|
|
|
|
const result = await callAI([
|
|
{ role: 'system', content: PROMPTS.askClarification + INJECTION_GUARD },
|
|
{ role: 'user', content: `Current draft:\n` + wrapUserText('draft', currentDraft || '') + `\n\nSource notes:\n` + wrapUserText('notes', JSON.stringify(notes || [])) }
|
|
], { model: req.body.model });
|
|
|
|
res.json({ success: true, questions: result.content, model: result.model });
|
|
} catch (err) {
|
|
res.status(500).json({ error: 'Request failed' });
|
|
}
|
|
});
|
|
|
|
// Add discharge day info
|
|
router.post('/hospital-course-update', authMiddleware, async (req, res) => {
|
|
try {
|
|
const { currentCourse, updates, model, instructions } = req.body;
|
|
if (!currentCourse) return res.status(400).json({ error: 'No current course' });
|
|
|
|
let userMsg = `CURRENT HOSPITAL COURSE:\n` + wrapUserText('course', currentCourse) + `\n\n`;
|
|
if (updates) userMsg += `NEW UPDATES TO ADD:\n` + wrapUserText('updates', updates) + `\n\n`;
|
|
if (instructions) userMsg += `INSTRUCTIONS:\n` + wrapUserText('instructions', instructions);
|
|
|
|
const result = await callAI([
|
|
{ role: 'system', content: `You are updating a hospital course document with new information.
|
|
Add the new information in the appropriate place. Maintain the same format and style.
|
|
If adding discharge day information, add it at the end.
|
|
${PROMPTS.refine.split('\n').slice(0, -1).join('\n')}` + INJECTION_GUARD },
|
|
{ role: 'user', content: userMsg }
|
|
], { model, maxTokens: 6000 });
|
|
|
|
res.json({ success: true, hospitalCourse: result.content, model: result.model });
|
|
} catch (err) {
|
|
res.status(500).json({ error: 'Request failed' });
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|