function positiveInt(value, fallback) { var n = Number(value); return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback; } function clip(text, max) { text = String(text == null ? '' : text); return text.length > max ? text.substring(0, max - 1).trimEnd() + '…' : text; } var PEDIATRIC_TAXONOMY = [ { category: 'neonates', weight: 85, ageBands: ['neonate'], seeds: [ 'febrile neonate sepsis evaluation empiric antibiotics lumbar puncture', 'neonatal jaundice bilirubin phototherapy cholestasis red flags', 'newborn respiratory distress differential management NICU admission', 'neonate poor feeding vomiting bilious emesis congenital obstruction', 'neonatal hypoglycemia seizures lethargy initial management' ] }, { category: 'respiratory', weight: 85, ageBands: ['infant', 'toddler', 'school_age', 'adolescent'], seeds: [ 'pediatric asthma exacerbation severity treatment discharge criteria', 'bronchiolitis infant oxygen hydration admission criteria', 'croup stridor epiglottitis bacterial tracheitis airway red flags', 'pediatric pneumonia wheezing foreign body aspiration differential', 'cystic fibrosis respiratory exacerbation airway clearance antibiotics' ] }, { category: 'gi', weight: 75, ageBands: ['infant', 'toddler', 'school_age', 'adolescent'], seeds: [ 'pediatric abdominal pain appendicitis intussusception differential', 'acute gastroenteritis dehydration oral rehydration ondansetron', 'bloody diarrhea hemolytic uremic syndrome STEC salmonella', 'pediatric vomiting bilious emesis increased intracranial pressure', 'constipation encopresis functional abdominal pain counseling' ] }, { category: 'infectious_disease', weight: 75, ageBands: ['neonate', 'infant', 'toddler', 'school_age', 'adolescent'], seeds: [ 'pediatric fever without source serious bacterial infection workup', 'meningitis encephalitis sepsis child empiric antibiotics', 'pertussis exposure prophylaxis vaccination household contacts', 'skin soft tissue infection cellulitis abscess child antibiotics', 'fever of unknown origin child infectious noninfectious differential' ] }, { category: 'emergency', weight: 70, ageBands: ['neonate', 'infant', 'toddler', 'school_age', 'adolescent'], seeds: [ 'pediatric emergency triage red flags resuscitation shock', 'child altered mental status seizure hypoglycemia toxic ingestion', 'pediatric anaphylaxis epinephrine airway hypotension disposition', 'trauma child head injury abdominal injury non accidental trauma', 'pediatric shock dehydration sepsis cardiac emergency management' ] }, { category: 'derm', weight: 55, ageBands: ['neonate', 'infant', 'toddler', 'school_age', 'adolescent'], seeds: [ 'pediatric rash fever petechiae purpura emergency differential', 'newborn skin lesions hemangioma melanocytic nevus epidermolysis bullosa', 'eczema atopic dermatitis impetigo cellulitis child management', 'urticaria angioedema anaphylaxis pediatric counseling', 'Kawasaki disease mucocutaneous findings MIS-C differential' ] }, { category: 'endocrine', weight: 55, ageBands: ['infant', 'toddler', 'school_age', 'adolescent'], seeds: [ 'diabetic ketoacidosis child fluids insulin cerebral edema', 'pediatric hypoglycemia adrenal insufficiency endocrine emergency', 'short stature delayed puberty precocious puberty evaluation', 'thyroid disease child hyperthyroidism hypothyroidism symptoms', 'polyuria polydipsia diabetes insipidus diabetes mellitus child' ] }, { category: 'cardiology', weight: 55, ageBands: ['neonate', 'infant', 'toddler', 'school_age', 'adolescent'], seeds: [ 'pediatric chest pain syncope murmur red flags cardiology referral', 'cyanotic congenital heart disease neonate prostaglandin ductal dependent', 'heart failure infant poor feeding tachypnea hepatomegaly', 'arrhythmia palpitations supraventricular tachycardia child management', 'Kawasaki disease coronary aneurysm aspirin IVIG' ] }, { category: 'neurology', weight: 65, ageBands: ['infant', 'toddler', 'school_age', 'adolescent'], seeds: [ 'pediatric headache red flags neuroimaging referral', 'first seizure child febrile seizure epilepsy workup', 'altered mental status child meningitis encephalitis toxic metabolic', 'developmental regression weakness ataxia neurologic emergency', 'migraine child acute treatment prevention counseling' ] }, { category: 'renal', weight: 50, ageBands: ['infant', 'toddler', 'school_age', 'adolescent'], seeds: [ 'urinary tract infection child fever pyelonephritis imaging', 'hematuria proteinuria nephrotic nephritic syndrome child', 'acute kidney injury child dehydration sepsis nephrotoxin workup', 'hypertension pediatric evaluation renal endocrine cardiac', 'electrolyte disorders child sodium potassium emergency management' ] }, { category: 'rheum', weight: 45, ageBands: ['toddler', 'school_age', 'adolescent'], seeds: [ 'juvenile idiopathic arthritis limp joint swelling fever differential', 'Kawasaki MIS-C fever rash conjunctivitis pediatric differential', 'pediatric lupus vasculitis purpura renal symptoms workup', 'periodic fever syndromes child aphthous pharyngitis adenitis', 'limp child septic arthritis osteomyelitis transient synovitis' ] }, { category: 'hematology', weight: 55, ageBands: ['infant', 'toddler', 'school_age', 'adolescent'], seeds: [ 'pediatric anemia microcytic hemolytic iron deficiency workup', 'sickle cell fever pain crisis acute chest child management', 'thrombocytopenia petechiae purpura ITP leukemia child differential', 'bleeding bruising child coagulation disorder non accidental trauma', 'neutropenia fever oncology pediatric emergency antibiotics' ] }, { category: 'development', weight: 60, ageBands: ['infant', 'toddler', 'school_age', 'adolescent'], seeds: [ 'developmental milestones delay screening autism speech motor', 'ADHD learning difficulty school performance pediatric assessment', 'autism spectrum toddler screening counseling referral', 'developmental regression child neurologic metabolic genetic evaluation', 'well child developmental behavioral screening anticipatory guidance' ] }, { category: 'toxicology', weight: 45, ageBands: ['toddler', 'school_age', 'adolescent'], seeds: [ 'pediatric ingestion acetaminophen iron button battery emergency', 'adolescent overdose suicide risk toxidrome initial management', 'carbon monoxide poisoning child headache altered mental status', 'caustic ingestion child drooling dysphagia endoscopy', 'medication poisoning toddler decontamination antidote observation' ] }, { category: 'procedures', weight: 45, ageBands: ['infant', 'toddler', 'school_age', 'adolescent'], seeds: [ 'pediatric lumbar puncture indications contraindications consent', 'laceration repair child sedation analgesia wound infection', 'foreign body removal ear nose airway child management', 'splinting fracture child neurovascular assessment analgesia', 'procedural sedation pediatric fasting monitoring discharge criteria' ] }, { category: 'dosing_fluids', weight: 70, ageBands: ['neonate', 'infant', 'toddler', 'school_age', 'adolescent'], seeds: [ 'pediatric maintenance fluids dehydration bolus electrolyte correction', 'weight based medication dosing child safety maximum dose', 'oral rehydration solution dosing gastroenteritis child', 'DKA fluids insulin potassium pediatric cerebral edema', 'antibiotic dosing renal adjustment pediatric infection' ] }, { category: 'adolescent', weight: 60, ageBands: ['adolescent'], seeds: [ 'adolescent abdominal pain pregnancy STI pelvic inflammatory disease', 'depression suicide screening adolescent confidentiality safety plan', 'eating disorder adolescent bradycardia weight loss admission criteria', 'substance use adolescent confidential history counseling', 'sports injury concussion return to play adolescent' ] }, { category: 'counseling', weight: 55, ageBands: ['neonate', 'infant', 'toddler', 'school_age', 'adolescent'], seeds: [ 'parent counseling fever return precautions child safety net', 'vaccine counseling pediatric hesitancy contraindications', 'asthma action plan inhaler technique parent education', 'gastroenteritis home care hydration return precautions', 'newborn discharge anticipatory guidance feeding jaundice safe sleep' ] } ]; var VALID_INTENTS = { diagnosis: true, management: true, red_flags: true, differential: true, counseling: true, dosing: true, admission: true, discharge: true, review: true }; var VALID_AGE_BANDS = { neonate: true, infant: true, toddler: true, school_age: true, adolescent: true }; function taxonomyByCategory() { var out = {}; PEDIATRIC_TAXONOMY.forEach(function(item) { out[item.category] = item; }); return out; } var TAXONOMY_BY_CATEGORY = taxonomyByCategory(); var CATEGORY_ALIASES = { 'infectious disease': 'infectious_disease', infectious: 'infectious_disease', 'dosing/fluids': 'dosing_fluids', dosing: 'dosing_fluids', fluids: 'dosing_fluids', 'red flags': 'emergency', 'admission/discharge': 'emergency', general: 'counseling' }; function parseJsonObject(text) { text = String(text || '').trim().replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/i, ''); try { return JSON.parse(text); } catch (e) {} var start = text.indexOf('{'); var end = text.lastIndexOf('}'); if (start !== -1 && end > start) { try { return JSON.parse(text.slice(start, end + 1)); } catch (e2) {} } return {}; } function createClinicalPromptPool(opts) { opts = opts || {}; var cacheMs = positiveInt(process.env.CLINICAL_ASSISTANT_EXAMPLE_CACHE_MS, 10 * 60 * 1000); var refreshMs = nonNegativeInt(process.env.CLINICAL_ASSISTANT_PROMPT_POOL_REFRESH_MS, 7 * 24 * 60 * 60 * 1000); var ttlSeconds = refreshMs > 0 ? Math.max(3600, Math.ceil(refreshMs / 1000) * 2) : 0; var target = positiveInt(process.env.CLINICAL_ASSISTANT_PROMPT_POOL_TARGET, 1000); var redisBaseKey = String(process.env.CLINICAL_ASSISTANT_PROMPT_POOL_KEY || 'clinical-assistant:prompt-pool:v2').replace(/:all$/, ''); var redisAllKey = redisBaseKey + ':all'; var redisMetaKey = redisBaseKey + ':meta'; var redisLastGoodKey = redisBaseKey + ':last-good'; var memoryCache = { expiresAt: 0, examples: [] }; var refreshPromise = null; async function getAvailableExamples() { var now = Date.now(); if (memoryCache.expiresAt > now && memoryCache.examples.length) return memoryCache.examples; var cached = await readPromptPool(); if (cached && Array.isArray(cached.examples) && cached.examples.length) { memoryCache = { expiresAt: now + cacheMs, examples: cached.examples }; if (refreshMs > 0 && (!cached.generatedAt || now - cached.generatedAt > refreshMs)) refreshIfNeeded(false).catch(function() {}); return cached.examples; } refreshIfNeeded(false).catch(function() {}); var examples = await fallbackIndexedExamples(); memoryCache = { expiresAt: now + cacheMs, examples: examples }; return examples; } async function refreshIfNeeded(force, context) { context = context || {}; var now = Date.now(); var cached = await readPromptPool(); if (!force && cached && Array.isArray(cached.examples) && cached.examples.length && (refreshMs === 0 || (cached.generatedAt && now - cached.generatedAt < refreshMs))) { return cached.examples; } if (refreshPromise) return refreshPromise; refreshPromise = buildCorpusPromptPool().then(function(examples) { refreshPromise = null; if (!examples.length) return []; var payload = { generatedAt: Date.now(), target: target, examples: examples }; memoryCache = { expiresAt: Date.now() + cacheMs, examples: examples }; return writePromptPool(payload, context).then(function() { return examples; }); }).catch(function(e) { refreshPromise = null; console.warn('[clinical-assistant] prompt pool refresh failed:', e.message); return cached && Array.isArray(cached.examples) ? cached.examples : []; }); return refreshPromise; } async function readPromptPool() { var cached = await opts.redisCache.getJson(redisAllKey).catch(function() { return null; }); if (cached && Array.isArray(cached.examples) && cached.examples.length) return cached; var lastGood = await opts.redisCache.getJson(redisLastGoodKey).catch(function() { return null; }); if (lastGood && Array.isArray(lastGood.examples) && lastGood.examples.length) return lastGood; if (typeof opts.loadStoredPromptPool === 'function') { var stored = await opts.loadStoredPromptPool().catch(function() { return null; }); if (stored && Array.isArray(stored.examples) && stored.examples.length) { await writePromptPool(stored, { skipStore: true }).catch(function() {}); return stored; } } return null; } async function buildCorpusPromptPool() { var taxonomy = taxonomyWithQuotas(target); var chatModel = await opts.getSetting('clinical_assistant.prompt_model', '') || process.env.CLINICAL_ASSISTANT_PROMPT_MODEL || await opts.getSetting('clinical_assistant.chat_model', '') || await opts.getSetting('models.default', ''); var generated = []; for (var t = 0; t < taxonomy.length && generated.length < target; t++) { var item = taxonomy[t]; var snippets = await collectPromptSeedSnippets(item); if (!snippets.length) continue; var categoryExamples = []; var batches = Math.max(1, Math.min(5, Math.ceil(item.quota / 25))); for (var i = 0; i < batches && categoryExamples.length < item.quota; i++) { var batchSnippets = rotateExamples(snippets, i * 18).slice(0, 18); var sourceText = batchSnippets.map(function(s, idx) { return '[' + (idx + 1) + '] ' + s.title + (s.page ? ', page ' + s.page : '') + '\n' + clip(s.excerpt, 700); }).join('\n\n'); var ai = await opts.callAI([ { role: 'system', content: 'Generate realistic pediatric clinical assistant starter questions from indexed source snippets. Use only the provided titles/snippets as inspiration. Do not answer the questions. Do not mention source names. Every question must be about children, neonates, infants, adolescents, pediatric dosing, pediatric diagnosis, pediatric management, pediatric red flags, pediatric counseling, or pediatric disposition. Return strict JSON only: {"questions":[{"label":"2-5 word label","prompt":"question ending with ?","category":"' + item.category + '","age_band":"' + item.ageBands.join('|') + '","intent":"diagnosis|management|red_flags|differential|counseling|dosing|admission|discharge|review"}]}.' }, { role: 'user', content: 'Category: ' + item.category + '\nAge bands: ' + item.ageBands.join(', ') + '\nIndexed pediatric source snippets:\n\n' + sourceText + '\n\nCreate 30 varied starter questions for this category. Balance emergencies, outpatient decisions, diagnostic workup, red flags, treatment, fluids, dosing, admission/discharge, and family counseling when relevant.' } ], { model: chatModel || undefined, temperature: 0.78, maxTokens: 2600 }); var parsed = parseJsonObject(String(ai.content || '')); categoryExamples = normalizeGeneratedExamples(categoryExamples.concat(parsed.questions || []), item).slice(0, item.quota); } generated = normalizeGeneratedExamples(generated.concat(categoryExamples)); } if (generated.length < 8) return []; return generated.slice(0, target); } async function collectPromptSeedSnippets(taxonomyItem) { var seeds = promptSeedQueries(taxonomyItem); var seen = new Set(); var snippets = []; for (var i = 0; i < seeds.length && snippets.length < 36; i++) { try { var searchResponse = await opts.semanticSearch(seeds[i], { limit: 5, includeContext: true, contextChars: 900 }); opts.dedupeSources(opts.normalizeMcpSearchResponse(searchResponse)).forEach(function(source) { var key = [source.title, source.page, source.chunk_index].join('|'); if (seen.has(key) || snippets.length >= 36) return; seen.add(key); snippets.push(source); }); } catch (e) { continue; } } return snippets; } function promptSeedQueries(taxonomyItem) { var seeds = (opts.exampleCandidates || []).map(function(item) { return item.prompt; }); Object.keys(opts.topicSuggestions || {}).forEach(function(key) { seeds = seeds.concat(opts.topicSuggestions[key]); }); seeds = seeds.concat(taxonomyItem.seeds).concat([ taxonomyItem.category + ' pediatric ' + taxonomyItem.seeds.join(' '), 'pediatric emergency red flags admission criteria treatment dosing', 'neonatal fever jaundice respiratory distress vomiting dehydration', 'childhood infectious diseases empiric antibiotics workup disposition', 'pediatric respiratory illness asthma bronchiolitis pneumonia croup', 'pediatric gastrointestinal dehydration abdominal pain bilious vomiting', 'developmental milestones anemia endocrine neurologic pediatric review' ]); return shuffle(seeds).slice(0, 8); } function fallbackIndexedExamples() { return typeof opts.getIndexedTopicExamples === 'function' ? opts.getIndexedTopicExamples().catch(function() { return []; }) : []; } async function getMeta() { return await opts.redisCache.getJson(redisMetaKey).catch(function() { return null; }); } function normalizeGeneratedExamples(items, defaultTaxonomy) { var seen = new Set(); var out = []; (Array.isArray(items) ? items : []).forEach(function(item) { var prompt = clip(String(item.prompt || item.question || '').replace(/[\r\n\t]+/g, ' ').replace(/\s+/g, ' ').trim(), 220); if (!isUsefulQuestion(prompt, item)) return; var key = prompt.toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim(); if (seen.has(key)) return; seen.add(key); var category = sanitizeCategory(item.category, defaultTaxonomy); var ageBand = sanitizeAgeBand(item.age_band || item.ageBand || item.age, TAXONOMY_BY_CATEGORY[category] || defaultTaxonomy, prompt); out.push({ label: cleanExampleLabel(item.label || prompt), prompt: prompt, category: category, age_band: ageBand, intent: sanitizeIntent(item.intent) }); }); return out.slice(0, target); } async function writePromptPool(payload, context) { context = context || {}; var counts = buildPoolCounts(payload.examples); var writes = [ opts.redisCache.setJson(redisAllKey, payload, ttlSeconds), opts.redisCache.setJson(redisLastGoodKey, payload, 0), opts.redisCache.setJson(redisMetaKey, { generatedAt: payload.generatedAt, target: target, count: payload.examples.length, categories: counts.categories, age_bands: counts.age_bands, intents: counts.intents }, ttlSeconds) ]; Object.keys(counts.categoryExamples).forEach(function(category) { writes.push(opts.redisCache.setJson(redisBaseKey + ':category:' + category, { generatedAt: payload.generatedAt, category: category, examples: counts.categoryExamples[category] }, ttlSeconds)); }); Object.keys(counts.ageExamples).forEach(function(ageBand) { writes.push(opts.redisCache.setJson(redisBaseKey + ':age:' + ageBand, { generatedAt: payload.generatedAt, age_band: ageBand, examples: counts.ageExamples[ageBand] }, ttlSeconds)); }); Object.keys(counts.intentExamples).forEach(function(intent) { writes.push(opts.redisCache.setJson(redisBaseKey + ':intent:' + intent, { generatedAt: payload.generatedAt, intent: intent, examples: counts.intentExamples[intent] }, ttlSeconds)); }); await Promise.all(writes); if (!context.skipStore && typeof opts.savePromptPool === 'function') { await opts.savePromptPool(payload, context).catch(function(e) { console.warn('[clinical-assistant] prompt pool snapshot save failed:', e.message); }); } } function buildPoolCounts(examples) { var counts = { categories: {}, age_bands: {}, intents: {}, categoryExamples: {}, ageExamples: {}, intentExamples: {} }; examples.forEach(function(example) { addCount(counts.categories, example.category); addCount(counts.age_bands, example.age_band); addCount(counts.intents, example.intent); pushGroup(counts.categoryExamples, example.category, example); pushGroup(counts.ageExamples, example.age_band, example); pushGroup(counts.intentExamples, example.intent, example); }); return counts; } function addCount(counts, key) { counts[key] = (counts[key] || 0) + 1; } function pushGroup(groups, key, example) { if (!groups[key]) groups[key] = []; groups[key].push(example); } function taxonomyWithQuotas(total) { var weightTotal = PEDIATRIC_TAXONOMY.reduce(function(sum, item) { return sum + item.weight; }, 0); var assigned = 0; var rows = PEDIATRIC_TAXONOMY.map(function(item) { var exact = total * item.weight / weightTotal; var quota = Math.max(10, Math.floor(exact)); assigned += quota; return Object.assign({}, item, { quota: quota, remainder: exact - Math.floor(exact) }); }).sort(function(a, b) { return b.remainder - a.remainder; }); for (var i = 0; assigned < total; i = (i + 1) % rows.length) { rows[i].quota += 1; assigned += 1; } for (var j = rows.length - 1; assigned > total && j >= 0; j--) { if (rows[j].quota <= 10) continue; rows[j].quota -= 1; assigned -= 1; } return rows.sort(function(a, b) { return PEDIATRIC_TAXONOMY.indexOf(TAXONOMY_BY_CATEGORY[a.category]) - PEDIATRIC_TAXONOMY.indexOf(TAXONOMY_BY_CATEGORY[b.category]); }); } function sanitizeCategory(category, defaultTaxonomy) { category = String(category || '').trim().toLowerCase().replace(/[\s/-]+/g, '_'); category = CATEGORY_ALIASES[category] || CATEGORY_ALIASES[category.replace(/_/g, ' ')] || category; if (TAXONOMY_BY_CATEGORY[category]) return category; return defaultTaxonomy && defaultTaxonomy.category || 'counseling'; } function sanitizeIntent(intent) { intent = String(intent || 'review').trim().toLowerCase().replace(/[\s/-]+/g, '_'); return VALID_INTENTS[intent] ? intent : 'review'; } function sanitizeAgeBand(ageBand, taxonomyItem, prompt) { ageBand = String(ageBand || '').trim().toLowerCase().replace(/[\s/-]+/g, '_'); if (VALID_AGE_BANDS[ageBand]) return ageBand; if (/\b(neonate|newborn)\b/i.test(prompt)) return 'neonate'; if (/\b(infant|baby)\b/i.test(prompt)) return 'infant'; if (/\b(toddler)\b/i.test(prompt)) return 'toddler'; if (/\b(adolescent|teen)\b/i.test(prompt)) return 'adolescent'; return taxonomyItem && taxonomyItem.ageBands && taxonomyItem.ageBands[0] || 'school_age'; } function isUsefulQuestion(prompt, item) { if (!prompt || prompt.length < 25 || prompt.length > 220) return false; if (prompt.indexOf('?') === -1) return false; if (!hasPediatricSignal(prompt, item)) return false; return !/\b(source|snippet|textbook|chapter|document|database)\b/i.test(prompt); } function hasPediatricSignal(prompt, item) { if (/\b(child|children|pediatric|paediatric|infant|neonate|newborn|adolescent|teen|toddler|baby)\b/i.test(prompt)) return true; var category = sanitizeCategory(item && item.category, null); var intent = sanitizeIntent(item && item.intent); var ageBand = String(item && (item.age_band || item.ageBand || item.age) || '').toLowerCase().replace(/[\s/-]+/g, '_'); return !!TAXONOMY_BY_CATEGORY[category] && !!VALID_INTENTS[intent] && (!ageBand || !!VALID_AGE_BANDS[ageBand]); } function cleanExampleLabel(label) { label = String(label || '').replace(/[?!.:,;]+$/g, '').replace(/\s+/g, ' ').trim(); if (!label || label.length > 42) label = labelFromQuestion(label || 'Clinical question'); return clip(label, 42); } function labelFromQuestion(question) { return String(question || 'Clinical question') .replace(/^(how|what|when|which|why)\s+(should|do|does|can|are|is)\s+/i, '') .replace(/\?.*$/, '') .split(/\s+/) .slice(0, 4) .join(' ') || 'Clinical question'; } function rotateExamples(items, seed) { if (!items.length) return []; var copy = items.slice(); var offset = Math.abs(Math.floor(seed)) % copy.length; return copy.slice(offset).concat(copy.slice(0, offset)); } function shuffle(items) { var copy = items.slice(); for (var i = copy.length - 1; i > 0; i--) { var j = Math.floor(Math.random() * (i + 1)); var tmp = copy[i]; copy[i] = copy[j]; copy[j] = tmp; } return copy; } return { getAvailableExamples: getAvailableExamples, refreshIfNeeded: refreshIfNeeded, getMeta: getMeta, writePromptPool: writePromptPool }; } function nonNegativeInt(value, fallback) { if (value == null || value === '') return fallback; var n = Number(value); return Number.isFinite(n) && n >= 0 ? Math.floor(n) : fallback; } module.exports = { createClinicalPromptPool };