From 4127e2d10122ddd3fc1023820ae6f3225b5577d6 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sun, 13 Sep 2026 18:38:11 +0200 Subject: [PATCH] feat: the starter-prompt pool is built once and regenerated from the admin button only, on gpt-4.1-mini by default; the question box sits two-thirds down before a chat and at the bottom during one Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU --- .env.example | 2 +- docs/clinical-assistant.md | 6 +++--- docs/configuration.md | 2 +- public/css/assistant.css | 11 +++++++++++ src/utils/clinicalPromptPool.js | 10 ++++++++-- test/clinical-assistant-prompt-pool.test.js | 4 +++- 6 files changed, 27 insertions(+), 8 deletions(-) diff --git a/.env.example b/.env.example index 95692a74..dd68c79c 100644 --- a/.env.example +++ b/.env.example @@ -266,7 +266,7 @@ DB_PASSWORD=pedscribe_secret_change_me # CLINICAL_ASSISTANT_PROMPT_MODEL= # model that writes the starter prompts # CLINICAL_ASSISTANT_PROMPT_POOL_TARGET=1000 # CLINICAL_ASSISTANT_PROMPT_POOL_KEY=clinical-assistant:prompt-pool:v2 -# CLINICAL_ASSISTANT_PROMPT_POOL_REFRESH_MS= +# CLINICAL_ASSISTANT_PROMPT_POOL_REFRESH_MS= # 0 (default): no schedule, admin button only; a positive number of ms adds a timer # CLINICAL_ASSISTANT_PROMPT_POOL_WARMUP_DELAY_MS= # CLINICAL_ASSISTANT_EXAMPLE_CACHE_MS=600000 # PATIENT_TAKEHOME_BEHAVIOR= # overrides the admin-editable take-home guidance diff --git a/docs/clinical-assistant.md b/docs/clinical-assistant.md index ff96cdce..75bcbe8d 100644 --- a/docs/clinical-assistant.md +++ b/docs/clinical-assistant.md @@ -117,7 +117,7 @@ falls back to the default in the right-hand column. | `clinical_assistant.system_behavior` | Admin-editable assistant behavior guidance | | `clinical_assistant.image_behavior` | Guidance for the `generate_image` tool | | `clinical_assistant.patient_takehome_behavior` | Guidance for patient take-home text | -| `clinical_assistant.prompt_model` | Model that generates the starter prompt pool | +| `clinical_assistant.prompt_model` | Model that generates the starter prompt pool (default `openrouter-gpt-4.1-mini`) | | `clinical_assistant.translate_provider` | Translation provider. `libretranslate` is the only value the server accepts | | `clinical_assistant.citations_enabled` | Legacy key, read only as a fallback for `show_sources` | @@ -168,9 +168,9 @@ a setting falls back to. | `CLINICAL_ASSISTANT_MCP_WARMUP_DELAY_MS` | 5000 | Delay before that warmup. | | `CLINICAL_ASSISTANT_CONVERSATION_CHARS` | 120000 | Input budget in UTF-16 code units, when the setting is empty. | | `CLINICAL_ASSISTANT_IMAGE_MODEL` | `openai-gpt-image-1` | Image model, when the setting is empty. | -| `CLINICAL_ASSISTANT_PROMPT_MODEL` | — | Model for the starter prompt pool, when the setting is empty. | +| `CLINICAL_ASSISTANT_PROMPT_MODEL` | `openrouter-gpt-4.1-mini` | Model for the starter prompt pool, when the setting is empty. A non-reasoning model. | | `CLINICAL_ASSISTANT_PROMPT_POOL_TARGET` | 1000 | How many example prompts to generate. | -| `CLINICAL_ASSISTANT_PROMPT_POOL_REFRESH_MS` | 7 days | How often the pool regenerates. `0` disables refresh. | +| `CLINICAL_ASSISTANT_PROMPT_POOL_REFRESH_MS` | `0` | No schedule: the pool is built once, kept in Redis (with a last-good copy and a database snapshot), and regenerated only from Admin → Clinical Assistant → Regenerate. A positive value adds a timer. | | `CLINICAL_ASSISTANT_PROMPT_POOL_KEY` | `clinical-assistant:prompt-pool:v2` | Redis key holding the pool. | | `CLINICAL_ASSISTANT_PROMPT_POOL_WARMUP_DELAY_MS` | 15000 | Delay before the pool warms at boot. | | `CLINICAL_ASSISTANT_EXAMPLE_CACHE_MS` | 600000 | How long the examples endpoint caches its answer. | diff --git a/docs/configuration.md b/docs/configuration.md index e309bc07..277ea5d2 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -60,7 +60,7 @@ keys): ### Starter questions | Setting | Meaning | |---|---| -| `clinical_assistant.prompt_model` | The model that writes the starter-question pool. Use a **non-reasoning** model (`openrouter-gpt-4.1-mini` in production): a reasoning model spends the whole completion budget thinking about a 20-question JSON list and returns nothing. Falls back to `clinical_assistant.chat_model`, then `models.default`. | +| `clinical_assistant.prompt_model` | The model that writes the starter-question pool. Use a **non-reasoning** model (`openrouter-gpt-4.1-mini` in production): a reasoning model spends the whole completion budget thinking about a 20-question JSON list and returns nothing. Defaults to `openrouter-gpt-4.1-mini` when unset. The pool is generated once and then only when an admin presses Regenerate; it is stored in Redis with a database snapshot. | ### Embeddings diff --git a/public/css/assistant.css b/public/css/assistant.css index 5b5d5d6e..4e5f8073 100644 --- a/public/css/assistant.css +++ b/public/css/assistant.css @@ -14,6 +14,17 @@ .assistant-new-chat:hover { border-color:var(--blue); background:var(--blue-light); } .assistant-main { display:flex; flex-direction:column; min-height:0; min-width:0; } #assistant-chat-view { display:grid; grid-template-rows:minmax(0,1fr) auto; min-height:0; min-width:0; } +/* Where the question box sits. With no conversation yet, the heading and the + starter questions are centred in a shorter view and the box sits just under + them, about two-thirds of the way down — neither hugging the header nor + pinned to the floor. Once there are messages the view fills the screen and + the box stays at the bottom, where a chat box belongs. */ +@media (min-width: 961px) { + #assistant-chat-view:not(:has(.assistant-msg)) { min-height:min(62vh, 640px); } + #assistant-chat-view:has(.assistant-msg) { min-height:calc(100vh - 190px); } + .assistant-messages:not(:has(.assistant-msg)) { display:flex; flex-direction:column; justify-content:center; } + .assistant-messages:not(:has(.assistant-msg)) .assistant-empty { margin:0 auto; } +} .assistant-messages { padding:16px; max-height:none; overflow-y:auto; overflow-x:hidden; background:transparent; min-width:0; } .assistant-empty { max-width:680px; margin:50px auto; text-align:center; color:var(--g500); } .assistant-empty i { font-size:34px; color:var(--blue); margin-bottom:10px; } diff --git a/src/utils/clinicalPromptPool.js b/src/utils/clinicalPromptPool.js index 63f2bfb8..016c2b6b 100644 --- a/src/utils/clinicalPromptPool.js +++ b/src/utils/clinicalPromptPool.js @@ -256,7 +256,11 @@ var PROMPT_VERSION = 2; 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); + // No schedule by default: the pool is built once (when there is none, or the + // prompt version changed) and after that only when an admin presses + // Regenerate. It lives in Redis with a last-good copy and a database + // snapshot, so a restart never rebuilds it. A positive value restores a timer. + var refreshMs = nonNegativeInt(process.env.CLINICAL_ASSISTANT_PROMPT_POOL_REFRESH_MS, 0); 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$/, ''); @@ -328,7 +332,9 @@ function createClinicalPromptPool(opts) { 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', ''); + // A small non-reasoning model writes the pool: a reasoning model spends its + // budget thinking about a twenty-line list and returns nothing. + var chatModel = await opts.getSetting('clinical_assistant.prompt_model', '') || process.env.CLINICAL_ASSISTANT_PROMPT_MODEL || 'openrouter-gpt-4.1-mini'; var generated = []; var startedAt = Date.now(); // A build that produced nothing used to vanish without a word; now it says diff --git a/test/clinical-assistant-prompt-pool.test.js b/test/clinical-assistant-prompt-pool.test.js index 6560af09..c01b3dee 100644 --- a/test/clinical-assistant-prompt-pool.test.js +++ b/test/clinical-assistant-prompt-pool.test.js @@ -22,7 +22,9 @@ test('clinical assistant starter prompts are Redis or indexed-source backed', () assert.match(pool, /redisBaseKey \+ ':age:' \+ ageBand/); assert.match(pool, /redisBaseKey \+ ':intent:' \+ intent/); assert.match(pool, /CLINICAL_ASSISTANT_PROMPT_POOL_TARGET, 1000/); - assert.match(pool, /CLINICAL_ASSISTANT_PROMPT_POOL_REFRESH_MS, 7 \* 24 \* 60 \* 60 \* 1000/); + // Built once, then only from the admin button: no timer unless the environment asks for one. + assert.match(pool, /CLINICAL_ASSISTANT_PROMPT_POOL_REFRESH_MS, 0\)/); + assert.match(pool, /process\.env\.CLINICAL_ASSISTANT_PROMPT_MODEL \|\| 'openrouter-gpt-4\.1-mini'/); assert.match(route, /loadStoredPromptPool: loadLatestPromptPoolSnapshot/); assert.match(pool, /PEDIATRIC_TAXONOMY/); assert.match(pool, /taxonomyWithQuotas\(target\)/);