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
All checks were successful
Forgejo Docker Build / Root app tests (push) Successful in 49s
Forgejo Docker Build / Build Docker image (push) Successful in 10s
Forgejo Docker Build / End-to-end (browser) (push) Successful in 6s

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
This commit is contained in:
Daniel 2026-09-13 18:38:11 +02:00
parent 4119788815
commit 4127e2d101
6 changed files with 27 additions and 8 deletions

View file

@ -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

View file

@ -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. |

View file

@ -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

View file

@ -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; }

View file

@ -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

View file

@ -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\)/);