Some checks failed
Forgejo Android APK / Root app tests (push) Successful in 50s
Forgejo Docker Build / Root app tests (push) Successful in 48s
Forgejo Android APK / Build signed APK (push) Successful in 2m8s
Forgejo Docker Build / Build Docker image (push) Successful in 18s
Forgejo Docker Build / Deploy to the host (push) Failing after 0s
**The regex is gone.** The route ran a pattern over the user's message and enqueued an image from the answer text when the model had not called the tool. It was a compatibility path for models without tool calling and it did more harm than good: it decided in English only, it could not see the conversation, and "image summary" fell through it while reading as an obvious image request to the model itself — which was measured, not assumed. A second and worse decision-maker sitting behind the first. Whether a message deserves a picture is now the model's call, made from the tool description, which is the only place it ever belonged. **Lending eyes.** The same shape, for a different capability. When someone attaches a photograph and the chat model cannot accept image input, the attachment was either refused by the provider or silently dropped — an answer about a picture nobody had looked at, which is worse than a refusal. The chat model is now offered look_at_image beside the image tool and decides when to use it. The attachment goes to clinical_assistant.vision_model, whose description comes back as a tool result, and the chat model answers in its own voice with its own sources. Only the seeing is delegated; the clinical reasoning stays with the model an administrator chose. The seeing model is told to report and not to diagnose, because it has a picture and no context and an opinion from it would carry weight it has not earned. Delegation triggers only on an explicit supports_vision: false from the gateway. An unknown is left alone — most of a roster reports nothing, and treating silence as blindness would route good models through a detour. The capability lookup moved to its own module, is cached for five minutes because it runs on exactly the requests that are already slowest, and is never inferred from the model id. liteLLMBaseUrl moved from the admin route to litellm.js, where the other gateway helpers live. The new setting is guarded like the slide reviewer: a model the gateway calls text-only cannot be saved as the one that looks at images. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
64 lines
3.6 KiB
JavaScript
64 lines
3.6 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 prompt states principles, not an enumerated list of cases', () => {
|
|
const { DEFAULT_BEHAVIOR } = require('../src/utils/clinicalPrompts');
|
|
assert.match(DEFAULT_BEHAVIOR, /Respond to the user's latest message, not to an earlier one/);
|
|
assert.match(DEFAULT_BEHAVIOR, /If it carries no question/, 'covers acknowledgements without naming any');
|
|
assert.match(DEFAULT_BEHAVIOR, /Never repeat a previous answer/);
|
|
assert.match(DEFAULT_BEHAVIOR, /do not answer from your own knowledge/, 'grounded in retrieved context');
|
|
// No word list anywhere: the model reads the message in whatever language it
|
|
// arrives in, so no vocabulary needs maintaining here.
|
|
for (const word of ['ok', 'nice', 'thanks', 'okay']) {
|
|
assert.doesNotMatch(DEFAULT_BEHAVIOR, new RegExp('"' + word + '"', 'i'), 'no example word list');
|
|
}
|
|
});
|
|
|
|
test('the tool description says when to use it and when not', () => {
|
|
const { tools } = require('../src/utils/imageTool');
|
|
const description = tools[0].function.description;
|
|
assert.match(description, /asks for a picture, or for a change to one you just made/);
|
|
assert.match(description, /and not otherwise/);
|
|
assert.match(description, /grounded in the current clinical content/);
|
|
});
|
|
|
|
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');
|
|
// And now no pattern at all. The last one was a compatibility path for models
|
|
// without tool calling: it decided in English only, could not see the
|
|
// conversation, and let "image summary" through while the model itself read
|
|
// that as an obvious image request. Whether a message deserves a picture is
|
|
// the model's decision, made from the tool description.
|
|
assert.doesNotMatch(src, /IMAGE_REQUEST_PATTERN/);
|
|
assert.doesNotMatch(src, /isExplicitImageRequest/);
|
|
assert.doesNotMatch(src, /dispatchImageRequestFallback/);
|
|
// The tool is still offered — removing the pattern must not remove the path.
|
|
assert.match(src, /tools: imageTool\.tools/);
|
|
});
|