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
5ebabb633a
commit
c48406536b
7 changed files with 366 additions and 320 deletions
|
|
@ -1,58 +1,67 @@
|
||||||
const express = require('express');
|
import express = require('express');
|
||||||
const router = express.Router();
|
import type { Request, Response } from 'express';
|
||||||
const { callAI } = require('../utils/ai');
|
const { callAI } = require('../utils/ai');
|
||||||
const PROMPTS = require('../utils/prompts');
|
const PROMPTS = require('../utils/prompts');
|
||||||
const { authMiddleware } = require('../middleware/auth');
|
const { authMiddleware } = require('../middleware/auth');
|
||||||
var logger = require('../utils/logger');
|
const logger = require('../utils/logger');
|
||||||
var { wrapUserText, INJECTION_GUARD } = require('../utils/promptSafe');
|
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
|
// Generate chart review
|
||||||
router.post('/generate-chart-review', authMiddleware, async (req, res) => {
|
router.post('/generate-chart-review', authMiddleware, async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
type, // 'outpatient' | 'subspecialty' | 'ed'
|
type,
|
||||||
patientAge, patientGender, pmh,
|
patientAge, patientGender, pmh,
|
||||||
visits, // Array of { date, type, content }
|
visits,
|
||||||
subspecialty, // Array of { date, specialistName, specialty, content }
|
subspecialty,
|
||||||
edVisits, // Array of { date, content, labs }
|
edVisits,
|
||||||
labs, // Array of { date, values }
|
labs,
|
||||||
model,
|
model,
|
||||||
additionalInstructions
|
additionalInstructions,
|
||||||
} = req.body;
|
} = 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' });
|
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'}`;
|
let clinicalData = `Today's date: ${today}\nPatient: ${patientAge || 'Unknown'}, ${patientGender || 'Unknown'}`;
|
||||||
if (pmh) clinicalData += `\nPMH: ${wrapUserText('pmh', pmh)}`;
|
if (pmh) clinicalData += `\nPMH: ${wrapUserText('pmh', pmh)}`;
|
||||||
|
|
||||||
// Use the top-level review type to select the prompt —
|
let prompt: string;
|
||||||
// the per-visit types only control data formatting, not the prompt.
|
if (type === 'ed') prompt = PROMPTS.chartReviewED;
|
||||||
let prompt;
|
else if (type === 'subspecialty') prompt = PROMPTS.chartReviewSubspecialty;
|
||||||
if (type === 'ed') {
|
else prompt = PROMPTS.chartReviewOutpatient;
|
||||||
prompt = PROMPTS.chartReviewED;
|
|
||||||
} else if (type === 'subspecialty') {
|
|
||||||
prompt = PROMPTS.chartReviewSubspecialty;
|
|
||||||
} else {
|
|
||||||
prompt = PROMPTS.chartReviewOutpatient;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Include ALL visit data regardless of per-visit type —
|
(visits || []).forEach((v) => {
|
||||||
// an outpatient chart review should include subspecialty consults too
|
|
||||||
(visits || []).forEach(v => {
|
|
||||||
clinicalData += `\n\n=== OUTPATIENT VISIT (${v.date}) ===\n` + wrapUserText('visit', v.content || '');
|
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);
|
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 || '');
|
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);
|
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 || '');
|
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 (v.labs && v.labs.trim()) clinicalData += `\n--- Labs from this visit (${v.date}) ---\n` + wrapUserText('labs', v.labs);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (labs && labs.length > 0) {
|
if (labs && labs.length > 0) {
|
||||||
clinicalData += '\n\n=== ADDITIONAL LABS (not tied to a specific visit) ===';
|
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) {
|
if (additionalInstructions) {
|
||||||
|
|
@ -61,14 +70,14 @@ router.post('/generate-chart-review', authMiddleware, async (req, res) => {
|
||||||
|
|
||||||
const result = await callAI([
|
const result = await callAI([
|
||||||
{ role: 'system', content: prompt + INJECTION_GUARD },
|
{ role: 'system', content: prompt + INJECTION_GUARD },
|
||||||
{ role: 'user', content: clinicalData }
|
{ role: 'user', content: clinicalData },
|
||||||
], { model, maxTokens: 4000 });
|
], { model, maxTokens: 4000 });
|
||||||
|
|
||||||
res.json({ success: true, review: result.content, model: result.model });
|
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) {
|
} catch (err) {
|
||||||
res.status(500).json({ error: 'Request failed' });
|
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
|
// SICK VISIT ROUTE — sick visit note generation
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
var express = require('express');
|
import express = require('express');
|
||||||
var router = express.Router();
|
import type { Request, Response } from 'express';
|
||||||
var { callAI } = require('../utils/ai');
|
const { callAI } = require('../utils/ai');
|
||||||
var PROMPTS = require('../utils/prompts');
|
const PROMPTS = require('../utils/prompts');
|
||||||
var { authMiddleware } = require('../middleware/auth');
|
const { authMiddleware } = require('../middleware/auth');
|
||||||
var logger = require('../utils/logger');
|
const logger = require('../utils/logger');
|
||||||
var { wrapUserText, INJECTION_GUARD } = require('../utils/promptSafe');
|
const { wrapUserText, INJECTION_GUARD } = require('../utils/promptSafe');
|
||||||
|
|
||||||
// ── POST generate sick visit note ────────────────────────────────────────
|
const router = express.Router();
|
||||||
router.post('/sick-visit/note', authMiddleware, async function(req, res) {
|
|
||||||
var start = Date.now();
|
router.post('/sick-visit/note', authMiddleware, async function (req: Request, res: Response) {
|
||||||
|
const start = Date.now();
|
||||||
try {
|
try {
|
||||||
var {
|
const {
|
||||||
patientAge, patientGender, chiefComplaint,
|
patientAge, patientGender, chiefComplaint,
|
||||||
transcript, dictation,
|
transcript, dictation,
|
||||||
ros, physicalExam, diagnoses,
|
ros, physicalExam, diagnoses,
|
||||||
physicianMemories,
|
physicianMemories,
|
||||||
model
|
model,
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
if (!chiefComplaint) {
|
if (!chiefComplaint) {
|
||||||
return res.status(400).json({ error: 'Chief complaint is required' });
|
return res.status(400).json({ error: 'Chief complaint is required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assemble context
|
let context = 'SICK VISIT\n';
|
||||||
var context = 'SICK VISIT\n';
|
|
||||||
context += 'Patient: ' + (patientAge || 'Unknown age') + ', ' + (patientGender || 'Unknown gender') + '\n';
|
context += 'Patient: ' + (patientAge || 'Unknown age') + ', ' + (patientGender || 'Unknown gender') + '\n';
|
||||||
context += 'Chief Complaint: ' + wrapUserText('chief_complaint', chiefComplaint) + '\n\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) {
|
} else if (dictation) {
|
||||||
context += 'PHYSICIAN DICTATION:\n' + wrapUserText('dictation', dictation) + '\n\n';
|
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 (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) {
|
if (physicianMemories) {
|
||||||
context += '[STYLE HINTS (low priority — only apply if relevant to the current note, never copy content from these examples)]\n'
|
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';
|
+ 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: 'system', content: PROMPTS.sickVisitNote + INJECTION_GUARD },
|
||||||
{ role: 'user', content: context }
|
{ role: 'user', content: context },
|
||||||
], { model });
|
], { model });
|
||||||
|
|
||||||
var dur = Date.now() - start;
|
const dur = Date.now() - start;
|
||||||
logger.apiCall(req.user.id, '/api/sick-visit/note', {
|
logger.apiCall(req.user!.id, '/api/sick-visit/note', {
|
||||||
model: result.model, tokensInput: result.usage?.input_tokens,
|
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 });
|
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' });
|
logger.audit(req.user!.id, 'generate_sick_visit', 'Generated sick visit note', req, { category: 'clinical' });
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
logger.error('[SickVisit] Note generation failed', e.message);
|
logger.error('[SickVisit] Note generation failed', e.message);
|
||||||
res.status(500).json({ error: 'Request failed' });
|
res.status(500).json({ error: 'Request failed' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = router;
|
export = router;
|
||||||
|
|
@ -2,38 +2,38 @@
|
||||||
// WELL VISIT ROUTES — SSHADESS assessment + note generation
|
// WELL VISIT ROUTES — SSHADESS assessment + note generation
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
var express = require('express');
|
import express = require('express');
|
||||||
var router = express.Router();
|
import type { Request, Response } from 'express';
|
||||||
var { callAI } = require('../utils/ai');
|
const { callAI } = require('../utils/ai');
|
||||||
var PROMPTS = require('../utils/prompts');
|
const PROMPTS = require('../utils/prompts');
|
||||||
var { authMiddleware } = require('../middleware/auth');
|
const { authMiddleware } = require('../middleware/auth');
|
||||||
var logger = require('../utils/logger');
|
const logger = require('../utils/logger');
|
||||||
var { wrapUserText, INJECTION_GUARD } = require('../utils/promptSafe');
|
const { wrapUserText, INJECTION_GUARD } = require('../utils/promptSafe');
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
// Load growth reference and BMI classification data
|
// Load growth reference and BMI classification data
|
||||||
var scheduleData;
|
let scheduleData: any;
|
||||||
try {
|
try {
|
||||||
scheduleData = require('../../public/js/pediatricScheduleData');
|
scheduleData = require('../../public/js/pediatricScheduleData');
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
scheduleData = {};
|
scheduleData = {};
|
||||||
}
|
}
|
||||||
var GROWTH_REFERENCE = scheduleData.GROWTH_REFERENCE || {};
|
const GROWTH_REFERENCE: Record<string, any> = scheduleData.GROWTH_REFERENCE || {};
|
||||||
var BMI_CLASSIFICATION = scheduleData.BMI_CLASSIFICATION || {};
|
const BMI_CLASSIFICATION: any = scheduleData.BMI_CLASSIFICATION || {};
|
||||||
|
|
||||||
// Map visit age text to GROWTH_REFERENCE keys
|
// Map visit age text to GROWTH_REFERENCE keys
|
||||||
function getGrowthRefForAge(ageStr) {
|
function getGrowthRefForAge(ageStr?: string): any {
|
||||||
if (!ageStr) return null;
|
if (!ageStr) return null;
|
||||||
var a = ageStr.toLowerCase().trim();
|
const a = ageStr.toLowerCase().trim();
|
||||||
// Direct key matches
|
const directKeys = ['newborn', '1mo', '2mo', '4mo', '6mo', '9mo', '12mo', '15mo', '18mo', '24mo', '30mo', '3y', '4y', '5y'];
|
||||||
var directKeys = ['newborn', '1mo', '2mo', '4mo', '6mo', '9mo', '12mo', '15mo', '18mo', '24mo', '30mo', '3y', '4y', '5y'];
|
for (let i = 0; i < directKeys.length; i++) {
|
||||||
for (var i = 0; i < directKeys.length; i++) {
|
|
||||||
if (a.indexOf(directKeys[i]) !== -1) return GROWTH_REFERENCE[directKeys[i]] || null;
|
if (a.indexOf(directKeys[i]) !== -1) return GROWTH_REFERENCE[directKeys[i]] || null;
|
||||||
}
|
}
|
||||||
// Age-based range matching
|
const ageMatch = a.match(/(\d+)\s*(month|mo|year|y|yr)/i);
|
||||||
var ageMatch = a.match(/(\d+)\s*(month|mo|year|y|yr)/i);
|
|
||||||
if (ageMatch) {
|
if (ageMatch) {
|
||||||
var num = parseInt(ageMatch[1]);
|
const num = parseInt(ageMatch[1]);
|
||||||
var unit = ageMatch[2].toLowerCase();
|
const unit = ageMatch[2].toLowerCase();
|
||||||
if (unit === 'month' || unit === 'mo') {
|
if (unit === 'month' || unit === 'mo') {
|
||||||
if (num <= 1) return GROWTH_REFERENCE['1mo'];
|
if (num <= 1) return GROWTH_REFERENCE['1mo'];
|
||||||
if (num <= 3) return GROWTH_REFERENCE['2mo'];
|
if (num <= 3) return GROWTH_REFERENCE['2mo'];
|
||||||
|
|
@ -60,17 +60,16 @@ function getGrowthRefForAge(ageStr) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── POST generate SSHADESS assessment summary ────────────────────────────
|
// ── POST generate SSHADESS assessment summary ────────────────────────────
|
||||||
router.post('/well-visit/shadess', authMiddleware, async function(req, res) {
|
router.post('/well-visit/shadess', authMiddleware, async function (req: Request, res: Response) {
|
||||||
var start = Date.now();
|
const start = Date.now();
|
||||||
try {
|
try {
|
||||||
var { domains, patientAge, patientGender, dictationText, model } = req.body;
|
const { domains, patientAge, patientGender, dictationText, model } = req.body;
|
||||||
|
|
||||||
if (!domains && !dictationText) {
|
if (!domains && !dictationText) {
|
||||||
return res.status(400).json({ error: 'Provide domain responses or dictation text' });
|
return res.status(400).json({ error: 'Provide domain responses or dictation text' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build structured input text
|
let inputText = 'PATIENT: ' + (patientAge || 'Adolescent') + ', ' + (patientGender || 'Unknown gender') + '\n\n';
|
||||||
var inputText = 'PATIENT: ' + (patientAge || 'Adolescent') + ', ' + (patientGender || 'Unknown gender') + '\n\n';
|
|
||||||
inputText += 'SSHADESS SCREENING DATA:\n\n';
|
inputText += 'SSHADESS SCREENING DATA:\n\n';
|
||||||
|
|
||||||
if (dictationText) {
|
if (dictationText) {
|
||||||
|
|
@ -78,8 +77,8 @@ router.post('/well-visit/shadess', authMiddleware, async function(req, res) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (domains) {
|
if (domains) {
|
||||||
var domainOrder = ['strengths', 'school', 'home', 'activities', 'drugs', 'emotions', 'sexuality', 'safety'];
|
const domainOrder = ['strengths', 'school', 'home', 'activities', 'drugs', 'emotions', 'sexuality', 'safety'];
|
||||||
var domainNames = {
|
const domainNames: Record<string, string> = {
|
||||||
strengths: 'STRENGTHS',
|
strengths: 'STRENGTHS',
|
||||||
school: 'SCHOOL',
|
school: 'SCHOOL',
|
||||||
home: 'HOME',
|
home: 'HOME',
|
||||||
|
|
@ -87,16 +86,16 @@ router.post('/well-visit/shadess', authMiddleware, async function(req, res) {
|
||||||
drugs: 'DRUGS/SUBSTANCES',
|
drugs: 'DRUGS/SUBSTANCES',
|
||||||
emotions: 'EMOTIONS/EATING',
|
emotions: 'EMOTIONS/EATING',
|
||||||
sexuality: 'SEXUALITY',
|
sexuality: 'SEXUALITY',
|
||||||
safety: 'SAFETY'
|
safety: 'SAFETY',
|
||||||
};
|
};
|
||||||
|
|
||||||
domainOrder.forEach(function(key) {
|
domainOrder.forEach((key) => {
|
||||||
var d = domains[key];
|
const d = domains[key];
|
||||||
if (!d || d.skipped) return;
|
if (!d || d.skipped) return;
|
||||||
|
|
||||||
inputText += domainNames[key] + ':\n';
|
inputText += domainNames[key] + ':\n';
|
||||||
if (d.questions) {
|
if (d.questions) {
|
||||||
d.questions.forEach(function(q) {
|
d.questions.forEach((q: any) => {
|
||||||
if (q.answer !== null && q.answer !== undefined && q.answer !== '') {
|
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';
|
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: 'system', content: PROMPTS.shadessAssessment + INJECTION_GUARD },
|
||||||
{ role: 'user', content: wrapUserText('sshadess_answers', inputText) }
|
{ role: 'user', content: wrapUserText('sshadess_answers', inputText) },
|
||||||
], { model });
|
], { model });
|
||||||
|
|
||||||
var dur = Date.now() - start;
|
const dur = Date.now() - start;
|
||||||
logger.apiCall(req.user.id, '/api/well-visit/shadess', {
|
logger.apiCall(req.user!.id, '/api/well-visit/shadess', {
|
||||||
model: result.model, tokensInput: result.usage?.input_tokens,
|
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 });
|
res.json({ success: true, assessment: result.content, model: result.model });
|
||||||
logger.audit(req.user.id, 'generate_shadess', 'Generated SSHADESS assessment', req, { category: 'clinical' });
|
logger.audit(req.user!.id, 'generate_shadess', 'Generated SSHADESS assessment', req, { category: 'clinical' });
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
logger.error('[WellVisit] SHADESS generation failed', e.message);
|
logger.error('[WellVisit] SHADESS generation failed', e.message);
|
||||||
res.status(500).json({ error: 'Request failed' });
|
res.status(500).json({ error: 'Request failed' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── POST generate full well visit encounter note ─────────────────────────
|
// ── POST generate full well visit encounter note ─────────────────────────
|
||||||
router.post('/well-visit/note', authMiddleware, async function(req, res) {
|
router.post('/well-visit/note', authMiddleware, async function (req: Request, res: Response) {
|
||||||
var start = Date.now();
|
const start = Date.now();
|
||||||
try {
|
try {
|
||||||
var {
|
const {
|
||||||
patientAge, patientGender, visitAge,
|
patientAge, patientGender, visitAge,
|
||||||
vitals, measurements,
|
vitals, measurements,
|
||||||
transcript, dictation,
|
transcript, dictation,
|
||||||
|
|
@ -144,36 +143,34 @@ router.post('/well-visit/note', authMiddleware, async function(req, res) {
|
||||||
parentConcerns,
|
parentConcerns,
|
||||||
ros, physicalExam, diagnoses,
|
ros, physicalExam, diagnoses,
|
||||||
physicianMemories,
|
physicianMemories,
|
||||||
noteStyle, // 'full' | 'short'
|
noteStyle,
|
||||||
model
|
model,
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
if (!patientAge && !visitAge) {
|
if (!patientAge && !visitAge) {
|
||||||
return res.status(400).json({ error: 'Patient age or visit age required' });
|
return res.status(400).json({ error: 'Patient age or visit age required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assemble context
|
let context = 'WELL CHILD VISIT\n';
|
||||||
var context = 'WELL CHILD VISIT\n';
|
|
||||||
context += 'Patient: ' + (patientAge || visitAge) + ', ' + (patientGender || 'Unknown') + '\n\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 (measurements) context += 'MEASUREMENTS/GROWTH:\n' + wrapUserText('measurements', measurements) + '\n\n';
|
||||||
if (parentConcerns) context += 'PARENT/PATIENT CONCERNS:\n' + wrapUserText('concerns', parentConcerns) + '\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';
|
else if (dictation) context += 'PHYSICIAN DICTATION:\n' + wrapUserText('dictation', dictation) + '\n\n';
|
||||||
if (screenings) context += 'SCREENINGS COMPLETED:\n' + wrapUserText('screenings', screenings) + '\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 (vaccines) context += 'IMMUNIZATIONS TODAY:\n' + wrapUserText('vaccines', vaccines) + '\n\n';
|
||||||
if (shadessAssessment) context += 'SSHADESS PSYCHOSOCIAL ASSESSMENT:\n' + wrapUserText('sshadess', shadessAssessment) + '\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 (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) {
|
if (physicianMemories) {
|
||||||
context += '[STYLE HINTS (low priority — only apply if relevant to the current note, never copy content from these examples)]\n'
|
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';
|
+ wrapUserText('style_hints', physicianMemories) + '\n[END STYLE HINTS]\n\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add growth reference and feeding guidance for this age
|
const growthRef = getGrowthRefForAge(patientAge || visitAge);
|
||||||
var growthRef = getGrowthRefForAge(patientAge || visitAge);
|
|
||||||
if (growthRef) {
|
if (growthRef) {
|
||||||
context += 'GROWTH/NUTRITION GUIDANCE (AAP reference for this age):\n';
|
context += 'GROWTH/NUTRITION GUIDANCE (AAP reference for this age):\n';
|
||||||
if (growthRef.weight) context += 'Expected weight gain: ' + growthRef.weight + '\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.headCirc) context += 'Expected head circumference gain: ' + growthRef.headCirc + '\n';
|
||||||
if (growthRef.feeding && growthRef.feeding.length) {
|
if (growthRef.feeding && growthRef.feeding.length) {
|
||||||
context += 'Feeding guidance:\n';
|
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) {
|
if (growthRef.bmiClassification && BMI_CLASSIFICATION.categories) {
|
||||||
context += 'BMI Classification (AAP 2023 / CDC Extended BMI):\n';
|
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 += ' - ' + c.label + ': ' + c.range + ' → ' + c.action + '\n';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
context += '\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: 'system', content: prompt + INJECTION_GUARD },
|
||||||
{ role: 'user', content: context }
|
{ role: 'user', content: context },
|
||||||
], { model });
|
], { model });
|
||||||
|
|
||||||
var dur = Date.now() - start;
|
const dur = Date.now() - start;
|
||||||
logger.apiCall(req.user.id, '/api/well-visit/note', {
|
logger.apiCall(req.user!.id, '/api/well-visit/note', {
|
||||||
model: result.model, tokensInput: result.usage?.input_tokens,
|
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 });
|
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' });
|
logger.audit(req.user!.id, 'generate_well_visit', 'Generated well visit note', req, { category: 'clinical' });
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
logger.error('[WellVisit] Note generation failed', e.message);
|
logger.error('[WellVisit] Note generation failed', e.message);
|
||||||
res.status(500).json({ error: 'Request failed' });
|
res.status(500).json({ error: 'Request failed' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = router;
|
export = router;
|
||||||
Loading…
Reference in a new issue