UNPKG

langflow-chatbot

Version:

Add a Langflow-powered chatbot to your website.

363 lines (362 loc) 19 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ChatMessageProcessor = void 0; const uiConstants_1 = require("../config/uiConstants"); class ChatMessageProcessor { /** * Constructs a ChatMessageProcessor. * @param chatClient The client for interacting with the Langflow API. * @param config Configuration defining the sender names/roles (e.g., user, bot, error, system). * @param logger Logger instance for logging messages. * @param ui Callbacks for UI interactions related to message processing. * @param messageParser The message parser for parsing messages. * @param getEnableStream Function to dynamically get the current stream enabled status. * @param getCurrentSessionId Function to dynamically get the current session ID. */ constructor(chatClient, config, logger, ui, messageParser, getEnableStream, getCurrentSessionId) { this.chatClient = chatClient; this.config = config; this.logger = logger; this.ui = ui; this.messageParser = messageParser; this.getEnableStream = getEnableStream; this.getCurrentSessionId = getCurrentSessionId; } /** * Displays the initial "thinking" indicator in the UI. */ displayInitialThinkingIndicator() { const thinkingMsgElement = this.ui.addMessage(this.config.botSender, uiConstants_1.THINKING_BUBBLE_HTML, true, new Date().toISOString()); this.ui.setBotMessageElement(thinkingMsgElement); } /** * Main public method to process a user's message. * It disables input, determines streaming vs. non-streaming, calls the appropriate handler, * and re-enables input. * @param messageText The text of the message from the user. */ async process(messageText) { this.logger.info(`ChatMessageProcessor starting to process: "${messageText}"`); const useStream = this.getEnableStream(); let sessionIdToSend = this.getCurrentSessionId() || undefined; this.ui.setInputDisabled(true); this.ui.setBotMessageElement(null); if (useStream) { await this.handleStreamingResponse(messageText, sessionIdToSend); } else { await this.handleNonStreamingResponse(messageText, sessionIdToSend); } this.ui.setInputDisabled(false); this.logger.info(`ChatMessageProcessor finished processing: "${messageText}"`); } /** * Attempts to update an existing "thinking" message bubble to display an error. * @param baseErrorMessage The main error message text. * @param detailErrorMessage Optional additional details for the error. * @returns True if a thinking bubble was successfully updated to an error, false otherwise. */ tryUpdateThinkingToError(baseErrorMessage, detailErrorMessage) { const botElement = this.ui.getBotMessageElement(); if (botElement && botElement.classList.contains('thinking')) { const fullErrorMessage = detailErrorMessage ? `${baseErrorMessage}: ${detailErrorMessage}` : baseErrorMessage; const parsedErrorMessage = this.messageParser.parseComplete(fullErrorMessage); this.ui.updateMessageContent(botElement, parsedErrorMessage); botElement.classList.remove('thinking', 'bot-message'); botElement.classList.add('error-message'); return true; } return false; } /** * Handles the message processing logic when streaming is enabled. * Iterates through stream events, updates UI progressively, and handles errors/completion. * @param messageText The user's message text. * @param sessionIdToSend The session ID to use for the request. */ async handleStreamingResponse(messageText, sessionIdToSend) { this.displayInitialThinkingIndicator(); let accumulatedResponse = ""; try { for await (const event of this.chatClient.streamMessage(messageText, sessionIdToSend)) { const currentBotElement = this.ui.getBotMessageElement(); if (event.event === 'stream_started') { const startData = event.data; if (startData.sessionId) { this.ui.updateSessionId(startData.sessionId); } continue; } this.clearThinkingIndicatorIfNeeded(event, currentBotElement, accumulatedResponse); accumulatedResponse = this.processStreamEvent(event, accumulatedResponse); } } catch (error) { this.logger.error("handleStreamingResponse: Failed to process stream message:", error); const displayMessage = error.message || "Error processing stream."; if (!this.tryUpdateThinkingToError("Stream Error", displayMessage)) { const parsedDisplayMessage = this.messageParser.parseComplete(`Stream Error: ${displayMessage}`); this.ui.addMessage(this.config.errorSender, parsedDisplayMessage, false, new Date().toISOString()); } } finally { const botElementForFinally = this.ui.getBotMessageElement(); const messageSpan = botElementForFinally?.querySelector('.message-text-content'); if (botElementForFinally && botElementForFinally.classList.contains('thinking')) { this.logger.warn("handleStreamingResponse: Bot element still marked as 'thinking' in finally block. Stream may have ended without content or error."); botElementForFinally.classList.remove('thinking'); const messageSpanAfterRemove = botElementForFinally.querySelector('.message-text-content'); if (messageSpanAfterRemove && messageSpanAfterRemove.innerHTML.trim() === "" && !botElementForFinally.classList.contains('error-message')) { this.logger.warn("handleStreamingResponse: Message span is empty and not an error after 'thinking' removed. Setting to '(No content streamed)'."); const parsedNoContent = this.messageParser.parseComplete("(No content streamed)"); this.ui.updateMessageContent(botElementForFinally, parsedNoContent); // This specific state (thinking-bubble HTML still present after class removal, not an error) // is a deep edge case unlikely to be hit in normal operation and complex to test. /* istanbul ignore next */ } else if (messageSpanAfterRemove && messageSpanAfterRemove.innerHTML.includes('thinking-bubble') && !botElementForFinally.classList.contains('error-message')) { this.logger.warn("handleStreamingResponse: Message span still contains 'thinking-bubble' and not an error. Setting to '(No content streamed)'."); const parsedNoContent = this.messageParser.parseComplete("(No content streamed)"); this.ui.updateMessageContent(botElementForFinally, parsedNoContent); } } this.ui.setBotMessageElement(null); } } /** * Clears the "thinking" indicator from a message element if conditions are met * (e.g., content starts streaming, or an error/end event occurs). * @param event The current stream event. * @param currentBotElement The current bot message HTML element. * @param accumulatedResponse The accumulated response text so far. */ clearThinkingIndicatorIfNeeded(event, currentBotElement, accumulatedResponse) { if (currentBotElement && currentBotElement.classList.contains('thinking')) { let shouldClearThinking = false; if (event.event === 'token' && event.data.chunk.length > 0) { shouldClearThinking = true; } if (event.event === 'end') { const endData = event.data; if (endData.flowResponse?.reply || accumulatedResponse.length > 0) { shouldClearThinking = true; } } if (event.event === 'error') { shouldClearThinking = true; } if (shouldClearThinking) { this.ui.updateMessageContent(currentBotElement, ""); currentBotElement.classList.remove('thinking'); } } } /** * Processes an individual stream event by dispatching to event-specific handlers. * @param event The stream event to process. * @param accumulatedResponse The accumulated response string before this event. * @returns The updated accumulated response string. */ processStreamEvent(event, accumulatedResponse) { switch (event.event) { case 'token': accumulatedResponse = this.handleStreamTokenEvent(event.data, accumulatedResponse); break; case 'error': this.handleStreamErrorEvent(event.data); break; case 'add_message': this.handleStreamAddMessageEvent(event.data); break; case 'end': this.handleStreamEndEvent(event.data, accumulatedResponse); break; default: this.logger.warn(`Received unknown stream event type: ${event.event}`); } return accumulatedResponse; } /** * Handles a 'token' event from the stream. * Appends the token to the accumulated response and updates the UI. * @param data The data associated with the token event. * @param accumulatedResponse The response accumulated so far. * @returns The new accumulated response. */ handleStreamTokenEvent(data, accumulatedResponse) { const botMessageElement = this.ui.getBotMessageElement(); if (botMessageElement) { const textSpan = botMessageElement.querySelector('.message-text-content'); if (textSpan) { const parsedChunk = this.messageParser.parseChunk(data.chunk, accumulatedResponse); textSpan.innerHTML += parsedChunk; this.ui.scrollChatToBottom(); } else { this.logger.warn("Stream token: message-text-content span not found in bot message element. Cannot append token."); } } return accumulatedResponse + data.chunk; } /** * Handles an 'error' event from the stream. * Updates the UI to display the error. * @param data The data associated with the error event. */ handleStreamErrorEvent(data) { const currentBotElement = this.ui.getBotMessageElement(); if (currentBotElement) { const displayMessage = data.detail ? `${data.message}: ${data.detail}` : data.message; const parsedDisplayMessage = this.messageParser.parseComplete(displayMessage); this.ui.updateMessageContent(currentBotElement, parsedDisplayMessage); currentBotElement.classList.remove('thinking', 'bot-message'); currentBotElement.classList.add('error-message'); } else { // If no current bot element, add a new error message const parsedErrorMessage = this.messageParser.parseComplete(`Stream Error: ${data.message}${data.detail ? " Details: " + JSON.stringify(data.detail) : ""}`); this.ui.addMessage(this.config.errorSender, parsedErrorMessage, false, new Date().toISOString()); } } /** * Handles an 'add_message' event from the stream (e.g. for auxiliary messages). * Currently, it only logs the event. * @param data The data associated with the add_message event. */ handleStreamAddMessageEvent(data) { this.logger.debug("handleStreamAddMessageEvent: Received 'add_message' event. Full data:", { data }); const eventData = data; const isBotMessage = eventData.sender === "Machine"; if (!isBotMessage) { return; } let messageContent = undefined; if (typeof eventData.text === 'string') { messageContent = eventData.text; // Fallback for add_message content if 'text' field is not primary. /* istanbul ignore next */ } else if (typeof eventData.message === 'string') { messageContent = eventData.message; // Fallback for add_message content if 'text' or 'message' fields are not primary. /* istanbul ignore next */ } else if (typeof eventData.html === 'string') { messageContent = eventData.html; // Fallback for add_message content if 'text', 'message', or 'html' fields are not primary. /* istanbul ignore next */ } else if (typeof eventData.content === 'string') { messageContent = eventData.content; } if (messageContent === undefined || messageContent.trim() === "") { return; } const currentBotElement = this.ui.getBotMessageElement(); if (currentBotElement) { if (currentBotElement.classList.contains('thinking')) { this.ui.updateMessageContent(currentBotElement, ""); currentBotElement.classList.remove('thinking'); } const parsedMessage = this.messageParser.parseComplete(messageContent); const textSpan = currentBotElement.querySelector('.message-text-content'); if (textSpan) { textSpan.innerHTML = parsedMessage; } else { this.logger.warn("handleStreamAddMessageEvent: .message-text-content span not found. Updating currentBotElement directly."); this.ui.updateMessageContent(currentBotElement, parsedMessage); } this.ui.scrollChatToBottom(); } else { this.logger.warn("handleStreamAddMessageEvent: Bot message content found, but no currentBotElement to update. This is unusual if a thinking indicator was expected."); } } /** * Handles an 'end' event from the stream. * Finalizes the message content in the UI and updates the session ID. * @param data The data associated with the end event. * @param accumulatedResponse The total response accumulated from tokens. */ handleStreamEndEvent(data, accumulatedResponse) { const botElement = this.ui.getBotMessageElement(); if (botElement) { if (botElement.classList.contains('thinking') && data.flowResponse?.reply && accumulatedResponse === "") { const parsedReply = this.messageParser.parseComplete(data.flowResponse.reply); this.ui.updateMessageContent(botElement, parsedReply); botElement.classList.remove('thinking'); } else if (accumulatedResponse.length === 0 && !data.flowResponse?.reply) { } else if (data.flowResponse?.reply && accumulatedResponse !== data.flowResponse.reply) { if (accumulatedResponse.length === 0) { const parsedReply = this.messageParser.parseComplete(data.flowResponse.reply); this.ui.updateMessageContent(botElement, parsedReply); if (botElement.classList.contains('thinking')) { botElement.classList.remove('thinking'); } } } if (data.sessionId) { this.ui.updateSessionId(data.sessionId); } } else { this.logger.warn("handleStreamEndEvent: No bot message element found at stream end. This is unusual."); } } /** * Handles the message processing logic when streaming is disabled. * Sends the message, waits for a full response, and updates the UI. * @param messageText The user's message text. * @param sessionIdToSend The session ID to use for the request. */ async handleNonStreamingResponse(messageText, sessionIdToSend) { this.displayInitialThinkingIndicator(); try { const result = await this.chatClient.sendMessage(messageText, sessionIdToSend); const botElement = this.ui.getBotMessageElement(); if (botElement && botElement.classList.contains('thinking')) { if (result.reply) { const parsedReply = this.messageParser.parseComplete(result.reply); this.ui.updateMessageContent(botElement, parsedReply); } else if (result.error) { const errorMessage = result.detail ? `${result.error}: ${result.detail}` : result.error; const parsedErrorMessage = this.messageParser.parseComplete(errorMessage); this.ui.updateMessageContent(botElement, parsedErrorMessage); botElement.classList.remove('bot-message'); // It's an error, not a regular bot message botElement.classList.add('error-message'); } else { this.logger.warn("handleNonStreamingResponse: No reply content or error from bot."); const parsedNoResponse = this.messageParser.parseComplete("Sorry, I couldn't get a valid response."); this.ui.updateMessageContent(botElement, parsedNoResponse); } botElement.classList.remove('thinking'); } else { // Fallback if the thinking bubble was somehow lost or not set correctly this.logger.warn("handleNonStreamingResponse: Thinking message element was not found or not in expected state. Adding new message."); const parsedFallbackReply = this.messageParser.parseComplete(result.reply || "Sorry, I couldn't get a valid response."); this.ui.addMessage(this.config.botSender, parsedFallbackReply, false, new Date().toISOString()); } if (result.sessionId) { this.ui.updateSessionId(result.sessionId); } } catch (error) { this.logger.error("Failed to send message via ChatClient:", error); const exceptionMessage = error.message || "Failed to send message."; if (!this.tryUpdateThinkingToError("Error sending message", exceptionMessage)) { const parsedErrorMessage = this.messageParser.parseComplete(`Error: ${exceptionMessage}`); this.ui.addMessage(this.config.errorSender, parsedErrorMessage, false, new Date().toISOString()); } } finally { this.ui.setBotMessageElement(null); } } } exports.ChatMessageProcessor = ChatMessageProcessor;