diff --git a/src/public/app/widgets/llm_chat/communication.ts b/src/public/app/widgets/llm_chat/communication.ts index 2330940a5..0a206b100 100644 --- a/src/public/app/widgets/llm_chat/communication.ts +++ b/src/public/app/widgets/llm_chat/communication.ts @@ -87,9 +87,13 @@ export async function setupStreamingResponse( // Handle content updates if (message.content) { receivedAnyContent = true; + + console.log(`[${responseId}] Received content chunk of length ${message.content.length}, preview: "${message.content.substring(0, 50)}${message.content.length > 50 ? '...' : ''}"`); + + // Add to our accumulated response assistantResponse += message.content; - // Update the UI immediately + // Update the UI immediately with each chunk onContentUpdate(assistantResponse); // Reset timeout since we got content diff --git a/src/services/llm/providers/ollama_service.ts b/src/services/llm/providers/ollama_service.ts index b65f435ae..f7cbb78d2 100644 --- a/src/services/llm/providers/ollama_service.ts +++ b/src/services/llm/providers/ollama_service.ts @@ -430,13 +430,27 @@ export class OllamaService extends BaseAIService { // Call the callback with the current chunk content if (opts.streamCallback) { - await StreamProcessor.sendChunkToCallback( - opts.streamCallback, - chunk.message?.content || '', - false, // Never mark as done during processing - chunk, - chunkCount - ); + // For chunks with content, send the content directly + if (chunk.message?.content) { + log.info(`Sending direct chunk #${chunkCount} with content: "${chunk.message.content.substring(0, 50)}${chunk.message.content.length > 50 ? '...' : ''}"`); + + await StreamProcessor.sendChunkToCallback( + opts.streamCallback, + chunk.message.content, + !!chunk.done, // Mark as done if done flag is set + chunk, + chunkCount + ); + } else if (chunk.done) { + // Send empty done message for final chunk with no content + await StreamProcessor.sendChunkToCallback( + opts.streamCallback, + '', + true, + chunk, + chunkCount + ); + } } // If this is the done chunk, log it @@ -446,7 +460,9 @@ export class OllamaService extends BaseAIService { } // Send one final callback with done=true after all chunks have been processed - if (opts.streamCallback) { + // Only send this if the last chunk didn't already have done=true + if (opts.streamCallback && (!finalChunk || !finalChunk.done)) { + log.info(`Sending explicit final callback with done=true flag after all chunks processed`); await StreamProcessor.sendFinalCallback(opts.streamCallback, completeText); } diff --git a/src/services/llm/providers/stream_handler.ts b/src/services/llm/providers/stream_handler.ts index 29aa7b114..8d8e3a97f 100644 --- a/src/services/llm/providers/stream_handler.ts +++ b/src/services/llm/providers/stream_handler.ts @@ -40,9 +40,9 @@ export class StreamProcessor { let textToAdd = ''; let logged = false; - // Log first chunk and periodic updates - if (chunkCount === 1 || chunkCount % 10 === 0) { - log.info(`Processing ${options.providerName} stream chunk #${chunkCount}, done=${!!chunk.done}, has content=${!!chunk.message?.content}`); + // Enhanced logging for content chunks and completion status + if (chunkCount === 1 || chunkCount % 10 === 0 || chunk.done) { + log.info(`Processing ${options.providerName} stream chunk #${chunkCount}, done=${!!chunk.done}, has content=${!!chunk.message?.content}, content length=${chunk.message?.content?.length || 0}`); logged = true; } @@ -52,10 +52,19 @@ export class StreamProcessor { const newCompleteText = completeText + textToAdd; if (chunkCount === 1) { - log.info(`First content chunk: "${textToAdd.substring(0, 50)}${textToAdd.length > 50 ? '...' : ''}"`); + // Log the first chunk more verbosely for debugging + log.info(`First content chunk [${chunk.message.content.length} chars]: "${textToAdd.substring(0, 100)}${textToAdd.length > 100 ? '...' : ''}"`); + } + + // For final chunks with done=true, log more information + if (chunk.done) { + log.info(`Final content chunk received with done=true flag. Length: ${chunk.message.content.length}`); } return { completeText: newCompleteText, logged }; + } else if (chunk.done) { + // If it's the final chunk with no content, log this case + log.info(`Empty final chunk received with done=true flag`); } return { completeText, logged }; @@ -72,7 +81,15 @@ export class StreamProcessor { chunkNumber: number ): Promise { try { - const result = callback(content || '', done, chunk); + // Log all done=true callbacks and first chunk for debugging + if (done || chunkNumber === 1) { + log.info(`Sending chunk to callback: chunkNumber=${chunkNumber}, contentLength=${content?.length || 0}, done=${done}`); + } + + // Always make sure we have a string for content + const safeContent = content || ''; + + const result = callback(safeContent, done, chunk); // Handle both Promise and void return types if (result instanceof Promise) { await result; @@ -81,6 +98,10 @@ export class StreamProcessor { if (chunkNumber === 1) { log.info(`Successfully called streamCallback with first chunk`); } + + if (done) { + log.info(`Successfully called streamCallback with done=true flag`); + } } catch (callbackError) { log.error(`Error in streamCallback: ${callbackError}`); } @@ -94,12 +115,18 @@ export class StreamProcessor { completeText: string ): Promise { try { - log.info(`Sending final done=true callback after processing all chunks`); - const result = callback('', true, { done: true }); + log.info(`Sending explicit final done=true callback after processing all chunks. Complete text length: ${completeText?.length || 0}`); + + // Pass the complete text instead of empty string for better UX + // The client will know it's done based on the done=true flag + const result = callback(completeText || '', true, { done: true, complete: true }); + // Handle both Promise and void return types if (result instanceof Promise) { await result; } + + log.info(`Final callback sent successfully with done=true flag`); } catch (finalCallbackError) { log.error(`Error in final streamCallback: ${finalCallbackError}`); } diff --git a/src/services/llm/rest_chat_service.ts b/src/services/llm/rest_chat_service.ts index b8379beaa..23bb8d129 100644 --- a/src/services/llm/rest_chat_service.ts +++ b/src/services/llm/rest_chat_service.ts @@ -1163,18 +1163,22 @@ class RestChatService { if (chunk.text) { messageContent += chunk.text; - // Send the chunk content via WebSocket + // Enhanced logging for each chunk + log.info(`Received stream chunk from ${service.getName()} with ${chunk.text.length} chars of text, done=${!!chunk.done}`); + + // Send each individual chunk via WebSocket as it arrives wsService.sendMessageToAllClients({ type: 'llm-stream', sessionId, content: chunk.text, + done: !!chunk.done, // Include done flag with each chunk // Include any raw data from the provider that might contain thinking/tool info ...(chunk.raw ? { raw: chunk.raw } : {}) } as LLMStreamMessage); // Log the first chunk (useful for debugging) if (messageContent.length === chunk.text.length) { - log.info(`First stream chunk received from ${service.getName()}`); + log.info(`First stream chunk received from ${service.getName()}: "${chunk.text.substring(0, 50)}${chunk.text.length > 50 ? '...' : ''}"`); } } @@ -1198,15 +1202,23 @@ class RestChatService { // Signal completion when done if (chunk.done) { - log.info(`Stream completed from ${service.getName()}`); + log.info(`Stream completed from ${service.getName()}, total content: ${messageContent.length} chars`); - // Send the final message with both content and done flag together - wsService.sendMessageToAllClients({ - type: 'llm-stream', - sessionId, - content: messageContent, // Send the accumulated content - done: true - } as LLMStreamMessage); + // Only send final done message if it wasn't already sent with content + // This ensures we don't duplicate the content but still mark completion + if (!chunk.text) { + // Send final message with both content and done flag together + wsService.sendMessageToAllClients({ + type: 'llm-stream', + sessionId, + content: messageContent, // Send the accumulated content + done: true + } as LLMStreamMessage); + + log.info(`Sent explicit final completion message with accumulated content`); + } else { + log.info(`Final done flag was already sent with content chunk, no need for extra message`); + } } });