fix assistant stream truncation handling

This commit is contained in:
Daniel 2026-05-07 23:02:26 +02:00
parent 93255a1c0d
commit b75c2f8dca
3 changed files with 44 additions and 4 deletions

View file

@ -21,6 +21,7 @@ import { escapeAttr, escapeHtml, renderAssistantMarkdown, stripSourcesSection }
var exportCacheKey = '';
var exportCacheItems = null;
var markdownRenderer = null;
var assistantBusy = false;
document.addEventListener('tabChanged', function (e) {
if (e.detail && e.detail.tab === 'assistant') initIfNeeded();
@ -100,6 +101,10 @@ import { escapeAttr, escapeHtml, renderAssistantMarkdown, stripSourcesSection }
var input = document.getElementById('assistant-input');
var includeContext = document.getElementById('assistant-include-context');
var text = input ? input.value.trim() : '';
if (assistantBusy) {
if (typeof showToast === 'function') showToast('Assistant is still finishing the current answer', 'error');
return;
}
if (!text) {
if (typeof showToast === 'function') showToast('Enter a clinical question', 'error');
return;
@ -202,6 +207,10 @@ import { escapeAttr, escapeHtml, renderAssistantMarkdown, stripSourcesSection }
var tail = parseSseEvent(buffer);
if (tail) handleEvent(tail.type, tail.data);
}
if (!doneData) {
updateLoadingMessage(loading, 'Stream ended early. Retrying without streaming...');
doneData = await fetchAssistantFallback(payload);
}
setBusy(false, 'Ready');
var answer = (doneData && (doneData.answer || doneData.markdown)) || partial;
@ -215,6 +224,18 @@ import { escapeAttr, escapeHtml, renderAssistantMarkdown, stripSourcesSection }
}
}
async function fetchAssistantFallback(payload) {
var response = await fetch('/api/clinical-assistant/chat', {
method: 'POST',
headers: getAuthHeaders(),
credentials: 'same-origin',
body: JSON.stringify(payload)
});
var data = await response.json().catch(function () { return {}; });
if (!response.ok || !data.success) throw new Error(data.error || ('Request failed (' + response.status + ')'));
return data;
}
function parseSseEvent(block) {
var type = 'message';
var data = '';
@ -899,9 +920,11 @@ import { escapeAttr, escapeHtml, renderAssistantMarkdown, stripSourcesSection }
}
function setBusy(isBusy, text, isError) {
assistantBusy = !!isBusy;
var status = document.getElementById('assistant-status');
var label = document.getElementById('assistant-status-text');
var send = document.getElementById('btn-assistant-send');
var input = document.getElementById('assistant-input');
if (status) {
status.classList.toggle('busy', !!isBusy);
status.classList.toggle('error', !!isError);
@ -911,6 +934,7 @@ import { escapeAttr, escapeHtml, renderAssistantMarkdown, stripSourcesSection }
send.disabled = !!isBusy;
send.innerHTML = isBusy ? '<i class="fas fa-spinner fa-spin"></i> Searching' : '<i class="fas fa-paper-plane"></i> Ask';
}
if (input) input.disabled = !!isBusy;
}
function sanitize(html) { return window.DOMPurify ? window.DOMPurify.sanitize(html, { ADD_ATTR: ['target'] }) : html; }

View file

@ -241,6 +241,18 @@ router.post('/clinical-assistant/chat', async function(req, res) {
maxTokens: 2600
});
var answer = stripModelSourcesSection(String(ai.content || '').trim());
if (shouldRegenerateTruncatedAnswer(answer, ai.finishReason)) {
console.warn('[clinical-assistant] non-stream answer looked truncated; regenerating', { finishReason: ai.finishReason, chars: answer.length });
var completed = await callAI(prepared.messages, {
model: prepared.chatModel || undefined,
temperature: 0.15,
maxTokens: 5000
});
answer = stripModelSourcesSection(String(completed.content || '').trim()) || answer;
ai.model = completed.model || ai.model;
ai.provider = completed.provider || ai.provider;
ai.finishReason = completed.finishReason || ai.finishReason;
}
logger.audit(req.user.id, 'clinical_assistant_query', 'Clinical assistant query', req, {
category: 'clinical', model: ai.model || prepared.chatModel, duration: Date.now() - started
@ -294,11 +306,12 @@ router.post('/clinical-assistant/chat/stream', async function(req, res) {
});
var answer = stripModelSourcesSection(String(ai.content || '').trim());
if (shouldRegenerateTruncatedAnswer(answer, ai.finishReason)) {
console.warn('[clinical-assistant] stream answer looked truncated; regenerating', { finishReason: ai.finishReason, chars: answer.length });
sendEvent('status', { message: 'Completing answer...' });
var completed = await callAI(prepared.messages, {
model: prepared.chatModel || undefined,
temperature: 0.15,
maxTokens: 3200
maxTokens: 5000
});
answer = stripModelSourcesSection(String(completed.content || '').trim()) || answer;
ai.model = completed.model || ai.model;

View file

@ -167,7 +167,8 @@ async function callOpenRouter(messages, model, temperature, maxTokens) {
content: completion.choices[0].message.content,
model: model,
provider: 'openrouter',
usage: completion.usage || null
usage: completion.usage || null,
finishReason: completion.choices[0].finish_reason || null
};
}
@ -189,7 +190,8 @@ async function callAzure(messages, model, temperature, maxTokens) {
content: completion.choices[0].message.content,
model: process.env.AZURE_DEPLOYMENT_NAME || model,
provider: 'azure',
usage: completion.usage || null
usage: completion.usage || null,
finishReason: completion.choices[0].finish_reason || null
};
}
@ -391,7 +393,8 @@ async function callLiteLLM(messages, model, temperature, maxTokens) {
content: completion.choices[0].message.content,
model: model,
provider: 'litellm',
usage: completion.usage || null
usage: completion.usage || null,
finishReason: completion.choices[0].finish_reason || null
};
}