refactor(ts): day 3 batch 3 — clinical note generators
sickVisit.ts — /api/sick-visit/note
wellVisit.ts — /api/well-visit/{shadess,note}
peGuide.ts — /api/generate-pe-narrative (with typed PeStep)
milestones.ts — /api/{milestones-data, generate-milestone-narrative, generate-milestone-summary}
chartReview.ts — /api/generate-chart-review (typed VisitEntry etc.)
All five follow the established pattern. Added a handful of inline
interfaces (PeStep, MilestoneItem, VisitEntry) where the existing code
was juggling anonymous object shapes — these will migrate into
shared/types.ts during Day 5 if reused elsewhere.
14 of 29 routes converted. Progress: 14/54 backend files.
tsc --noEmit green.
This commit is contained in:
parent
004e36cd67
commit
87654b6005
7 changed files with 366 additions and 320 deletions
|
|
@ -1,58 +1,67 @@
|
|||
const express = require('express');
|
||||
const router = express.Router();
|
||||
import express = require('express');
|
||||
import type { Request, Response } from 'express';
|
||||
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');
|
||||
const logger = require('../utils/logger');
|
||||
const { wrapUserText, INJECTION_GUARD } = require('../utils/promptSafe');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
interface VisitEntry { date?: string; type?: string; content?: string; labs?: string }
|
||||
interface SubspecialtyEntry { date?: string; specialistName?: string; specialty?: string; content?: string; labs?: string }
|
||||
interface LabEntry { date?: string; values?: string }
|
||||
|
||||
// Generate chart review
|
||||
router.post('/generate-chart-review', authMiddleware, async (req, res) => {
|
||||
router.post('/generate-chart-review', authMiddleware, async (req: Request, res: Response) => {
|
||||
try {
|
||||
const {
|
||||
type, // 'outpatient' | 'subspecialty' | 'ed'
|
||||
type,
|
||||
patientAge, patientGender, pmh,
|
||||
visits, // Array of { date, type, content }
|
||||
subspecialty, // Array of { date, specialistName, specialty, content }
|
||||
edVisits, // Array of { date, content, labs }
|
||||
labs, // Array of { date, values }
|
||||
visits,
|
||||
subspecialty,
|
||||
edVisits,
|
||||
labs,
|
||||
model,
|
||||
additionalInstructions
|
||||
} = req.body;
|
||||
additionalInstructions,
|
||||
} = req.body as {
|
||||
type?: 'outpatient' | 'subspecialty' | 'ed';
|
||||
patientAge?: string;
|
||||
patientGender?: string;
|
||||
pmh?: string;
|
||||
visits?: VisitEntry[];
|
||||
subspecialty?: SubspecialtyEntry[];
|
||||
edVisits?: VisitEntry[];
|
||||
labs?: LabEntry[];
|
||||
model?: string;
|
||||
additionalInstructions?: string;
|
||||
};
|
||||
|
||||
const today = new Date().toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
|
||||
let clinicalData = `Today's date: ${today}\nPatient: ${patientAge || 'Unknown'}, ${patientGender || 'Unknown'}`;
|
||||
if (pmh) clinicalData += `\nPMH: ${wrapUserText('pmh', pmh)}`;
|
||||
|
||||
// Use the top-level review type to select the prompt —
|
||||
// the per-visit types only control data formatting, not the prompt.
|
||||
let prompt;
|
||||
if (type === 'ed') {
|
||||
prompt = PROMPTS.chartReviewED;
|
||||
} else if (type === 'subspecialty') {
|
||||
prompt = PROMPTS.chartReviewSubspecialty;
|
||||
} else {
|
||||
prompt = PROMPTS.chartReviewOutpatient;
|
||||
}
|
||||
let prompt: string;
|
||||
if (type === 'ed') prompt = PROMPTS.chartReviewED;
|
||||
else if (type === 'subspecialty') prompt = PROMPTS.chartReviewSubspecialty;
|
||||
else prompt = PROMPTS.chartReviewOutpatient;
|
||||
|
||||
// Include ALL visit data regardless of per-visit type —
|
||||
// an outpatient chart review should include subspecialty consults too
|
||||
(visits || []).forEach(v => {
|
||||
(visits || []).forEach((v) => {
|
||||
clinicalData += `\n\n=== OUTPATIENT VISIT (${v.date}) ===\n` + wrapUserText('visit', v.content || '');
|
||||
if (v.labs && v.labs.trim()) clinicalData += `\n--- Labs from this visit (${v.date}) ---\n` + wrapUserText('labs', v.labs);
|
||||
});
|
||||
(subspecialty || []).forEach(s => {
|
||||
(subspecialty || []).forEach((s) => {
|
||||
clinicalData += `\n\n=== SUBSPECIALTY: ${(s.specialty || 'Subspecialty').toUpperCase()} — ${s.specialistName || 'Unknown'} (${s.date}) ===\n` + wrapUserText('consult', s.content || '');
|
||||
if (s.labs && s.labs.trim()) clinicalData += `\n--- Labs from this visit (${s.date}) ---\n` + wrapUserText('labs', s.labs);
|
||||
});
|
||||
(edVisits || []).forEach(v => {
|
||||
(edVisits || []).forEach((v) => {
|
||||
clinicalData += `\n\n=== ED VISIT (${v.date}) ===\n` + wrapUserText('ed_visit', v.content || '');
|
||||
if (v.labs && v.labs.trim()) clinicalData += `\n--- Labs from this visit (${v.date}) ---\n` + wrapUserText('labs', v.labs);
|
||||
});
|
||||
|
||||
if (labs && labs.length > 0) {
|
||||
clinicalData += '\n\n=== ADDITIONAL LABS (not tied to a specific visit) ===';
|
||||
labs.forEach(l => { clinicalData += `\n${l.date ? l.date + ': ' : ''}` + wrapUserText('labs', l.values || ''); });
|
||||
labs.forEach((l) => { clinicalData += `\n${l.date ? l.date + ': ' : ''}` + wrapUserText('labs', l.values || ''); });
|
||||
}
|
||||
|
||||
if (additionalInstructions) {
|
||||
|
|
@ -61,14 +70,14 @@ router.post('/generate-chart-review', authMiddleware, async (req, res) => {
|
|||
|
||||
const result = await callAI([
|
||||
{ role: 'system', content: prompt + INJECTION_GUARD },
|
||||
{ role: 'user', content: clinicalData }
|
||||
{ role: 'user', content: clinicalData },
|
||||
], { model, maxTokens: 4000 });
|
||||
|
||||
res.json({ success: true, review: result.content, model: result.model });
|
||||
logger.audit(req.user.id, 'generate_chart_review', 'Generated chart review', req, { category: 'clinical' });
|
||||
logger.audit(req.user!.id, 'generate_chart_review', 'Generated chart review', req, { category: 'clinical' });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: 'Request failed' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
export = router;
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { callAI } = require('../utils/ai');
|
||||
const PROMPTS = require('../utils/prompts');
|
||||
const { authMiddleware } = require('../middleware/auth');
|
||||
const db = require('../db/database');
|
||||
var logger = require('../utils/logger');
|
||||
var { wrapUserText, INJECTION_GUARD } = require('../utils/promptSafe');
|
||||
|
||||
// Get all milestones for client (authenticated users)
|
||||
router.get('/milestones-data', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const result = await db.query(
|
||||
'SELECT * FROM developmental_milestones ORDER BY age_group, domain, sort_order, id'
|
||||
);
|
||||
|
||||
// Group by age_group and domain to match the static data structure
|
||||
const grouped = {};
|
||||
result.rows.forEach(row => {
|
||||
if (!grouped[row.age_group]) {
|
||||
grouped[row.age_group] = {};
|
||||
}
|
||||
if (!grouped[row.age_group][row.domain]) {
|
||||
grouped[row.age_group][row.domain] = [];
|
||||
}
|
||||
grouped[row.age_group][row.domain].push(row.milestone_text);
|
||||
});
|
||||
|
||||
res.json({ success: true, milestones: grouped });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: 'Request failed' });
|
||||
}
|
||||
});
|
||||
|
||||
// Narrative format
|
||||
router.post('/generate-milestone-narrative', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const { milestones, ageGroup, patientAge, patientGender, model, format } = req.body;
|
||||
if (!milestones) return res.status(400).json({ error: 'No milestones' });
|
||||
|
||||
const achieved = milestones.filter(m => m.status === 'yes');
|
||||
const notAchieved = milestones.filter(m => m.status === 'no');
|
||||
const notAssessed = milestones.filter(m => m.status === null);
|
||||
|
||||
if (achieved.length === 0 && notAchieved.length === 0) {
|
||||
return res.json({ success: true, narrative: 'No developmental milestones were assessed during this visit.', summary: { achieved: 0, notAchieved: 0, notAssessed: notAssessed.length } });
|
||||
}
|
||||
|
||||
let milestoneText = '';
|
||||
if (achieved.length > 0) {
|
||||
milestoneText += 'ACHIEVED:\n';
|
||||
achieved.forEach(m => { milestoneText += ` - [${m.domain}] ${m.milestone}\n`; });
|
||||
}
|
||||
if (notAchieved.length > 0) {
|
||||
milestoneText += '\nNOT YET ACHIEVED:\n';
|
||||
notAchieved.forEach(m => { milestoneText += ` - [${m.domain}] ${m.milestone}\n`; });
|
||||
}
|
||||
|
||||
// Choose narrative or list format
|
||||
const prompt = (format === 'list') ? PROMPTS.milestoneList : PROMPTS.milestoneNarrative;
|
||||
|
||||
const result = await callAI([
|
||||
{ role: 'system', content: prompt + INJECTION_GUARD },
|
||||
{ role: 'user', content: `Patient: ${patientAge || 'Unknown'}, ${patientGender || 'Unknown'}\nAge Group: ${ageGroup}\n\n` + wrapUserText('milestones', milestoneText) + `\n\nAssessed: ${achieved.length + notAchieved.length} | Achieved: ${achieved.length} | Not achieved: ${notAchieved.length} | Not assessed (OMIT): ${notAssessed.length}` }
|
||||
], { model });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
narrative: result.content,
|
||||
model: result.model,
|
||||
summary: { achieved: achieved.length, notAchieved: notAchieved.length, notAssessed: notAssessed.length }
|
||||
});
|
||||
logger.audit(req.user.id, 'generate_milestone_narrative', 'Generated milestone narrative', req, { category: 'clinical' });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: 'Request failed' });
|
||||
}
|
||||
});
|
||||
|
||||
// 3-sentence summary
|
||||
router.post('/generate-milestone-summary', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const { narrative, ageGroup, patientAge, patientGender, model } = req.body;
|
||||
if (!narrative) return res.status(400).json({ error: 'No narrative' });
|
||||
|
||||
const result = await callAI([
|
||||
{ role: 'system', content: PROMPTS.milestoneSummary + INJECTION_GUARD },
|
||||
{ role: 'user', content: `Patient: ${patientAge || 'Unknown'}, ${patientGender || 'Unknown'}\nAge Group: ${ageGroup}\n\nNARRATIVE:\n` + wrapUserText('narrative', narrative) }
|
||||
], { model, maxTokens: 500 });
|
||||
|
||||
res.json({ success: true, summary: result.content, model: result.model });
|
||||
logger.audit(req.user.id, 'generate_milestone_summary', 'Generated milestone summary', req, { category: 'clinical' });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: 'Request failed' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
119
src/routes/milestones.ts
Normal file
119
src/routes/milestones.ts
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
import express = require('express');
|
||||
import type { Request, Response } from 'express';
|
||||
const { callAI } = require('../utils/ai');
|
||||
const PROMPTS = require('../utils/prompts');
|
||||
const { authMiddleware } = require('../middleware/auth');
|
||||
const db = require('../db/database');
|
||||
const logger = require('../utils/logger');
|
||||
const { wrapUserText, INJECTION_GUARD } = require('../utils/promptSafe');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
interface MilestoneItem {
|
||||
domain: string;
|
||||
milestone: string;
|
||||
status: 'yes' | 'no' | null;
|
||||
}
|
||||
|
||||
// Get all milestones for client (authenticated users)
|
||||
router.get('/milestones-data', authMiddleware, async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const result = await db.query(
|
||||
'SELECT * FROM developmental_milestones ORDER BY age_group, domain, sort_order, id'
|
||||
);
|
||||
|
||||
const grouped: Record<string, Record<string, string[]>> = {};
|
||||
result.rows.forEach((row: any) => {
|
||||
if (!grouped[row.age_group]) grouped[row.age_group] = {};
|
||||
if (!grouped[row.age_group][row.domain]) grouped[row.age_group][row.domain] = [];
|
||||
grouped[row.age_group][row.domain].push(row.milestone_text);
|
||||
});
|
||||
|
||||
res.json({ success: true, milestones: grouped });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: 'Request failed' });
|
||||
}
|
||||
});
|
||||
|
||||
// Narrative format
|
||||
router.post('/generate-milestone-narrative', authMiddleware, async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { milestones, ageGroup, patientAge, patientGender, model, format } = req.body as {
|
||||
milestones: MilestoneItem[];
|
||||
ageGroup?: string;
|
||||
patientAge?: string;
|
||||
patientGender?: string;
|
||||
model?: string;
|
||||
format?: string;
|
||||
};
|
||||
if (!milestones) return res.status(400).json({ error: 'No milestones' });
|
||||
|
||||
const achieved = milestones.filter((m) => m.status === 'yes');
|
||||
const notAchieved = milestones.filter((m) => m.status === 'no');
|
||||
const notAssessed = milestones.filter((m) => m.status === null);
|
||||
|
||||
if (achieved.length === 0 && notAchieved.length === 0) {
|
||||
return res.json({
|
||||
success: true,
|
||||
narrative: 'No developmental milestones were assessed during this visit.',
|
||||
summary: { achieved: 0, notAchieved: 0, notAssessed: notAssessed.length },
|
||||
});
|
||||
}
|
||||
|
||||
let milestoneText = '';
|
||||
if (achieved.length > 0) {
|
||||
milestoneText += 'ACHIEVED:\n';
|
||||
achieved.forEach((m) => { milestoneText += ` - [${m.domain}] ${m.milestone}\n`; });
|
||||
}
|
||||
if (notAchieved.length > 0) {
|
||||
milestoneText += '\nNOT YET ACHIEVED:\n';
|
||||
notAchieved.forEach((m) => { milestoneText += ` - [${m.domain}] ${m.milestone}\n`; });
|
||||
}
|
||||
|
||||
const prompt = format === 'list' ? PROMPTS.milestoneList : PROMPTS.milestoneNarrative;
|
||||
|
||||
const result = await callAI([
|
||||
{ role: 'system', content: prompt + INJECTION_GUARD },
|
||||
{
|
||||
role: 'user',
|
||||
content: `Patient: ${patientAge || 'Unknown'}, ${patientGender || 'Unknown'}\nAge Group: ${ageGroup}\n\n`
|
||||
+ wrapUserText('milestones', milestoneText)
|
||||
+ `\n\nAssessed: ${achieved.length + notAchieved.length} | Achieved: ${achieved.length} | Not achieved: ${notAchieved.length} | Not assessed (OMIT): ${notAssessed.length}`,
|
||||
},
|
||||
], { model });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
narrative: result.content,
|
||||
model: result.model,
|
||||
summary: { achieved: achieved.length, notAchieved: notAchieved.length, notAssessed: notAssessed.length },
|
||||
});
|
||||
logger.audit(req.user!.id, 'generate_milestone_narrative', 'Generated milestone narrative', req, { category: 'clinical' });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: 'Request failed' });
|
||||
}
|
||||
});
|
||||
|
||||
// 3-sentence summary
|
||||
router.post('/generate-milestone-summary', authMiddleware, async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { narrative, ageGroup, patientAge, patientGender, model } = req.body;
|
||||
if (!narrative) return res.status(400).json({ error: 'No narrative' });
|
||||
|
||||
const result = await callAI([
|
||||
{ role: 'system', content: PROMPTS.milestoneSummary + INJECTION_GUARD },
|
||||
{
|
||||
role: 'user',
|
||||
content: `Patient: ${patientAge || 'Unknown'}, ${patientGender || 'Unknown'}\nAge Group: ${ageGroup}\n\nNARRATIVE:\n`
|
||||
+ wrapUserText('narrative', narrative),
|
||||
},
|
||||
], { model, maxTokens: 500 });
|
||||
|
||||
res.json({ success: true, summary: result.content, model: result.model });
|
||||
logger.audit(req.user!.id, 'generate_milestone_summary', 'Generated milestone summary', req, { category: 'clinical' });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: 'Request failed' });
|
||||
}
|
||||
});
|
||||
|
||||
export = router;
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
// ============================================================
|
||||
// PHYSICAL EXAM GUIDE — step-level narrative + list generation
|
||||
// ============================================================
|
||||
// Receives a flat array of exam STEPS (each with component name, label,
|
||||
// method, expected normal, status, and optional abnormal note). Returns
|
||||
// a professional two-section report (Technique performed + Findings).
|
||||
// Mirrors the milestone-narrative pattern: same AppRole-injection guard,
|
||||
// same clinical audit category.
|
||||
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
var { callAI } = require('../utils/ai');
|
||||
var PROMPTS = require('../utils/prompts');
|
||||
var { wrapUserText, INJECTION_GUARD } = require('../utils/promptSafe');
|
||||
var { authMiddleware } = require('../middleware/auth');
|
||||
var logger = require('../utils/logger');
|
||||
|
||||
router.post('/generate-pe-narrative', authMiddleware, async function (req, res) {
|
||||
try {
|
||||
var { steps, ageGroup, system, patientAge, patientGender, model, format } = req.body;
|
||||
if (!Array.isArray(steps) || steps.length === 0) {
|
||||
return res.status(400).json({ error: 'No steps provided' });
|
||||
}
|
||||
|
||||
// Partition by assessment status
|
||||
var normal = steps.filter(function (s) { return s.status === 'normal'; });
|
||||
var abnormal = steps.filter(function (s) { return s.status === 'abnormal'; });
|
||||
|
||||
if (normal.length === 0 && abnormal.length === 0) {
|
||||
return res.json({
|
||||
success: true,
|
||||
narrative: 'No physical-exam steps were assessed.',
|
||||
summary: { normal: 0, abnormal: 0, notAssessed: steps.length }
|
||||
});
|
||||
}
|
||||
|
||||
// Group by component for readability — AI works better with structure.
|
||||
function groupByComponent(list) {
|
||||
var groups = {};
|
||||
list.forEach(function (s) {
|
||||
var key = s.component || 'Other';
|
||||
if (!groups[key]) groups[key] = [];
|
||||
groups[key].push(s);
|
||||
});
|
||||
return groups;
|
||||
}
|
||||
var normalGroups = groupByComponent(normal);
|
||||
var abnormalGroups = groupByComponent(abnormal);
|
||||
|
||||
// Build structured text for the model
|
||||
var text = '';
|
||||
if (Object.keys(normalGroups).length) {
|
||||
text += 'NORMAL FINDINGS (grouped by exam component):\n';
|
||||
Object.keys(normalGroups).forEach(function (comp) {
|
||||
text += '\n[' + comp + ']\n';
|
||||
normalGroups[comp].forEach(function (s) {
|
||||
text += ' • ' + s.label + '\n';
|
||||
text += ' method: ' + (s.method || 'standard method') + '\n';
|
||||
text += ' expected normal: ' + (s.normal || 'unspecified') + '\n';
|
||||
});
|
||||
});
|
||||
}
|
||||
if (Object.keys(abnormalGroups).length) {
|
||||
text += '\nABNORMAL FINDINGS (grouped by exam component):\n';
|
||||
Object.keys(abnormalGroups).forEach(function (comp) {
|
||||
text += '\n[' + comp + ']\n';
|
||||
abnormalGroups[comp].forEach(function (s) {
|
||||
text += ' • ' + s.label + '\n';
|
||||
text += ' method: ' + (s.method || 'standard method') + '\n';
|
||||
text += ' physician note: ' + (s.note || 'abnormal, no detail provided') + '\n';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var prompt = (format === 'list') ? PROMPTS.peGuideList : PROMPTS.peGuideNarrative;
|
||||
var systemLabel = (system === 'neuro') ? 'Neurologic' : 'Musculoskeletal';
|
||||
|
||||
var result = await callAI([
|
||||
{ role: 'system', content: prompt + INJECTION_GUARD },
|
||||
{ role: 'user', content:
|
||||
'Patient: ' + (patientAge || 'Unknown') + ', ' + (patientGender || 'Unknown') + '\n' +
|
||||
'Age group: ' + (ageGroup || 'Unknown') + '\n' +
|
||||
'Exam system: ' + systemLabel + '\n\n' +
|
||||
wrapUserText('pe_steps', text) +
|
||||
'\n\nAssessed steps: ' + (normal.length + abnormal.length) +
|
||||
' | Normal: ' + normal.length +
|
||||
' | Abnormal: ' + abnormal.length
|
||||
}
|
||||
], { model: model });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
narrative: result.content,
|
||||
model: result.model,
|
||||
summary: { normal: normal.length, abnormal: abnormal.length, notAssessed: steps.length - normal.length - abnormal.length }
|
||||
});
|
||||
logger.audit(req.user.id, 'generate_pe_narrative', 'Generated PE narrative (' + systemLabel + ')', req, { category: 'clinical' });
|
||||
} catch (err) {
|
||||
logger.error('POST /generate-pe-narrative', err.message);
|
||||
res.status(500).json({ error: 'Request failed' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
122
src/routes/peGuide.ts
Normal file
122
src/routes/peGuide.ts
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
// ============================================================
|
||||
// PHYSICAL EXAM GUIDE — step-level narrative + list generation
|
||||
// ============================================================
|
||||
// Receives a flat array of exam STEPS (each with component name, label,
|
||||
// method, expected normal, status, and optional abnormal note). Returns
|
||||
// a professional two-section report (Technique performed + Findings).
|
||||
// Mirrors the milestone-narrative pattern: same AppRole-injection guard,
|
||||
// same clinical audit category.
|
||||
|
||||
import express = require('express');
|
||||
import type { Request, Response } from 'express';
|
||||
const { callAI } = require('../utils/ai');
|
||||
const PROMPTS = require('../utils/prompts');
|
||||
const { wrapUserText, INJECTION_GUARD } = require('../utils/promptSafe');
|
||||
const { authMiddleware } = require('../middleware/auth');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
interface PeStep {
|
||||
component?: string;
|
||||
label: string;
|
||||
method?: string;
|
||||
normal?: string;
|
||||
status?: 'normal' | 'abnormal' | null;
|
||||
note?: string;
|
||||
}
|
||||
|
||||
router.post('/generate-pe-narrative', authMiddleware, async function (req: Request, res: Response) {
|
||||
try {
|
||||
const { steps, ageGroup, system, patientAge, patientGender, model, format } = req.body as {
|
||||
steps: PeStep[];
|
||||
ageGroup?: string;
|
||||
system?: string;
|
||||
patientAge?: string;
|
||||
patientGender?: string;
|
||||
model?: string;
|
||||
format?: string;
|
||||
};
|
||||
if (!Array.isArray(steps) || steps.length === 0) {
|
||||
return res.status(400).json({ error: 'No steps provided' });
|
||||
}
|
||||
|
||||
const normal = steps.filter((s) => s.status === 'normal');
|
||||
const abnormal = steps.filter((s) => s.status === 'abnormal');
|
||||
|
||||
if (normal.length === 0 && abnormal.length === 0) {
|
||||
return res.json({
|
||||
success: true,
|
||||
narrative: 'No physical-exam steps were assessed.',
|
||||
summary: { normal: 0, abnormal: 0, notAssessed: steps.length },
|
||||
});
|
||||
}
|
||||
|
||||
function groupByComponent(list: PeStep[]): Record<string, PeStep[]> {
|
||||
const groups: Record<string, PeStep[]> = {};
|
||||
list.forEach((s) => {
|
||||
const key = s.component || 'Other';
|
||||
if (!groups[key]) groups[key] = [];
|
||||
groups[key].push(s);
|
||||
});
|
||||
return groups;
|
||||
}
|
||||
const normalGroups = groupByComponent(normal);
|
||||
const abnormalGroups = groupByComponent(abnormal);
|
||||
|
||||
let text = '';
|
||||
if (Object.keys(normalGroups).length) {
|
||||
text += 'NORMAL FINDINGS (grouped by exam component):\n';
|
||||
Object.keys(normalGroups).forEach((comp) => {
|
||||
text += '\n[' + comp + ']\n';
|
||||
normalGroups[comp].forEach((s) => {
|
||||
text += ' • ' + s.label + '\n';
|
||||
text += ' method: ' + (s.method || 'standard method') + '\n';
|
||||
text += ' expected normal: ' + (s.normal || 'unspecified') + '\n';
|
||||
});
|
||||
});
|
||||
}
|
||||
if (Object.keys(abnormalGroups).length) {
|
||||
text += '\nABNORMAL FINDINGS (grouped by exam component):\n';
|
||||
Object.keys(abnormalGroups).forEach((comp) => {
|
||||
text += '\n[' + comp + ']\n';
|
||||
abnormalGroups[comp].forEach((s) => {
|
||||
text += ' • ' + s.label + '\n';
|
||||
text += ' method: ' + (s.method || 'standard method') + '\n';
|
||||
text += ' physician note: ' + (s.note || 'abnormal, no detail provided') + '\n';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const prompt = format === 'list' ? PROMPTS.peGuideList : PROMPTS.peGuideNarrative;
|
||||
const systemLabel = system === 'neuro' ? 'Neurologic' : 'Musculoskeletal';
|
||||
|
||||
const result = await callAI([
|
||||
{ role: 'system', content: prompt + INJECTION_GUARD },
|
||||
{
|
||||
role: 'user',
|
||||
content:
|
||||
'Patient: ' + (patientAge || 'Unknown') + ', ' + (patientGender || 'Unknown') + '\n' +
|
||||
'Age group: ' + (ageGroup || 'Unknown') + '\n' +
|
||||
'Exam system: ' + systemLabel + '\n\n' +
|
||||
wrapUserText('pe_steps', text) +
|
||||
'\n\nAssessed steps: ' + (normal.length + abnormal.length) +
|
||||
' | Normal: ' + normal.length +
|
||||
' | Abnormal: ' + abnormal.length,
|
||||
},
|
||||
], { model });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
narrative: result.content,
|
||||
model: result.model,
|
||||
summary: { normal: normal.length, abnormal: abnormal.length, notAssessed: steps.length - normal.length - abnormal.length },
|
||||
});
|
||||
logger.audit(req.user!.id, 'generate_pe_narrative', 'Generated PE narrative (' + systemLabel + ')', req, { category: 'clinical' });
|
||||
} catch (err: any) {
|
||||
logger.error('POST /generate-pe-narrative', err.message);
|
||||
res.status(500).json({ error: 'Request failed' });
|
||||
}
|
||||
});
|
||||
|
||||
export = router;
|
||||
|
|
@ -2,32 +2,32 @@
|
|||
// SICK VISIT ROUTE — sick visit note generation
|
||||
// ============================================================
|
||||
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
var { callAI } = require('../utils/ai');
|
||||
var PROMPTS = require('../utils/prompts');
|
||||
var { authMiddleware } = require('../middleware/auth');
|
||||
var logger = require('../utils/logger');
|
||||
var { wrapUserText, INJECTION_GUARD } = require('../utils/promptSafe');
|
||||
import express = require('express');
|
||||
import type { Request, Response } from 'express';
|
||||
const { callAI } = require('../utils/ai');
|
||||
const PROMPTS = require('../utils/prompts');
|
||||
const { authMiddleware } = require('../middleware/auth');
|
||||
const logger = require('../utils/logger');
|
||||
const { wrapUserText, INJECTION_GUARD } = require('../utils/promptSafe');
|
||||
|
||||
// ── POST generate sick visit note ────────────────────────────────────────
|
||||
router.post('/sick-visit/note', authMiddleware, async function(req, res) {
|
||||
var start = Date.now();
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/sick-visit/note', authMiddleware, async function (req: Request, res: Response) {
|
||||
const start = Date.now();
|
||||
try {
|
||||
var {
|
||||
const {
|
||||
patientAge, patientGender, chiefComplaint,
|
||||
transcript, dictation,
|
||||
ros, physicalExam, diagnoses,
|
||||
physicianMemories,
|
||||
model
|
||||
model,
|
||||
} = req.body;
|
||||
|
||||
if (!chiefComplaint) {
|
||||
return res.status(400).json({ error: 'Chief complaint is required' });
|
||||
}
|
||||
|
||||
// Assemble context
|
||||
var context = 'SICK VISIT\n';
|
||||
let context = 'SICK VISIT\n';
|
||||
context += 'Patient: ' + (patientAge || 'Unknown age') + ', ' + (patientGender || 'Unknown gender') + '\n';
|
||||
context += 'Chief Complaint: ' + wrapUserText('chief_complaint', chiefComplaint) + '\n\n';
|
||||
|
||||
|
|
@ -36,31 +36,31 @@ router.post('/sick-visit/note', authMiddleware, async function(req, res) {
|
|||
} else if (dictation) {
|
||||
context += 'PHYSICIAN DICTATION:\n' + wrapUserText('dictation', dictation) + '\n\n';
|
||||
}
|
||||
if (ros) context += wrapUserText('ros', ros) + '\n\n';
|
||||
if (ros) context += wrapUserText('ros', ros) + '\n\n';
|
||||
if (physicalExam) context += wrapUserText('physical_exam', physicalExam) + '\n\n';
|
||||
if (diagnoses) context += wrapUserText('diagnoses', diagnoses) + '\n\n';
|
||||
if (diagnoses) context += wrapUserText('diagnoses', diagnoses) + '\n\n';
|
||||
if (physicianMemories) {
|
||||
context += '[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]\n\n';
|
||||
}
|
||||
|
||||
var result = await callAI([
|
||||
const result = await callAI([
|
||||
{ role: 'system', content: PROMPTS.sickVisitNote + INJECTION_GUARD },
|
||||
{ role: 'user', content: context }
|
||||
{ role: 'user', content: context },
|
||||
], { model });
|
||||
|
||||
var dur = Date.now() - start;
|
||||
logger.apiCall(req.user.id, '/api/sick-visit/note', {
|
||||
const dur = Date.now() - start;
|
||||
logger.apiCall(req.user!.id, '/api/sick-visit/note', {
|
||||
model: result.model, tokensInput: result.usage?.input_tokens,
|
||||
tokensOutput: result.usage?.output_tokens, duration: dur, statusCode: 200
|
||||
tokensOutput: result.usage?.output_tokens, duration: dur, statusCode: 200,
|
||||
});
|
||||
|
||||
res.json({ success: true, note: result.content, model: result.model });
|
||||
logger.audit(req.user.id, 'generate_sick_visit', 'Generated sick visit note', req, { category: 'clinical' });
|
||||
} catch (e) {
|
||||
logger.audit(req.user!.id, 'generate_sick_visit', 'Generated sick visit note', req, { category: 'clinical' });
|
||||
} catch (e: any) {
|
||||
logger.error('[SickVisit] Note generation failed', e.message);
|
||||
res.status(500).json({ error: 'Request failed' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
export = router;
|
||||
|
|
@ -2,38 +2,38 @@
|
|||
// WELL VISIT ROUTES — SSHADESS assessment + note generation
|
||||
// ============================================================
|
||||
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
var { callAI } = require('../utils/ai');
|
||||
var PROMPTS = require('../utils/prompts');
|
||||
var { authMiddleware } = require('../middleware/auth');
|
||||
var logger = require('../utils/logger');
|
||||
var { wrapUserText, INJECTION_GUARD } = require('../utils/promptSafe');
|
||||
import express = require('express');
|
||||
import type { Request, Response } from 'express';
|
||||
const { callAI } = require('../utils/ai');
|
||||
const PROMPTS = require('../utils/prompts');
|
||||
const { authMiddleware } = require('../middleware/auth');
|
||||
const logger = require('../utils/logger');
|
||||
const { wrapUserText, INJECTION_GUARD } = require('../utils/promptSafe');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Load growth reference and BMI classification data
|
||||
var scheduleData;
|
||||
let scheduleData: any;
|
||||
try {
|
||||
scheduleData = require('../../public/js/pediatricScheduleData');
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
scheduleData = {};
|
||||
}
|
||||
var GROWTH_REFERENCE = scheduleData.GROWTH_REFERENCE || {};
|
||||
var BMI_CLASSIFICATION = scheduleData.BMI_CLASSIFICATION || {};
|
||||
const GROWTH_REFERENCE: Record<string, any> = scheduleData.GROWTH_REFERENCE || {};
|
||||
const BMI_CLASSIFICATION: any = scheduleData.BMI_CLASSIFICATION || {};
|
||||
|
||||
// Map visit age text to GROWTH_REFERENCE keys
|
||||
function getGrowthRefForAge(ageStr) {
|
||||
function getGrowthRefForAge(ageStr?: string): any {
|
||||
if (!ageStr) return null;
|
||||
var a = ageStr.toLowerCase().trim();
|
||||
// Direct key matches
|
||||
var directKeys = ['newborn', '1mo', '2mo', '4mo', '6mo', '9mo', '12mo', '15mo', '18mo', '24mo', '30mo', '3y', '4y', '5y'];
|
||||
for (var i = 0; i < directKeys.length; i++) {
|
||||
const a = ageStr.toLowerCase().trim();
|
||||
const directKeys = ['newborn', '1mo', '2mo', '4mo', '6mo', '9mo', '12mo', '15mo', '18mo', '24mo', '30mo', '3y', '4y', '5y'];
|
||||
for (let i = 0; i < directKeys.length; i++) {
|
||||
if (a.indexOf(directKeys[i]) !== -1) return GROWTH_REFERENCE[directKeys[i]] || null;
|
||||
}
|
||||
// Age-based range matching
|
||||
var ageMatch = a.match(/(\d+)\s*(month|mo|year|y|yr)/i);
|
||||
const ageMatch = a.match(/(\d+)\s*(month|mo|year|y|yr)/i);
|
||||
if (ageMatch) {
|
||||
var num = parseInt(ageMatch[1]);
|
||||
var unit = ageMatch[2].toLowerCase();
|
||||
const num = parseInt(ageMatch[1]);
|
||||
const unit = ageMatch[2].toLowerCase();
|
||||
if (unit === 'month' || unit === 'mo') {
|
||||
if (num <= 1) return GROWTH_REFERENCE['1mo'];
|
||||
if (num <= 3) return GROWTH_REFERENCE['2mo'];
|
||||
|
|
@ -60,17 +60,16 @@ function getGrowthRefForAge(ageStr) {
|
|||
}
|
||||
|
||||
// ── POST generate SSHADESS assessment summary ────────────────────────────
|
||||
router.post('/well-visit/shadess', authMiddleware, async function(req, res) {
|
||||
var start = Date.now();
|
||||
router.post('/well-visit/shadess', authMiddleware, async function (req: Request, res: Response) {
|
||||
const start = Date.now();
|
||||
try {
|
||||
var { domains, patientAge, patientGender, dictationText, model } = req.body;
|
||||
const { domains, patientAge, patientGender, dictationText, model } = req.body;
|
||||
|
||||
if (!domains && !dictationText) {
|
||||
return res.status(400).json({ error: 'Provide domain responses or dictation text' });
|
||||
}
|
||||
|
||||
// Build structured input text
|
||||
var inputText = 'PATIENT: ' + (patientAge || 'Adolescent') + ', ' + (patientGender || 'Unknown gender') + '\n\n';
|
||||
let inputText = 'PATIENT: ' + (patientAge || 'Adolescent') + ', ' + (patientGender || 'Unknown gender') + '\n\n';
|
||||
inputText += 'SSHADESS SCREENING DATA:\n\n';
|
||||
|
||||
if (dictationText) {
|
||||
|
|
@ -78,8 +77,8 @@ router.post('/well-visit/shadess', authMiddleware, async function(req, res) {
|
|||
}
|
||||
|
||||
if (domains) {
|
||||
var domainOrder = ['strengths', 'school', 'home', 'activities', 'drugs', 'emotions', 'sexuality', 'safety'];
|
||||
var domainNames = {
|
||||
const domainOrder = ['strengths', 'school', 'home', 'activities', 'drugs', 'emotions', 'sexuality', 'safety'];
|
||||
const domainNames: Record<string, string> = {
|
||||
strengths: 'STRENGTHS',
|
||||
school: 'SCHOOL',
|
||||
home: 'HOME',
|
||||
|
|
@ -87,16 +86,16 @@ router.post('/well-visit/shadess', authMiddleware, async function(req, res) {
|
|||
drugs: 'DRUGS/SUBSTANCES',
|
||||
emotions: 'EMOTIONS/EATING',
|
||||
sexuality: 'SEXUALITY',
|
||||
safety: 'SAFETY'
|
||||
safety: 'SAFETY',
|
||||
};
|
||||
|
||||
domainOrder.forEach(function(key) {
|
||||
var d = domains[key];
|
||||
domainOrder.forEach((key) => {
|
||||
const d = domains[key];
|
||||
if (!d || d.skipped) return;
|
||||
|
||||
inputText += domainNames[key] + ':\n';
|
||||
if (d.questions) {
|
||||
d.questions.forEach(function(q) {
|
||||
d.questions.forEach((q: any) => {
|
||||
if (q.answer !== null && q.answer !== undefined && q.answer !== '') {
|
||||
inputText += ' - ' + q.text + ': ' + (q.answer === true || q.answer === 'yes' ? 'YES' : (q.answer === false || q.answer === 'no' ? 'NO' : q.answer)) + '\n';
|
||||
}
|
||||
|
|
@ -112,30 +111,30 @@ router.post('/well-visit/shadess', authMiddleware, async function(req, res) {
|
|||
});
|
||||
}
|
||||
|
||||
var result = await callAI([
|
||||
const result = await callAI([
|
||||
{ role: 'system', content: PROMPTS.shadessAssessment + INJECTION_GUARD },
|
||||
{ role: 'user', content: wrapUserText('sshadess_answers', inputText) }
|
||||
{ role: 'user', content: wrapUserText('sshadess_answers', inputText) },
|
||||
], { model });
|
||||
|
||||
var dur = Date.now() - start;
|
||||
logger.apiCall(req.user.id, '/api/well-visit/shadess', {
|
||||
const dur = Date.now() - start;
|
||||
logger.apiCall(req.user!.id, '/api/well-visit/shadess', {
|
||||
model: result.model, tokensInput: result.usage?.input_tokens,
|
||||
tokensOutput: result.usage?.output_tokens, duration: dur, statusCode: 200
|
||||
tokensOutput: result.usage?.output_tokens, duration: dur, statusCode: 200,
|
||||
});
|
||||
|
||||
res.json({ success: true, assessment: result.content, model: result.model });
|
||||
logger.audit(req.user.id, 'generate_shadess', 'Generated SSHADESS assessment', req, { category: 'clinical' });
|
||||
} catch (e) {
|
||||
logger.audit(req.user!.id, 'generate_shadess', 'Generated SSHADESS assessment', req, { category: 'clinical' });
|
||||
} catch (e: any) {
|
||||
logger.error('[WellVisit] SHADESS generation failed', e.message);
|
||||
res.status(500).json({ error: 'Request failed' });
|
||||
}
|
||||
});
|
||||
|
||||
// ── POST generate full well visit encounter note ─────────────────────────
|
||||
router.post('/well-visit/note', authMiddleware, async function(req, res) {
|
||||
var start = Date.now();
|
||||
router.post('/well-visit/note', authMiddleware, async function (req: Request, res: Response) {
|
||||
const start = Date.now();
|
||||
try {
|
||||
var {
|
||||
const {
|
||||
patientAge, patientGender, visitAge,
|
||||
vitals, measurements,
|
||||
transcript, dictation,
|
||||
|
|
@ -144,36 +143,34 @@ router.post('/well-visit/note', authMiddleware, async function(req, res) {
|
|||
parentConcerns,
|
||||
ros, physicalExam, diagnoses,
|
||||
physicianMemories,
|
||||
noteStyle, // 'full' | 'short'
|
||||
model
|
||||
noteStyle,
|
||||
model,
|
||||
} = req.body;
|
||||
|
||||
if (!patientAge && !visitAge) {
|
||||
return res.status(400).json({ error: 'Patient age or visit age required' });
|
||||
}
|
||||
|
||||
// Assemble context
|
||||
var context = 'WELL CHILD VISIT\n';
|
||||
let context = 'WELL CHILD VISIT\n';
|
||||
context += 'Patient: ' + (patientAge || visitAge) + ', ' + (patientGender || 'Unknown') + '\n\n';
|
||||
|
||||
if (vitals) context += 'VITAL SIGNS:\n' + wrapUserText('vitals', vitals) + '\n\n';
|
||||
if (vitals) context += 'VITAL SIGNS:\n' + wrapUserText('vitals', vitals) + '\n\n';
|
||||
if (measurements) context += 'MEASUREMENTS/GROWTH:\n' + wrapUserText('measurements', measurements) + '\n\n';
|
||||
if (parentConcerns) context += 'PARENT/PATIENT CONCERNS:\n' + wrapUserText('concerns', parentConcerns) + '\n\n';
|
||||
if (transcript) context += 'ENCOUNTER TRANSCRIPT/DICTATION:\n' + wrapUserText('transcript', transcript) + '\n\n';
|
||||
if (transcript) context += 'ENCOUNTER TRANSCRIPT/DICTATION:\n' + wrapUserText('transcript', transcript) + '\n\n';
|
||||
else if (dictation) context += 'PHYSICIAN DICTATION:\n' + wrapUserText('dictation', dictation) + '\n\n';
|
||||
if (screenings) context += 'SCREENINGS COMPLETED:\n' + wrapUserText('screenings', screenings) + '\n\n';
|
||||
if (vaccines) context += 'IMMUNIZATIONS TODAY:\n' + wrapUserText('vaccines', vaccines) + '\n\n';
|
||||
if (screenings) context += 'SCREENINGS COMPLETED:\n' + wrapUserText('screenings', screenings) + '\n\n';
|
||||
if (vaccines) context += 'IMMUNIZATIONS TODAY:\n' + wrapUserText('vaccines', vaccines) + '\n\n';
|
||||
if (shadessAssessment) context += 'SSHADESS PSYCHOSOCIAL ASSESSMENT:\n' + wrapUserText('sshadess', shadessAssessment) + '\n\n';
|
||||
if (ros) context += wrapUserText('ros', ros) + '\n\n';
|
||||
if (ros) context += wrapUserText('ros', ros) + '\n\n';
|
||||
if (physicalExam) context += wrapUserText('physical_exam', physicalExam) + '\n\n';
|
||||
if (diagnoses) context += wrapUserText('diagnoses', diagnoses) + '\n\n';
|
||||
if (diagnoses) context += wrapUserText('diagnoses', diagnoses) + '\n\n';
|
||||
if (physicianMemories) {
|
||||
context += '[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]\n\n';
|
||||
}
|
||||
|
||||
// Add growth reference and feeding guidance for this age
|
||||
var growthRef = getGrowthRefForAge(patientAge || visitAge);
|
||||
const growthRef = getGrowthRefForAge(patientAge || visitAge);
|
||||
if (growthRef) {
|
||||
context += 'GROWTH/NUTRITION GUIDANCE (AAP reference for this age):\n';
|
||||
if (growthRef.weight) context += 'Expected weight gain: ' + growthRef.weight + '\n';
|
||||
|
|
@ -181,36 +178,36 @@ router.post('/well-visit/note', authMiddleware, async function(req, res) {
|
|||
if (growthRef.headCirc) context += 'Expected head circumference gain: ' + growthRef.headCirc + '\n';
|
||||
if (growthRef.feeding && growthRef.feeding.length) {
|
||||
context += 'Feeding guidance:\n';
|
||||
growthRef.feeding.forEach(function(f) { context += ' - ' + f + '\n'; });
|
||||
growthRef.feeding.forEach((f: string) => { context += ' - ' + f + '\n'; });
|
||||
}
|
||||
if (growthRef.bmiClassification && BMI_CLASSIFICATION.categories) {
|
||||
context += 'BMI Classification (AAP 2023 / CDC Extended BMI):\n';
|
||||
BMI_CLASSIFICATION.categories.forEach(function(c) {
|
||||
BMI_CLASSIFICATION.categories.forEach((c: any) => {
|
||||
context += ' - ' + c.label + ': ' + c.range + ' → ' + c.action + '\n';
|
||||
});
|
||||
}
|
||||
context += '\n';
|
||||
}
|
||||
|
||||
var prompt = (noteStyle === 'short') ? PROMPTS.wellVisitShort : PROMPTS.wellVisitNote;
|
||||
const prompt = noteStyle === 'short' ? PROMPTS.wellVisitShort : PROMPTS.wellVisitNote;
|
||||
|
||||
var result = await callAI([
|
||||
const result = await callAI([
|
||||
{ role: 'system', content: prompt + INJECTION_GUARD },
|
||||
{ role: 'user', content: context }
|
||||
{ role: 'user', content: context },
|
||||
], { model });
|
||||
|
||||
var dur = Date.now() - start;
|
||||
logger.apiCall(req.user.id, '/api/well-visit/note', {
|
||||
const dur = Date.now() - start;
|
||||
logger.apiCall(req.user!.id, '/api/well-visit/note', {
|
||||
model: result.model, tokensInput: result.usage?.input_tokens,
|
||||
tokensOutput: result.usage?.output_tokens, duration: dur, statusCode: 200
|
||||
tokensOutput: result.usage?.output_tokens, duration: dur, statusCode: 200,
|
||||
});
|
||||
|
||||
res.json({ success: true, note: result.content, model: result.model });
|
||||
logger.audit(req.user.id, 'generate_well_visit', 'Generated well visit note', req, { category: 'clinical' });
|
||||
} catch (e) {
|
||||
logger.audit(req.user!.id, 'generate_well_visit', 'Generated well visit note', req, { category: 'clinical' });
|
||||
} catch (e: any) {
|
||||
logger.error('[WellVisit] Note generation failed', e.message);
|
||||
res.status(500).json({ error: 'Request failed' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
export = router;
|
||||
Loading…
Reference in a new issue