Expand clinical prompt pool taxonomy
Some checks are pending
Forgejo Android APK / Build signed APK (push) Waiting to run

This commit is contained in:
Daniel 2026-05-22 05:56:08 +02:00
parent 2cef65fb1f
commit cb63729656
3 changed files with 462 additions and 49 deletions

View file

@ -15,7 +15,7 @@ services:
LITELLM_TTS_MODEL: local-kokoro-tts
LITELLM_TTS_VOICE: sherpa/kokoro:am_adam
LITELLM_TTS_VOICES: sherpa/kokoro:am_adam,sherpa/kokoro:am_michael,sherpa/kokoro:af_bella,sherpa/kokoro:af_nicole,sherpa/kokoro:bf_emma,sherpa/kokoro:bm_lewis
CLINICAL_ASSISTANT_PROMPT_POOL_TARGET: 200
CLINICAL_ASSISTANT_PROMPT_POOL_TARGET: 1000
volumes:
- scribe-logs:/app/data/logs
- clinical-assistant-mcp-data:/app/mcp-data:ro

View file

@ -8,6 +8,227 @@ function clip(text, max) {
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) {}
@ -24,15 +245,17 @@ function createClinicalPromptPool(opts) {
var cacheMs = positiveInt(process.env.CLINICAL_ASSISTANT_EXAMPLE_CACHE_MS, 10 * 60 * 1000);
var refreshMs = positiveInt(process.env.CLINICAL_ASSISTANT_PROMPT_POOL_REFRESH_MS, 24 * 60 * 60 * 1000);
var ttlSeconds = Math.max(3600, Math.ceil(refreshMs / 1000) * 2);
var target = positiveInt(process.env.CLINICAL_ASSISTANT_PROMPT_POOL_TARGET, 200);
var redisKey = process.env.CLINICAL_ASSISTANT_PROMPT_POOL_KEY || 'clinical-assistant:prompt-pool:v1';
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 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 opts.redisCache.getJson(redisKey).catch(function() { return null; });
var cached = await opts.redisCache.getJson(redisAllKey).catch(function() { return null; });
if (cached && Array.isArray(cached.examples) && cached.examples.length) {
memoryCache = { expiresAt: now + cacheMs, examples: cached.examples };
if (!cached.generatedAt || now - cached.generatedAt > refreshMs) refreshIfNeeded(false).catch(function() {});
@ -49,7 +272,7 @@ function createClinicalPromptPool(opts) {
async function refreshIfNeeded(force) {
var now = Date.now();
var cached = await opts.redisCache.getJson(redisKey).catch(function() { return null; });
var cached = await opts.redisCache.getJson(redisAllKey).catch(function() { return null; });
if (!force && cached && Array.isArray(cached.examples) && cached.examples.length && cached.generatedAt && now - cached.generatedAt < refreshMs) {
return cached.examples;
}
@ -57,9 +280,9 @@ function createClinicalPromptPool(opts) {
refreshPromise = buildCorpusPromptPool().then(function(examples) {
refreshPromise = null;
if (!examples.length) return [];
var payload = { generatedAt: Date.now(), examples: examples };
var payload = { generatedAt: Date.now(), target: target, examples: examples };
memoryCache = { expiresAt: Date.now() + cacheMs, examples: examples };
return opts.redisCache.setJson(redisKey, payload, ttlSeconds).then(function() { return examples; });
return writePromptPool(payload).then(function() { return examples; });
}).catch(function(e) {
refreshPromise = null;
console.warn('[clinical-assistant] prompt pool refresh failed:', e.message);
@ -69,60 +292,71 @@ function createClinicalPromptPool(opts) {
}
async function buildCorpusPromptPool() {
var snippets = await collectPromptSeedSnippets();
if (!snippets.length) return fallbackIndexedExamples();
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 = [];
var batches = Math.max(1, Math.min(40, Math.ceil(target / 25)));
for (var i = 0; i < batches && generated.length < target; i++) {
var batchSnippets = rotateExamples(snippets, i * 24).slice(0, 24);
var sourceText = batchSnippets.map(function(s, idx) {
return '[' + (idx + 1) + '] ' + s.title + (s.page ? ', page ' + s.page : '') + '\n' + clip(s.excerpt, 650);
}).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. Prefer broad practical clinical questions involving diagnosis, red flags, severity, treatment, dosing, fluids, admission/discharge, counseling, or differential diagnosis. Return strict JSON only: {"questions":[{"label":"2-5 word label","prompt":"question","category":"emergency|infectious disease|neonates|respiratory|gi|neurology|cardiology|endocrine|hematology|development|toxicology|procedures|dosing/fluids|red flags|differential|admission/discharge|counseling|general","intent":"diagnosis|management|red_flags|differential|counseling|dosing|admission|review"}]}.'
},
{ role: 'user', content: 'Indexed pediatric source snippets:\n\n' + sourceText + '\n\nCreate 35 varied starter questions. Make this batch different from common asthma, croup, and UTI examples when possible. Include a mix of emergencies, outpatient questions, red flags, diagnostics, treatment, counseling, fluids, dosing, admission/discharge, and differentials.' }
], {
model: chatModel || undefined,
temperature: 0.75,
maxTokens: 2600
});
var parsed = parseJsonObject(String(ai.content || ''));
generated = normalizeGeneratedExamples(generated.concat(parsed.questions || []));
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));
}
generated = normalizeGeneratedExamples(generated.concat(taxonomyFallbackExamples(taxonomy, generated.length)));
if (generated.length < target) {
generated = normalizeGeneratedExamples(generated.concat(taxonomySupplementExamples(taxonomy, target - generated.length)));
}
if (generated.length < 8) return fallbackIndexedExamples();
return generated;
return generated.slice(0, target);
}
async function collectPromptSeedSnippets() {
var seeds = promptSeedQueries();
async function collectPromptSeedSnippets(taxonomyItem) {
var seeds = promptSeedQueries(taxonomyItem);
var seen = new Set();
var snippets = [];
for (var i = 0; i < seeds.length && snippets.length < 120; i++) {
for (var i = 0; i < seeds.length && snippets.length < 36; i++) {
try {
var searchResponse = await opts.semanticSearch(seeds[i], { limit: 5, includeContext: true, contextChars: 700 });
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 >= 120) return;
if (seen.has(key) || snippets.length >= 36) return;
seen.add(key);
snippets.push(source);
});
} catch (e) {
if (snippets.length === 0) throw e;
continue;
}
}
return snippets;
}
function promptSeedQueries() {
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([
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',
@ -130,14 +364,14 @@ function createClinicalPromptPool(opts) {
'pediatric gastrointestinal dehydration abdominal pain bilious vomiting',
'developmental milestones anemia endocrine neurologic pediatric review'
]);
return shuffle(seeds).slice(0, 30);
return shuffle(seeds).slice(0, 8);
}
function fallbackIndexedExamples() {
return typeof opts.getIndexedTopicExamples === 'function' ? opts.getIndexedTopicExamples().catch(function() { return []; }) : [];
}
function normalizeGeneratedExamples(items) {
function normalizeGeneratedExamples(items, defaultTaxonomy) {
var seen = new Set();
var out = [];
(Array.isArray(items) ? items : []).forEach(function(item) {
@ -146,16 +380,190 @@ function createClinicalPromptPool(opts) {
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: clip(String(item.category || 'general'), 40),
intent: clip(String(item.intent || 'review'), 30)
category: category,
age_band: ageBand,
intent: sanitizeIntent(item.intent)
});
});
return out.slice(0, target);
}
async function writePromptPool(payload) {
var counts = buildPoolCounts(payload.examples);
var writes = [
opts.redisCache.setJson(redisAllKey, payload, ttlSeconds),
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);
}
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 taxonomyFallbackExamples(taxonomy, existingCount) {
var templates = [
'How should I evaluate a {age} with {seed}, focusing on {focus}?',
'What red flags change disposition for a {age} with {seed} and {focus}?',
'What pediatric differential fits {seed} in a {age} with {focus}?',
'What initial management is recommended for a {age} with {seed} and {focus}?',
'When should a {age} with {seed} be admitted because of {focus}?',
'What return precautions fit {seed} in a {age} with {focus}?'
];
var focusAreas = [
'severity assessment',
'diagnostic workup',
'age-specific risks',
'medication dosing',
'fluid management',
'family safety netting',
'follow-up timing',
'comorbid conditions',
'initial stabilization',
'consultation thresholds'
];
var examples = [];
taxonomy.forEach(function(item) {
var needed = Math.max(0, item.quota - Math.floor(existingCount / taxonomy.length));
var itemExamples = [];
var seen = new Set();
for (var i = 0; i < needed * 3 && itemExamples.length < needed; i++) {
var seed = item.seeds[i % item.seeds.length];
var seedPhrase = seed.split(/\s+/).slice(0, 5).join(' ');
var ageBand = item.ageBands[i % item.ageBands.length];
var template = templates[i % templates.length];
var focus = focusAreas[Math.floor(i / (templates.length * item.seeds.length)) % focusAreas.length];
var example = {
label: cleanExampleLabel(seedPhrase.split(/\s+/).slice(0, 4).join(' ')),
prompt: template.replace('{age}', ageBand.replace('_', ' ') + ' patient').replace('{seed}', seedPhrase).replace('{focus}', focus),
category: item.category,
age_band: ageBand,
intent: ['diagnosis', 'red_flags', 'differential', 'management', 'admission', 'counseling'][i % 6]
};
var key = example.prompt.toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
if (seen.has(key)) continue;
seen.add(key);
itemExamples.push(example);
}
examples = examples.concat(itemExamples);
});
return examples;
}
function taxonomySupplementExamples(taxonomy, needed) {
var examples = [];
for (var i = 0; i < needed * 2 && examples.length < needed; i++) {
var item = taxonomy[i % taxonomy.length];
var seedPhrase = item.seeds[i % item.seeds.length].split(/\s+/).slice(0, 5).join(' ');
var ageBand = item.ageBands[i % item.ageBands.length];
var categoryLabel = item.category.replace(/_/g, ' ');
examples.push({
label: cleanExampleLabel(categoryLabel + ' review'),
prompt: 'For pediatric ' + categoryLabel + ' cases, how should I handle a ' + ageBand.replace('_', ' ') + ' patient with ' + seedPhrase + '?',
category: item.category,
age_band: ageBand,
intent: ['diagnosis', 'management', 'red_flags', 'differential', 'counseling', 'dosing', 'admission', 'review'][i % 8]
});
}
return examples;
}
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;
@ -165,9 +573,10 @@ function createClinicalPromptPool(opts) {
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 = String(item && item.category || '').toLowerCase();
var intent = String(item && item.intent || '').toLowerCase();
return /\b(neonates?|development|respiratory|infectious disease|gi|neurology|cardiology|endocrine|hematology|toxicology|procedures|dosing\/fluids|red flags|differential|admission\/discharge|counseling|emergency|general)\b/.test(category) && /\b(diagnosis|management|red_flags|differential|counseling|dosing|admission|review)\b/.test(intent);
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) {

View file

@ -14,11 +14,15 @@ test('clinical assistant starter prompts are Redis or indexed-source backed', ()
const pool = read('src/utils/clinicalPromptPool.js');
assert.doesNotMatch(route, /EXAMPLE_CANDIDATES|TOPIC_SUGGESTIONS|topic_suggestions|buildSuggestionAnswer/);
assert.match(route, /createClinicalPromptPool\(\{[\s\S]*redisCache: redisCache/);
assert.match(pool, /opts\.redisCache\.getJson\(redisKey\)/);
assert.match(pool, /opts\.redisCache\.setJson\(redisKey, payload, ttlSeconds\)/);
assert.match(pool, /CLINICAL_ASSISTANT_PROMPT_POOL_TARGET, 200/);
assert.match(pool, /Math\.min\(40, Math\.ceil\(target \/ 25\)\)/);
assert.match(pool, /rotateExamples\(snippets, i \* 24\)/);
assert.match(pool, /clinical-assistant:prompt-pool:v2/);
assert.match(pool, /redisBaseKey \+ ':all'/);
assert.match(pool, /redisBaseKey \+ ':category:' \+ category/);
assert.match(pool, /redisBaseKey \+ ':age:' \+ ageBand/);
assert.match(pool, /redisBaseKey \+ ':intent:' \+ intent/);
assert.match(pool, /CLINICAL_ASSISTANT_PROMPT_POOL_TARGET, 1000/);
assert.match(pool, /PEDIATRIC_TAXONOMY/);
assert.match(pool, /taxonomyWithQuotas\(target\)/);
assert.match(pool, /age_band: ageBand/);
assert.match(pool, /function hasPediatricSignal\(prompt, item\)/);
assert.match(pool, /var examples = await fallbackIndexedExamples\(\)/);
});