pediatric-ai-scribe-v3/test/assistant-image-intent.test.js
Daniel a95236e363 refactor: the model decides about acknowledgements and images, not route heuristics
The previous fix put a hand-maintained multilingual word list in the route. That
does not generalise — the bug report itself was "Окей", and the next one would be
a language not on the list. The model already has the tool, the system prompt and
the whole conversation, so the policy belongs where it reads it.

Removed from src/routes/clinicalAssistant.js:
- GREETING_RE and its short-circuit (an ASCII keyword list that decided, before
  the model ever saw the message, that a greeting deserved a canned reply).
- The IMAGE_NOUN / IMAGE_REPEAT vocabulary lists and the answer-repetition
  backstop added earlier today.

Moved into the prompt and the tool definition:
- DEFAULT_BEHAVIOR now says that a greeting, acknowledgement or thanks in ANY
  language gets a one-sentence request for a clinical question, and that a
  previous turn's answer must never be repeated to justify a second image.
- The generate_image description says to call it ONLY when the user's latest
  message asks for a picture or a change to one just made, and that an
  acknowledgement of an existing image is not such a request.

Both are admin-editable (clinical_assistant.system_behavior), so this can now be
tuned without a deploy.

dispatchImageRequestFallback and IMAGE_REQUEST_PATTERN stay: that is the
compatibility path for a serving model that writes the image prompt as text
instead of calling the tool. It reads the USER's message only, never the model's
answer, so it cannot replay a previous turn — it was not the cause of this bug.

The DEFAULT_BEHAVIOR byte-hash lock in prompt-administration.test.js is updated
deliberately, which is what that guard is for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BkfrkQwA4YGrGw9LZSpeAq
2026-09-09 19:06:28 +02:00

53 lines
3 KiB
JavaScript

const { test } = require('node:test');
const assert = require('node:assert/strict');
const path = require('node:path');
const { pathToFileURL } = require('node:url');
async function imagesModule() {
return import(pathToFileURL(path.join(__dirname, '..', 'public/js/assistant/images.js')).href);
}
test('assistant image intent does not intercept table retrieval requests', async () => {
const { isImageRequest } = await imagesModule();
assert.equal(isImageRequest('can you show me the table?'), false);
assert.equal(isImageRequest('show me Table 13.1'), false);
});
test('assistant image intent still handles explicit visual requests', async () => {
const { isImageRequest } = await imagesModule();
assert.equal(isImageRequest('show me the diagram'), true);
assert.equal(isImageRequest('create an infographic for asthma'), true);
});
// A run of image turns used to make every acknowledgement produce another image:
// the model saw its own "I'll generate an educational image…" in the history and
// replayed it verbatim for "Окей" and "Nice". The fix is not a word list in the
// route — the model already has the tool, the prompt and the conversation, so the
// policy lives where the model reads it.
test('the system prompt tells the model to decide, in the user\'s own language', () => {
const { DEFAULT_BEHAVIOR } = require('../src/utils/clinicalPrompts');
assert.match(DEFAULT_BEHAVIOR, /in any language/i, 'acknowledgements are recognised by the model, not by a list');
assert.match(DEFAULT_BEHAVIOR, /ONLY when the user's latest message itself asks for a picture/);
assert.match(DEFAULT_BEHAVIOR, /never repeat a previous turn's answer/);
assert.match(DEFAULT_BEHAVIOR, /ask what they would like you to look up|asking what they would like you to look up/i);
});
test('the tool description itself says when not to call it', () => {
const { tools } = require('../src/utils/imageTool');
const description = tools[0].function.description;
assert.match(description, /ONLY when the user's latest message itself asks for a picture/);
assert.match(description, /is not a request for another one/);
assert.match(description, /any other language/);
});
test('no hand-maintained acknowledgement or language list is left in the route', () => {
const fs = require('node:fs');
const src = fs.readFileSync(path.join(__dirname, '..', 'src/routes/clinicalAssistant.js'), 'utf8');
assert.doesNotMatch(src, /GREETING_RE/, 'greeting word list removed — the model decides');
assert.doesNotMatch(src, /IMAGE_NOUN_NON_LATIN|IMAGE_REPEAT_NON_LATIN/, 'no multilingual vocabulary lists');
assert.doesNotMatch(src, /suppressRepeatedImage/, 'no answer-inspection backstop');
// The one surviving pattern is the compatibility path for models that write
// the prompt as text; it reads the USER's message only.
const fallback = src.slice(src.indexOf('async function dispatchImageRequestFallback'));
assert.match(fallback, /isExplicitImageRequest\(prepared\.message\)/, 'gated on the user, never on the answer');
});