// The prompt is IDENTICAL whether or not sources are displayed. Branching it // would change how the model reasons and cites, so the same question could get a // different answer depending on a display setting — exactly the bias this must // avoid. Hiding sources is a presentation decision, applied after generation. function buildSystemPrompt(behavior) { return behavior + '\n\nIf the user asks for an image, call the generate_image tool with a self-contained prompt and briefly say you are preparing the image; the image appears automatically. Do not merely write an image prompt.\n\nRules:\n- Answer only the user question; do not dump unrelated textbook content.\n- For recognizable medical terms, abbreviations, diseases, and acronyms, answer directly without prefacing with "Assuming you meant".\n- For genuinely misspelled or partial terms, use the retrieved sources to infer the closest medical concept when there is a plausible match, then answer directly. Ask for clarification only when the retrieved sources do not indicate any plausible concept.\n- Use the exact source numbers from the retrieved sources; do not renumber citations for order or style.\n- Cite factual claims immediately with numbered citations like [1] or [1, 3]. Never escape citation brackets: write [1], not \\[1\\]; reserved LaTeX delimiters are not citations.\n- Every clinical recommendation, dose, threshold, lab value, statistic, comparison, contraindication, red flag, and table row must include its own supporting citation.\n- If a table has a Source, Source(s), Citation, or Citation(s) column, every cell in that column must use bracketed citation tokens like [1] or [1, 3], never bare numbers like 1 or 1, 3.\n- Do not leave a paragraph, bullet, or table row with multiple factual claims supported only by an uncited heading.\n- Do not cite a source number that is not provided.\n- If a claim is not directly supported by retrieved sources, omit it or say the available sources are insufficient.\n- If the user names a specific source, textbook, guideline, or table, do not claim that another source is from the named source. If the named source is absent from the retrieved sources, say that explicitly before using other sources.\n- If retrieved sources mention the medication/intervention only for other diseases, explicitly say the available sources do not support it for the user\'s requested disease.\n- If sources disagree or are insufficient, say so.\n- Keep the main answer concise and clinically useful.\n- Use clear markdown with headings, bullets, and tables when useful.\n- When using a table, output a valid GitHub-flavored markdown table with pipe characters and a separator row. Never output tab-separated tables.\n- Put any summary sentence in a separate paragraph after the table, not as a table row.\n- Do not add a final Sources or References section; the UI displays all retrieved sources separately.\n- Do not add generic disclaimers about clinician judgment.'; } // With the sources panel hidden the [n] markers point at nothing, so they are // removed from the copy that is DISPLAYED. The answer is generated, stored and // exported with its citations intact, so turning the setting back on restores // them without re-asking anything. function stripCitationMarkers(answer) { return String(answer || '') .replace(/(?:\s*\[(?:\d+\s*,\s*)*\d+\])+/g, '') .replace(/[ \t]+([.,;:])/g, '$1') .replace(/[ \t]{2,}/g, ' '); } function buildUserPrompt(question, context, history, searchQuery) { var hist = history.map(function(m) { return m.role.toUpperCase() + ': ' + String(m.content); }).join('\n'); var searchNote = searchQuery && searchQuery !== question ? ('\n\nStandalone retrieval query used:\n' + searchQuery) : ''; return 'Question:\n' + question + searchNote + '\n\nFull conversation context (prior AI output is not evidence; use fresh retrieved sources for factual claims):\n' + (hist || 'None') + '\n\nRetrieved sources:\n' + context + '\n\nWrite the answer now. If the question is a short misspelled or partial term and the sources point to a likely concept, answer the likely concept rather than asking for clarification.'; } function assistantGenerationOptions(overrides) { return Object.assign({ reasoningEffort: 'low', reasoningFormat: 'hidden' }, overrides || {}); } async function finalizeAssistantAnswer(ai, options) { options = options || {}; var answer = stripModelSourcesSection(String(ai && ai.content || '').trim()); if (shouldRegenerateTruncatedAnswer(answer, ai && ai.finishReason) && typeof options.callAI === 'function') { console.warn('[clinical-assistant] answer looked truncated; regenerating final answer', { finishReason: ai && ai.finishReason, chars: answer.length, streamed: Boolean(options.streamed) }); if (typeof options.onRegenerating === 'function') options.onRegenerating(); var completed = await options.callAI(options.messages, Object.assign({}, options.generationOptions || {}, { model: options.chatModel || undefined, maxTokens: 5000 })); answer = stripModelSourcesSection(String(completed.content || '').trim()) || answer; ai.model = completed.model || ai.model; ai.provider = completed.provider || ai.provider; ai.finishReason = completed.finishReason || ai.finishReason; } return { answer: answer, ai: ai }; } function stripModelSourcesSection(answer) { return cleanDanglingSourceLeadIn(String(answer || '') .replace(/(?:^|\s)(?:\bsrc\b\s*){2,}$/i, '') .replace(/\bsrc\b(?=\s*src\b)/gi, '') .replace(/\n\s*(---\s*)?(#{1,3}\s*)?(Sources|References)\s*\n[\s\S]*$/i, '') .replace(/\n\s*>?\s*(⚠️\s*)?Clinical decision support[^\n]*$/gim, '') .trim()); } function cleanDanglingSourceLeadIn(answer) { answer = String(answer || '').trim(); return answer .replace(/([.!?])\s+(?:however,?\s*)?(?:but\s*)?(?:the\s*)?available(?:\s+(?:sources?|retrieved\s+sources?|evidence|material))?\s*$/i, '$1') .replace(/([.!?])\s+(?:however,?\s*)?(?:but\s*)?(?:based\s+on\s+)?(?:the\s*)?available\s*$/i, '$1') .replace(/(?:^|\n\n)(?:however,?\s*)?(?:but\s*)?(?:the\s*)?available(?:\s+(?:sources?|retrieved\s+sources?|evidence|material))?\s*$/i, '') .replace(/(?:^|\n\n)(?:however,?\s*)?(?:but\s*)?(?:based\s+on\s+)?(?:the\s*)?available\s*$/i, '') .trim(); } function shouldRegenerateTruncatedAnswer(answer, finishReason) { answer = String(answer || '').trim(); if (!answer) return false; if (finishReason === 'length') return true; if (/\b(the|a|an|and|or|but|with|without|for|to|of|in|on|as|by|from|because|therefore|however|some|many|most|few|several|additional|other|further|including|such|available)$/i.test(answer)) return true; if (/[,:;\-(]$/.test(answer)) return true; var lines = answer.split(/\n+/).map(function(line) { return line.trim(); }).filter(Boolean); var last = lines.length ? lines[lines.length - 1] : answer; if (last.length > 24 && /[A-Za-z]$/.test(last) && !/[.!?\])"']$/.test(last)) return true; return false; } module.exports = { buildSystemPrompt: buildSystemPrompt, stripCitationMarkers: stripCitationMarkers, buildUserPrompt: buildUserPrompt, assistantGenerationOptions: assistantGenerationOptions, finalizeAssistantAnswer: finalizeAssistantAnswer, stripModelSourcesSection: stripModelSourcesSection, shouldRegenerateTruncatedAnswer: shouldRegenerateTruncatedAnswer };