fix assistant stream truncation handling
This commit is contained in:
parent
93255a1c0d
commit
b75c2f8dca
3 changed files with 44 additions and 4 deletions
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue