From 388dc1789ba832766dc36688e50ea16671a0e73e Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Wed, 28 Feb 2024 15:15:45 -0500 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A0=EF=B8=8F=20fix:=20RunManager,=20As?= =?UTF-8?q?sistantService=20and=20useContentHandler=20Issues=20(#1920)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(useContentHandler): retain undefined parts and handle them within `ContentParts` rendering * fix(AssistantService/in_progress): skip empty messages * refactor(RunManager): create highly specific `seenSteps` Set keys for RunSteps with use of `getDetailsSignature` and `getToolCallSignature`,to ensure changes from polling are always captured --- api/server/services/AssistantService.js | 3 ++ api/server/services/Runs/RunManager.js | 54 ++++++++++++++++++- .../Chat/Messages/Content/ContentParts.tsx | 11 ++-- client/src/hooks/SSE/useContentHandler.ts | 2 - 4 files changed, 60 insertions(+), 10 deletions(-) diff --git a/api/server/services/AssistantService.js b/api/server/services/AssistantService.js index f9f613acf5..6ab14bad4f 100644 --- a/api/server/services/AssistantService.js +++ b/api/server/services/AssistantService.js @@ -286,6 +286,9 @@ function createInProgressHandler(openai, thread_id, messages) { openai.seenCompletedMessages.add(message_id); const message = await openai.beta.threads.messages.retrieve(thread_id, message_id); + if (!message?.content?.length) { + return; + } messages.push(message); let messageIndex = openai.mappedOrder.get(step.id); diff --git a/api/server/services/Runs/RunManager.js b/api/server/services/Runs/RunManager.js index b7f7400c3d..c8deeb9264 100644 --- a/api/server/services/Runs/RunManager.js +++ b/api/server/services/Runs/RunManager.js @@ -1,3 +1,4 @@ +const { ToolCallTypes } = require('librechat-data-provider'); const { logger } = require('~/config'); /** @@ -18,6 +19,53 @@ const { logger } = require('~/config'); * @property {Function} handleStep - Handles a run step based on its status. */ +/** + * Generates a signature string for a given tool call object. This signature includes + * the tool call's id, type, and other distinguishing features based on its type. + * + * @param {ToolCall} toolCall The tool call object for which to generate a signature. + * @returns {string} The generated signature for the tool call. + */ +function getToolCallSignature(toolCall) { + if (toolCall.type === ToolCallTypes.CODE_INTERPRETER) { + const inputLength = toolCall.code_interpreter?.input?.length ?? 0; + const outputsLength = toolCall.code_interpreter?.outputs?.length ?? 0; + return `${toolCall.id}-${toolCall.type}-${inputLength}-${outputsLength}`; + } + if (toolCall.type === ToolCallTypes.RETRIEVAL) { + return `${toolCall.id}-${toolCall.type}`; + } + if (toolCall.type === ToolCallTypes.FUNCTION) { + const argsLength = toolCall.function?.arguments?.length ?? 0; + const hasOutput = toolCall.function?.output ? 1 : 0; + return `${toolCall.id}-${toolCall.type}-${argsLength}-${hasOutput}`; + } + + return `${toolCall.id}-unknown-type`; +} + +/** + * Generates a signature based on the specifics of the step details. + * This function supports 'message_creation' and 'tool_calls' types, and returns a default signature + * for any other type or in case the details are undefined. + * + * @param {MessageCreationStepDetails | ToolCallsStepDetails | undefined} details - The detailed content of the step, which can be undefined. + * @returns {string} A signature string derived from the content of step details. + */ +function getDetailsSignature(details) { + if (!details) { + return 'undefined-details'; + } + + if (details.type === 'message_creation') { + return `${details.type}-${details.message_creation.message_id}`; + } else if (details.type === 'tool_calls') { + const toolCallsSignature = details.tool_calls.map(getToolCallSignature).join('|'); + return `${details.type}-${toolCallsSignature}`; + } + return 'unknown-type'; +} + /** * Manages the retrieval and processing of run steps based on run status. */ @@ -55,12 +103,14 @@ class RunManager { ); const steps = _steps.sort((a, b) => a.created_at - b.created_at); for (const [i, step] of steps.entries()) { - if (!final && this.seenSteps.has(`${step.id}-${step.status}`)) { + const detailsSignature = getDetailsSignature(step.step_details); + const stepKey = `${step.id}-${step.status}-${detailsSignature}`; + if (!final && this.seenSteps.has(stepKey)) { continue; } const isLast = i === steps.length - 1; - this.seenSteps.add(`${step.id}-${step.status}`); + this.seenSteps.add(stepKey); this.stepsByStatus[runStatus] = this.stepsByStatus[runStatus] || []; const currentStepPromise = (async () => { diff --git a/client/src/components/Chat/Messages/Content/ContentParts.tsx b/client/src/components/Chat/Messages/Content/ContentParts.tsx index d897d6f2c3..e436ea1a81 100644 --- a/client/src/components/Chat/Messages/Content/ContentParts.tsx +++ b/client/src/components/Chat/Messages/Content/ContentParts.tsx @@ -1,12 +1,10 @@ import { Suspense } from 'react'; -// import type { ContentPart } from 'librechat-data-provider'; +import type { TMessageContentParts } from 'librechat-data-provider'; import { UnfinishedMessage } from './MessageContent'; import { DelayedRender } from '~/components/ui'; import Part from './Part'; -// Content Component const ContentParts = ({ - edit, error, unfinished, isSubmitting, @@ -17,15 +15,16 @@ const ContentParts = ({ any) => { if (error) { // return ; - } else if (edit) { - // return ; } else { const { message } = props; const { messageId } = message; return ( <> - {content.map((part, idx) => { + {content.map((part: TMessageContentParts | undefined, idx: number) => { + if (!part) { + return null; + } return ( p !== undefined); - setMessages([...messages, response]); }; }