perf: context expansion is off by default — the stored excerpt already carries the page, its tables and figure captions
All checks were successful
Forgejo Docker Build / Root app tests (push) Successful in 55s
Forgejo Docker Build / Build Docker image (push) Successful in 10s
Forgejo Docker Build / End-to-end (browser) (push) Successful in 5s

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 17:16:28 +02:00
parent cd2ee9192a
commit a6c2cb1080
5 changed files with 20 additions and 8 deletions

View file

@ -53,7 +53,7 @@ clamped on read so a bad value cannot break a search.
| Feature | Keys | Default | Clamp |
|---|---|---|---|
| Clinical Assistant | `clinical_assistant.search_limit`, `clinical_assistant.context_chars` | 8, 1400 | 320, 3004000 |
| Clinical Assistant | `clinical_assistant.search_limit`, `clinical_assistant.context_chars` | 8, 0 | 320, 04000 (0 = excerpt only) |
| My Resources | `learning.search_limit`, `learning.context_chars` | 30, 2500 | 360, 3008000 |
The `learning.*` names are historical: they were the Learning Hub's, and My
@ -61,7 +61,7 @@ Resources inherited the retrieval code when that was removed. Renaming the keys
would orphan whatever an administrator has already set, so they keep the old
names.
`search_limit` is how many excerpts to request; `context_chars` is how much text
`search_limit` is how many excerpts to request; `context_chars` is how much extra text around each excerpt to fetch from the source at query time — 0, the default, sends the stored excerpt as it is, which already carries the page, its tables and figure captions; any other value makes the search service download and re-extract the source document per hit (about a second each, cached for 15 minutes). It is how much text
to pull around each one.
See [my-resources.md](my-resources.md) for the rest of that feature — its

View file

@ -409,7 +409,7 @@
</div>
<div class="admin-row">
<label for="assistant-context-chars" class="admin-row-label">Context per excerpt (characters)</label>
<input id="assistant-context-chars" type="number" min="300" max="4000" value="1400" class="admin-control">
<input id="assistant-context-chars" type="number" min="0" max="4000" value="0" class="admin-control" title="0 = the stored excerpt only (no re-extraction of the source at query time)">
</div>
</div>
<div class="admin-row" id="assistant-library-index">

View file

@ -226,7 +226,7 @@ export function initClinicalAssistantAdmin(adminEscapeHtml) {
setValue('assistant-indexer-url', cfg['clinical_assistant.indexer_url'] || '');
setValue('assistant-indexer-token', ''); // never echoed back; blank means keep
loadLibraryIndexStatus();
setValue('assistant-context-chars', cfg['clinical_assistant.context_chars'] || '1400');
setValue('assistant-context-chars', cfg['clinical_assistant.context_chars'] == null || cfg['clinical_assistant.context_chars'] === '' ? '0' : cfg['clinical_assistant.context_chars']);
setValue('assistant-translate-provider', 'libretranslate'); // the only provider the server accepts
var sourcesBox = document.getElementById('assistant-show-sources');
if (sourcesBox) {
@ -436,7 +436,7 @@ export function initClinicalAssistantAdmin(adminEscapeHtml) {
Promise.all([
putAssistantConfig('clinical_assistant.conversation_chars', getValue('assistant-conversation-budget')),
putAssistantConfig('clinical_assistant.search_limit', getValue('assistant-search-limit') || '8'),
putAssistantConfig('clinical_assistant.context_chars', getValue('assistant-context-chars') || '1400'),
putAssistantConfig('clinical_assistant.context_chars', getValue('assistant-context-chars') || '0'),
putAssistantConfig('clinical_assistant.translate_provider', getValue('assistant-translate-provider') || 'libretranslate'),
putAssistantConfig('clinical_assistant.show_sources',
(document.getElementById('assistant-show-sources') || {}).checked === false ? 'false' : 'true')

View file

@ -91,7 +91,7 @@ router.get('/clinical-assistant/status', async function(req, res) {
var chatModel = choices.chatConfigured;
var imageModel = choices.imageConfigured;
var searchLimit = clampInt(await getSetting('clinical_assistant.search_limit', '8'), 3, 20, 8);
var contextChars = clampInt(await getSetting('clinical_assistant.context_chars', '1400'), 300, 4000, 1400);
var contextChars = clampInt(await getSetting('clinical_assistant.context_chars', '0'), 0, 4000, 0);
var translateProvider = String(await getSetting('clinical_assistant.translate_provider', '') || 'libretranslate').toLowerCase();
if (!clinicalTranslation.TRANSLATE_PROVIDERS.includes(translateProvider)) translateProvider = 'libretranslate';
var budget = conversationBudget(process.env);
@ -574,9 +574,13 @@ async function prepareAssistantChat(body) {
// The model that gets shown an attachment when the chat model cannot be.
var visionModel = String(await getSetting('clinical_assistant.vision_model', '') || '');
var searchLimit = clampInt(await getSetting('clinical_assistant.search_limit', '8'), 3, 20, 8);
var contextChars = clampInt(await getSetting('clinical_assistant.context_chars', '1400'), 300, 4000, 1400);
var contextChars = clampInt(await getSetting('clinical_assistant.context_chars', '0'), 0, 4000, 0);
var behavior = await getSetting('clinical_assistant.system_behavior', DEFAULT_BEHAVIOR) || DEFAULT_BEHAVIOR;
var includeContext = body.includeContext !== false;
// Context expansion made the search service download each hit's whole PDF
// and extract it again at query time to widen the excerpt — a second per
// hit. The stored excerpt already carries the page, its tables and figure
// captions, so the default is off: 0 characters means "the excerpt only".
var includeContext = body.includeContext !== false && contextChars > 0;
var showSources = await showSourcesEnabled();
// Phase timings, numbers only, so "is it slow?" can be answered from the

View file

@ -295,3 +295,11 @@ test('silent SSO: prompt=none on request, a refusal is not an error, the page tr
assert.match(js, /urlParams\.get\('sso'\) === 'none'/);
assert.match(read('docs/authentication.md'), /## Signed in at PedsHub means signed in here/);
});
test('context expansion is off unless an admin asks for it: 0 characters means the stored excerpt only', () => {
const src = read('src/routes/clinicalAssistant.js');
assert.equal((src.match(/getSetting\('clinical_assistant\.context_chars', '0'\), 0, 4000, 0\)/g) || []).length, 2);
assert.match(src, /includeContext = body\.includeContext !== false && contextChars > 0;/);
assert.match(read('public/components/admin.html'), /id="assistant-context-chars" type="number" min="0"/);
});