mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 08:50:15 +01:00
✨ feat: Assistants API, General File Support, Side Panel, File Explorer (#1696)
* feat: assistant name/icon in Landing & Header * feat: assistname in textarea placeholder, and use `Assistant` as default name * feat: display non-image files in user messages * fix: only render files if files.length is > 0 * refactor(config -> file-config): move file related configuration values to separate module, add excel types * chore: spreadsheet file rendering * fix(Landing): dark mode style for Assistant Name * refactor: move progress incrementing to own hook, start smaller, cap near limit \(1\) * refactor(useContentHandler): add empty Text part if last part was completed tool or image * chore: add accordion trigger border styling for dark mode * feat: Assistant Builder model selection * chore: use Spinner when Assistant is mutating * fix(get/assistants): return correct response object `AssistantListResponse` * refactor(Spinner): pass size as prop * refactor: make assistant crud mutations optimistic, add types for options * chore: remove assistants route and view * chore: move assistant builder components to separate directory * feat(ContextButton): delete Assistant via context button/dialog, add localization * refactor: conditionally show use and context menu buttons, add localization for create assistant * feat: save side panel states to localStorage * style(SidePanel): improve avatar menu and assistant select styling for dark mode * refactor: make NavToggle reusable for either side (left or right), add SidePanel Toggle with ability to close it completely * fix: resize handle and navToggle behavior * fix(/avatar/:assistant_id): await `deleteFile` and assign unique name to uploaded image * WIP: file UI components from PR #576 * refactor(OpenAIMinimalIcon): pass className * feat: formatDate helper fn * feat: DataTableColumnHeader * feat: add row selection, formatted row values, number of rows selected * WIP: add files to Side panel temporarily * feat: `LB_QueueAsyncCall`: Leaky Bucket queue for external APIs, use in `processDeleteRequest` * fix(TFile): correct `source` type with `FileSources` * fix(useFileHandling): use `continue` instead of return when iterating multiple files, add file type to extendedFile * chore: add generic setter type * refactor(processDeleteRequest): settle promises to prevent rejections from processing deletions, log errors * feat: `useFileDeletion` to reuse file deletion logic * refactor(useFileDeletion): make `setFiles` an optional param and use object as param * feat: useDeleteFilesFromTable * feat: use real `files` data and add deletion action to data table * fix(Table): make headers sticky * feat: add dynamic filtering for columns; only show to user Host or OpenAI storage type * style(DropdownMenu): replace `slate` with `gray` * style(DataTable): apply dark mode themes and other misc styling * style(Columns): add color to OpenAI Storage option * refactor(FileContainer): make file preview reusable * refactor(Images): make image preview reusable * refactor(FilePreview): make file prop optional for FileIcon and FilePreview, fix relative style * feat(Columns): add file/image previews, set a minimum size to show for file size in bytes * WIP: File Panel with real files and formatted * feat: open files dialog from panel * style: file data table mobile and general column styling fixes * refactor(api/files): return files sorted by the most recently updated * refactor: provide fileMap through context to prevent re-selecting files to map in different areas; remove unused imports commented out in PanelColumns * refactor(ExtendFile): make File type optional, add `attached` to prevent attached files from being deleted on remove, make Message.files a partial TFile type * feat: attach files through file panel * refactor(useFileHandling): move files to the start of cache list when uploaded * refactor(useDeleteFilesMutation): delete files from cache when successfully deleted from server * fix(FileRow): handle possible edge case of duplication due to attaching recently uploaded file * style(SidePanel): make resize grip border transparent, remove unnecessary styling on close sidepanel button * feat: action utilities and tests * refactor(actions): add `ValidationResult` type and change wording for no server URL found * refactor(actions): check for empty server URL * fix(data-provider): revert tsconfig to fix type issue resolution * feat(client): first pass of actions input for assistants * refactor(FunctionSignature): change method to output object instead of string * refactor(models/Assistant): add actions field to schema, use searchParams object for methods, and add `getAssistant` * feat: post actions input first pass - create new Action document - add actions to Assistant DB document - create /action/:assistant_id POST route - pass more props down from PanelSwitcher, derive assistant_id from switcher - move privacy policy to ActionInput - reset data on input change/validation - add `useUpdateAction` - conform FunctionSignature type to FunctionTool - add action, assistant doc, update hook related types * refactor: optimize assistant/actions relationship - past domain in metadata as hostname and not a URL - include domain in tool name - add `getActions` for actions retrieval by user - add `getAssistants` for assistant docs retrieval by user - add `assistant_id` to Action schema - move actions to own module as a subroute to `api/assistants` - add `useGetActionsQuery` and `useGetAssistantDocsQuery` hooks - fix Action type def * feat: show assistant actions in assistant builder * feat: switch to actions on action click, editing action styling * fix: add Assistant state for builder panel to allow immediate selection of newly created assistants as well as retaining the current assistant when switching to a different panel within the builder * refactor(SidePanel/NavToggle): offset less from right when SidePanel is completely collapsed * chore: rename `processActions` -> `processRequiredActions` * chore: rename Assistant API Action to RequiredAction * refactor(actions): avoid nesting actual API params under generic `requestBody` to optimize LLM token usage * fix(handleTools): avoid calling `validTool` if not defined, add optional param to skip the loading of specs, which throws an error in the context of assistants * WIP: working first pass of toolCalls generated from openapi specs * WIP: first pass ToolCall styling * feat: programmatic iv encryption/decryption helpers * fix: correct ActionAuth types/enums, and define type for AuthForm * feat: encryption/decryption helpers for Action AuthMetadata * refactor(getActions): remove sensitive fields from query response * refactor(POST/actions): encrypt and remove sensitive fields from mutation response * fix(ActionService): change ESM import to CJS * feat: frontend auth handling for actions + optimistic update on action update/creation * refactor(actions): use the correct variables and types for setAuth method * refactor: POST /:assistant_id action can now handle updating an existing action, add `saved_auth_fields` to determine when user explicitly saves new auth creds. only send auth metadata if user explicitly saved fields * refactor(createActionTool): catch errors and send back meaningful error message, add flag to `getActions` to determine whether to retrieve sensitive values or not * refactor(ToolService): add `action` property to ToolCall PartMetadata to determine if the tool call was an action, fix parsing function name issue with actionDelimiter * fix(ActionRequest): use URL class to correctly join endpoint parts for `execute` call * feat: delete assistant actions * refactor: conditionally show Available actions * refactor: show `retrieval` and `code_interpreter` as Capabilities, swap `Switch` for `Checkbox` * chore: remove shadow-stroke from messages * WIP: first pass of Assistants Knowledge attachments * refactor: remove AssistantsProvider in favor of FormProvider, fix selectedAssistant re-render bug, map Assistant file_ids to files via fileMap, initialize Knowledge component with mapped files if any exist * fix: prevent deleting files on assistant file upload * chore: remove console.log * refactor(useUploadFileMutation): update files and assistants cache on upload * chore: disable oauth option as not supported yet * feat: cancel assistant runs * refactor: initialize OpenAI client with helper function, resolve all related circular dependencies * fix(DALL-E): initialization * fix(process): openai client initialization * fix: select an existing Assistant when the active one is deleted * chore: allow attaching files for assistant endpoint, send back relevant OpenAI error message when uploading, deconstruct openAI initialization correctly, add `message_file` to formData when a file is attached to the message but not the assistant * fix: add assistant_id on newConvo * fix(initializeClient): import fix * chore: swap setAssistant for setOption in useEffect * fix(DALL-E): add processFileURL to loadTools call * chore: add customConfig to debug logs * feat: delete threads on convo delete * chore: replace Assistants icon * chore: remove console.dir() in `abortRun` * feat(AssistantService): accumulate text values from run in openai.responseText * feat: titling for assistants endpoint * chore: move panel file components to appropriate directory, add file checks for attaching files, change icon for Attach Files * refactor: add localizations to tools, plugins, add condition for adding/remove user plugins so tool selections don't affect this value * chore: disable `import from url` action for now * chore: remove textMimeTypes from default fileConfig for now * fix: catch tool errors and send as outputs with error messages * fix: React warning about button as descendant of button * style: retrieval and cancelled icon * WIP: pass isSubmitting to Parts, use InProgressCall to display cancelled tool calls correctly, show domain/function name * fix(meilisearch): fix `postSaveHook` issue where indexing expects a mongo document, and join all text content parts for meili indexing * ci: fix dall-e tests * ci: fix client tests * fix: button types in actions panel * fix: plugin auth form persisting across tool selections * fix(ci): update AppService spec with `loadAndFormatTools` * fix(clearConvos): add id check earlier on * refactor(AssistantAvatar): set previewURL dynamically when emtadata.avatar changes * feat(assistants): addTitle cache setting * fix(useSSE): resolve rebase conflicts * fix: delete mutation * style(SidePanel): make grip visible on active and hover, invisible otherwise * ci: add data-provider tests to workflow, also update eslint/tsconfig to recognize specs, and add `text/csv` to fileConfig * fix: handle edge case where auth object is undefined, and log errors * refactor(actions): resolve schemas, add tests for resolving refs, import specs from separate file for tests * chore: remove comment * fix(ActionsInput): re-render bug when initializing states with action fields * fix(patch/assistant): filter undefined tools * chore: add logging for errors in assistants routes * fix(updateAssistant): map actions to functions to avoid overwriting * fix(actions): properly handle GET paths * fix(convos): unhandled delete thread exception * refactor(AssistantService): pass both thread_id and conversationId when sending intermediate assistant messages, remove `mapMessagesToSteps` from AssistantService * refactor(useSSE): replace all messages with runMessages and pass latestMessageId to abortRun; fix(checkMessageGaps): include tool calls when syncing messages * refactor(assistants/chat): invoke `createOnTextProgress` after thread creation * chore: add typing * style: sidepanel styling * style: action tool call domain styling * feat(assistants): default models, limit retrieval to certain models, add env variables to to env.example * feat: assistants api key in EndpointService * refactor: set assistant model to conversation on assistant switch * refactor: set assistant model to conversation on assistant select from panel * fix(retrieveAndProcessFile): catch attempt to download file with `assistant` purpose which is not allowed; add logging * feat: retrieval styling, handling, and logging * chore: rename ASSISTANTS_REVERSE_PROXY to ASSISTANTS_BASE_URL * feat: FileContext for file metadata * feat: context file mgmt and filtering * style(Select): hover/rounded changes * refactor: explicit conversation switch, endpoint dependent, through `useSelectAssistant`, which does not create new chat if current endpoint is assistant endpoint * fix(AssistantAvatar): make empty previewURL if no avatar present * refactor: side panel mobile styling * style: merge tool and action section, optimize mobile styling for action/tool buttons * fix: localStorage issues * fix(useSelectAssistant): invoke react query hook directly in select hook as Map was not being updated in time * style: light mode fixes * fix: prevent sidepanel nav styling from shifting layout up * refactor: change default layout (collapsed by default) * style: mobile optimization of DataTable * style: datatable * feat: client-side hide right-side panel * chore(useNewConvo): add partial typing for preset * fix(useSelectAssistant): pass correct model name by using template as preset * WIP: assistant presets * refactor(ToolService): add native solution for `TavilySearchResults` and log tool output errors * refactor: organize imports and use native TavilySearchResults * fix(TavilySearchResults): stringify result * fix(ToolCall): show tool call outputs when not an action * chore: rename Prompt Prefix to custom instructions (in user facing text only) * refactor(EditPresetDialog): Optimize setting title by debouncing, reset preset on dialog close to avoid state mixture * feat: add `presetOverride` to overwrite active conversation settings when saving a Preset (relevant for client side updates only) * feat: Assistant preset settings (client-side) * fix(Switcher): only set assistant_id and model if current endpoint is Assistants * feat: use `useDebouncedInput` for updating conversation settings, starting with EditPresetDialog title setting and Assistant instructions setting * feat(Assistants): add instructions field to settings * feat(chat/assistants): pass conversation settings to run body * wip: begin localization and only allow actions if the assistant is created * refactor(AssistantsPanel): knowledge localization, allow tools on creation * feat: experimental: allow 'priming' values before assistant is created, that would normally require an assistant_id to be defined * chore: trim console logs and make more meaningful * chore: toast messages * fix(ci): date test * feat: create file when uploading Assistant Avatar * feat: file upload rate limiting from custom config with dynamic file route initialization * refactor: use file upload limiters on post routes only * refactor(fileConfig): add endpoints field for endpoint specific fileconfigs, add mergeConfig function, add tests * refactor: fileConfig route, dynamic multer instances used on all '/' and '/images' POST routes, data service and query hook * feat: supportedMimeTypesSchema, test for array of regex * feat: configurable file config limits * chore: clarify assistants file knowledge prereq. * chore(useTextarea): default to localized 'Assistant' if assistant name is empty * feat: configurable file limits and toggle file upload per endpoint * fix(useUploadFileMutation): prevent updating assistant.files cache if file upload is a message_file attachment * fix(AssistantSelect): set last selected assistant only when timeout successfully runs * refactor(queries): disable assistant queries if assistants endpoint is not enabled * chore(Switcher): add localization * chore: pluralize `assistant` for `EModelEndpoint key and value * feat: show/hide assistant UI components based on endpoint availability; librechat.yaml config for disabling builder section and setting polling/timeout intervals * fix(compactEndpointSchemas): use EModelEndpoint for schema access * feat(runAssistant): use configured values from `librechat.yaml` for `pollIntervalMs` and `timeout` * fix: naming issue * wip: revert landing * 🎉 happy birthday LibreChat (#1768) * happy birthday LibreChat * Refactor endpoint condition in Landing component * Update birthday message in Eng.tsx * fix(/config): avoid nesting ternaries * refactor(/config): check birthday --------- Co-authored-by: Danny Avila <messagedaniel@protonmail.com> * fix: landing * fix: landing * fix(useMessageHelpers): hardcoded check to use EModelEndpoint instead * fix(ci): convo test revert to main * fix(assistants/chat): fix issue where assistant_id was being saved as model for convo * chore: added logging, promises racing to prevent longer timeouts, explicit setting of maxRetries and timeouts, robust catching of invalid abortRun params * refactor: use recoil state for `showStopButton` and only show for assistants endpoint after syncing conversation data * refactor: optimize abortRun strategy using localStorage, refactor `abortConversation` to use async/await and await the result, refactor how the abortKey cache is set for runs * fix(checkMessageGaps): assign `assistant_id` to synced messages if defined; prevents UI from showing blank assistant for cancelled messages * refactor: re-order sequence of chat route, only allow aborting messages after run is created, cancel abortRun if there was a cancelling error (likely due already cancelled in chat route), and add extra logging * chore(typedefs): add httpAgent type to OpenAIClient * refactor: use custom implementation of retrieving run with axios to allow for timing out run query * fix(waitForRun): handle timed out run retrieval query * refactor: update preset conditions: - presets will retain settings when a different endpoint is selected; for existing convos, either when modular or is assistant switch - no longer use `navigateToConvo` on preset select * fix: temporary calculator hack as expects string input when invoked * fix: cancel abortRun only when cancelling error is a result of the run already being cancelled * chore: remove use of `fileMaxSizeMB` and total counterpart (redundant) * docs: custom config documentation update * docs: assistants api setup and dotenv, new custom config fields * refactor(Switcher): make Assistant switcher sticky in SidePanel * chore(useSSE): remove console log of data and message index * refactor(AssistantPanel): button styling and add secondary select button to bottom of panel * refactor(OpenAIClient): allow passing conversationId to RunManager through titleConvo and initializeLLM to properly record title context tokens used in cases where conversationId was not defined by the client * feat(assistants): token tracking for assistant runs * chore(spendTokens): improve logging * feat: support/exclude specific assistant Ids * chore: add update `librechat.example.yaml`, optimize `AppService` handling, new tests for `AppService`, optimize missing/outdate config logging * chore: mount docker logs to root of project * chore: condense axios errors * chore: bump vite * chore: vite hot reload fix using latest version * chore(getOpenAIModels): sort instruct models to the end of models list * fix(assistants): user provided key * fix(assistants): user provided key, invalidate more queries on revoke --------- Co-authored-by: Marco Beretta <81851188+Berry-13@users.noreply.github.com>
This commit is contained in:
parent
cd2786441a
commit
ecd63eb9f1
316 changed files with 21873 additions and 6315 deletions
572
client/src/hooks/SSE/useSSE.ts
Normal file
572
client/src/hooks/SSE/useSSE.ts
Normal file
|
|
@ -0,0 +1,572 @@
|
|||
import { v4 } from 'uuid';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
/* @ts-ignore */
|
||||
SSE,
|
||||
QueryKeys,
|
||||
EndpointURLs,
|
||||
Constants,
|
||||
createPayload,
|
||||
tPresetSchema,
|
||||
tMessageSchema,
|
||||
EModelEndpoint,
|
||||
tConvoUpdateSchema,
|
||||
removeNullishValues,
|
||||
} from 'librechat-data-provider';
|
||||
import { useGetUserBalance, useGetStartupConfig } from 'librechat-data-provider/react-query';
|
||||
import type {
|
||||
TResPlugin,
|
||||
TMessage,
|
||||
TConversation,
|
||||
TSubmission,
|
||||
ConversationData,
|
||||
} from 'librechat-data-provider';
|
||||
import { addConversation, deleteConversation, updateConversation } from '~/utils';
|
||||
import { useGenTitleMutation } from '~/data-provider';
|
||||
import useContentHandler from './useContentHandler';
|
||||
import { useAuthContext } from '../AuthContext';
|
||||
import useChatHelpers from '../useChatHelpers';
|
||||
import useSetStorage from '../useSetStorage';
|
||||
import store from '~/store';
|
||||
|
||||
type TResData = {
|
||||
plugin?: TResPlugin;
|
||||
final?: boolean;
|
||||
initial?: boolean;
|
||||
previousMessages?: TMessage[];
|
||||
requestMessage: TMessage;
|
||||
responseMessage: TMessage;
|
||||
conversation: TConversation;
|
||||
conversationId?: string;
|
||||
runMessages?: TMessage[];
|
||||
};
|
||||
|
||||
type TSyncData = {
|
||||
sync: boolean;
|
||||
thread_id: string;
|
||||
messages?: TMessage[];
|
||||
requestMessage: TMessage;
|
||||
responseMessage: TMessage;
|
||||
conversationId: string;
|
||||
};
|
||||
|
||||
export default function useSSE(submission: TSubmission | null, index = 0) {
|
||||
const setStorage = useSetStorage();
|
||||
const queryClient = useQueryClient();
|
||||
const genTitle = useGenTitleMutation();
|
||||
|
||||
const { conversationId: paramId } = useParams();
|
||||
const { token, isAuthenticated } = useAuthContext();
|
||||
const [completed, setCompleted] = useState(new Set());
|
||||
const setShowStopButton = useSetRecoilState(store.showStopButtonByIndex(index));
|
||||
|
||||
const {
|
||||
setMessages,
|
||||
getMessages,
|
||||
setConversation,
|
||||
setIsSubmitting,
|
||||
newConversation,
|
||||
resetLatestMessage,
|
||||
} = useChatHelpers(index, paramId);
|
||||
const contentHandler = useContentHandler({ setMessages, getMessages });
|
||||
|
||||
const { data: startupConfig } = useGetStartupConfig();
|
||||
const balanceQuery = useGetUserBalance({
|
||||
enabled: !!isAuthenticated && startupConfig?.checkBalance,
|
||||
});
|
||||
|
||||
const messageHandler = (data: string, submission: TSubmission) => {
|
||||
const {
|
||||
messages,
|
||||
message,
|
||||
plugin,
|
||||
plugins,
|
||||
initialResponse,
|
||||
isRegenerate = false,
|
||||
} = submission;
|
||||
|
||||
if (isRegenerate) {
|
||||
setMessages([
|
||||
...messages,
|
||||
{
|
||||
...initialResponse,
|
||||
text: data,
|
||||
parentMessageId: message?.overrideParentMessageId ?? null,
|
||||
messageId: message?.overrideParentMessageId + '_',
|
||||
plugin: plugin ?? null,
|
||||
plugins: plugins ?? [],
|
||||
// unfinished: true
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
setMessages([
|
||||
...messages,
|
||||
message,
|
||||
{
|
||||
...initialResponse,
|
||||
text: data,
|
||||
parentMessageId: message?.messageId,
|
||||
messageId: message?.messageId + '_',
|
||||
plugin: plugin ?? null,
|
||||
plugins: plugins ?? [],
|
||||
// unfinished: true
|
||||
},
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
const cancelHandler = (data: TResData, submission: TSubmission) => {
|
||||
const { requestMessage, responseMessage, conversation } = data;
|
||||
const { messages, isRegenerate = false } = submission;
|
||||
|
||||
const convoUpdate = conversation ?? submission.conversation;
|
||||
|
||||
// update the messages
|
||||
if (isRegenerate) {
|
||||
const messagesUpdate = [...messages, responseMessage].filter((msg) => msg);
|
||||
setMessages(messagesUpdate);
|
||||
} else {
|
||||
const messagesUpdate = [...messages, requestMessage, responseMessage].filter((msg) => msg);
|
||||
setMessages(messagesUpdate);
|
||||
}
|
||||
|
||||
const isNewConvo = conversation.conversationId !== submission.conversation.conversationId;
|
||||
if (isNewConvo) {
|
||||
queryClient.setQueryData<ConversationData>([QueryKeys.allConversations], (convoData) => {
|
||||
if (!convoData) {
|
||||
return convoData;
|
||||
}
|
||||
return deleteConversation(convoData, submission.conversation.conversationId as string);
|
||||
});
|
||||
}
|
||||
|
||||
// refresh title
|
||||
if (isNewConvo && requestMessage?.parentMessageId === Constants.NO_PARENT) {
|
||||
setTimeout(() => {
|
||||
genTitle.mutate({ conversationId: convoUpdate.conversationId as string });
|
||||
}, 2500);
|
||||
}
|
||||
|
||||
setConversation((prevState) => {
|
||||
const update = {
|
||||
...prevState,
|
||||
...convoUpdate,
|
||||
};
|
||||
|
||||
setStorage(update);
|
||||
return update;
|
||||
});
|
||||
|
||||
setIsSubmitting(false);
|
||||
};
|
||||
|
||||
const syncHandler = (data: TSyncData, submission: TSubmission) => {
|
||||
const { conversationId, thread_id, responseMessage, requestMessage } = data;
|
||||
const { initialResponse, messages: _messages, message } = submission;
|
||||
|
||||
const messages = _messages.filter((msg) => msg.messageId !== message.messageId);
|
||||
|
||||
setMessages([
|
||||
...messages,
|
||||
requestMessage,
|
||||
{
|
||||
...initialResponse,
|
||||
...responseMessage,
|
||||
},
|
||||
]);
|
||||
|
||||
let update = {} as TConversation;
|
||||
setConversation((prevState) => {
|
||||
update = tConvoUpdateSchema.parse({
|
||||
...prevState,
|
||||
conversationId,
|
||||
thread_id,
|
||||
messages: [requestMessage.messageId, responseMessage.messageId],
|
||||
}) as TConversation;
|
||||
|
||||
setStorage(update);
|
||||
return update;
|
||||
});
|
||||
|
||||
queryClient.setQueryData<ConversationData>([QueryKeys.allConversations], (convoData) => {
|
||||
if (!convoData) {
|
||||
return convoData;
|
||||
}
|
||||
if (requestMessage.parentMessageId === Constants.NO_PARENT) {
|
||||
return addConversation(convoData, update);
|
||||
} else {
|
||||
return updateConversation(convoData, update);
|
||||
}
|
||||
});
|
||||
|
||||
setShowStopButton(true);
|
||||
|
||||
resetLatestMessage();
|
||||
};
|
||||
|
||||
const createdHandler = (data: TResData, submission: TSubmission) => {
|
||||
const { messages, message, initialResponse, isRegenerate = false } = submission;
|
||||
|
||||
if (isRegenerate) {
|
||||
setMessages([
|
||||
...messages,
|
||||
{
|
||||
...initialResponse,
|
||||
parentMessageId: message?.overrideParentMessageId ?? null,
|
||||
messageId: message?.overrideParentMessageId + '_',
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
setMessages([
|
||||
...messages,
|
||||
message,
|
||||
{
|
||||
...initialResponse,
|
||||
parentMessageId: message?.messageId,
|
||||
messageId: message?.messageId + '_',
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
const { conversationId } = message;
|
||||
|
||||
let update = {} as TConversation;
|
||||
setConversation((prevState) => {
|
||||
update = tConvoUpdateSchema.parse({
|
||||
...prevState,
|
||||
conversationId,
|
||||
}) as TConversation;
|
||||
|
||||
setStorage(update);
|
||||
return update;
|
||||
});
|
||||
|
||||
queryClient.setQueryData<ConversationData>([QueryKeys.allConversations], (convoData) => {
|
||||
if (!convoData) {
|
||||
return convoData;
|
||||
}
|
||||
if (message.parentMessageId === Constants.NO_PARENT) {
|
||||
return addConversation(convoData, update);
|
||||
} else {
|
||||
return updateConversation(convoData, update);
|
||||
}
|
||||
});
|
||||
resetLatestMessage();
|
||||
};
|
||||
|
||||
const finalHandler = (data: TResData, submission: TSubmission) => {
|
||||
const { requestMessage, responseMessage, conversation, runMessages } = data;
|
||||
const { messages, conversation: submissionConvo, isRegenerate = false } = submission;
|
||||
|
||||
setShowStopButton(false);
|
||||
setCompleted((prev) => new Set(prev.add(submission?.initialResponse?.messageId)));
|
||||
|
||||
// update the messages; if assistants endpoint, client doesn't receive responseMessage
|
||||
if (runMessages) {
|
||||
setMessages([...runMessages]);
|
||||
} else if (isRegenerate && responseMessage) {
|
||||
setMessages([...messages, responseMessage]);
|
||||
} else if (responseMessage) {
|
||||
setMessages([...messages, requestMessage, responseMessage]);
|
||||
}
|
||||
|
||||
const isNewConvo = conversation.conversationId !== submissionConvo.conversationId;
|
||||
if (isNewConvo) {
|
||||
queryClient.setQueryData<ConversationData>([QueryKeys.allConversations], (convoData) => {
|
||||
if (!convoData) {
|
||||
return convoData;
|
||||
}
|
||||
return deleteConversation(convoData, submissionConvo.conversationId as string);
|
||||
});
|
||||
}
|
||||
|
||||
// refresh title
|
||||
if (isNewConvo && requestMessage && requestMessage.parentMessageId === Constants.NO_PARENT) {
|
||||
setTimeout(() => {
|
||||
genTitle.mutate({ conversationId: conversation.conversationId as string });
|
||||
}, 2500);
|
||||
}
|
||||
|
||||
setConversation((prevState) => {
|
||||
const update = {
|
||||
...prevState,
|
||||
...conversation,
|
||||
};
|
||||
|
||||
// Revert to previous model if the model was auto-switched by backend due to message attachments
|
||||
if (conversation.model?.includes('vision') && !submissionConvo.model?.includes('vision')) {
|
||||
update.model = submissionConvo?.model;
|
||||
}
|
||||
|
||||
setStorage(update);
|
||||
return update;
|
||||
});
|
||||
|
||||
setIsSubmitting(false);
|
||||
};
|
||||
|
||||
const errorHandler = ({ data, submission }: { data?: TResData; submission: TSubmission }) => {
|
||||
const { messages, message, initialResponse } = submission;
|
||||
|
||||
setCompleted((prev) => new Set(prev.add(initialResponse.messageId)));
|
||||
|
||||
const conversationId = message?.conversationId ?? submission?.conversationId;
|
||||
|
||||
const parseErrorResponse = (data: TResData | Partial<TMessage>) => {
|
||||
const metadata = data['responseMessage'] ?? data;
|
||||
const errorMessage = {
|
||||
...initialResponse,
|
||||
...metadata,
|
||||
error: true,
|
||||
parentMessageId: message?.messageId,
|
||||
};
|
||||
|
||||
if (!errorMessage.messageId) {
|
||||
errorMessage.messageId = v4();
|
||||
}
|
||||
|
||||
return tMessageSchema.parse(errorMessage);
|
||||
};
|
||||
|
||||
if (!data) {
|
||||
const convoId = conversationId ?? v4();
|
||||
const errorResponse = parseErrorResponse({
|
||||
text: 'Error connecting to server',
|
||||
...submission,
|
||||
conversationId: convoId,
|
||||
});
|
||||
setMessages([...messages, message, errorResponse]);
|
||||
newConversation({
|
||||
template: { conversationId: convoId },
|
||||
preset: tPresetSchema.parse(submission?.conversation),
|
||||
});
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!conversationId && !data.conversationId) {
|
||||
const convoId = v4();
|
||||
const errorResponse = parseErrorResponse(data);
|
||||
setMessages([...messages, message, errorResponse]);
|
||||
newConversation({
|
||||
template: { conversationId: convoId },
|
||||
preset: tPresetSchema.parse(submission?.conversation),
|
||||
});
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Error:', data);
|
||||
const errorResponse = tMessageSchema.parse({
|
||||
...data,
|
||||
error: true,
|
||||
parentMessageId: message?.messageId,
|
||||
});
|
||||
|
||||
setMessages([...messages, message, errorResponse]);
|
||||
if (data.conversationId && paramId === 'new') {
|
||||
newConversation({
|
||||
template: { conversationId: data.conversationId },
|
||||
preset: tPresetSchema.parse(submission?.conversation),
|
||||
});
|
||||
}
|
||||
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
};
|
||||
|
||||
const abortConversation = async (conversationId = '', submission: TSubmission) => {
|
||||
console.log(submission);
|
||||
let runAbortKey = '';
|
||||
try {
|
||||
const conversation = (JSON.parse(localStorage.getItem('lastConversationSetup') ?? '') ??
|
||||
{}) as TConversation;
|
||||
const { conversationId, messages } = conversation;
|
||||
runAbortKey = `${conversationId}:${messages?.[messages.length - 1]}`;
|
||||
} catch (error) {
|
||||
console.error('Error getting last conversation setup');
|
||||
console.error(error);
|
||||
}
|
||||
const { endpoint: _endpoint, endpointType } = submission?.conversation || {};
|
||||
const endpoint = endpointType ?? _endpoint;
|
||||
try {
|
||||
const response = await fetch(`${EndpointURLs[endpoint ?? '']}/abort`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
abortKey: _endpoint === EModelEndpoint.assistants ? runAbortKey : conversationId,
|
||||
endpoint,
|
||||
}),
|
||||
});
|
||||
|
||||
// Check if the response is JSON
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (contentType && contentType.includes('application/json')) {
|
||||
const data = await response.json();
|
||||
console.log('aborted', data);
|
||||
if (response.status === 404) {
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
}
|
||||
if (data.final) {
|
||||
finalHandler(data, submission);
|
||||
} else {
|
||||
cancelHandler(data, submission);
|
||||
}
|
||||
} else if (response.status === 204) {
|
||||
const responseMessage = {
|
||||
...submission.initialResponse,
|
||||
};
|
||||
|
||||
const data = {
|
||||
requestMessage: submission.message,
|
||||
responseMessage: responseMessage,
|
||||
conversation: submission.conversation,
|
||||
};
|
||||
console.log('aborted', data);
|
||||
} else {
|
||||
throw new Error(
|
||||
'Unexpected response from server; Status: ' + response.status + ' ' + response.statusText,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error cancelling request');
|
||||
console.error(error);
|
||||
const convoId = conversationId ?? v4();
|
||||
const text =
|
||||
submission.initialResponse?.text?.length > 45 ? submission.initialResponse?.text : '';
|
||||
const errorMessage = {
|
||||
...submission,
|
||||
...submission.initialResponse,
|
||||
text: text ?? (error as Error).message ?? 'Error cancelling request',
|
||||
unfinished: !!text.length,
|
||||
error: true,
|
||||
};
|
||||
const errorResponse = tMessageSchema.parse(errorMessage);
|
||||
setMessages([...submission.messages, submission.message, errorResponse]);
|
||||
newConversation({
|
||||
template: { conversationId: convoId },
|
||||
preset: tPresetSchema.parse(submission?.conversation),
|
||||
});
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (submission === null) {
|
||||
return;
|
||||
}
|
||||
if (Object.keys(submission).length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let { message } = submission;
|
||||
|
||||
const payloadData = createPayload(submission);
|
||||
let { payload } = payloadData;
|
||||
if (payload.endpoint === EModelEndpoint.assistants) {
|
||||
payload = removeNullishValues(payload);
|
||||
}
|
||||
|
||||
let textIndex = null;
|
||||
|
||||
const events = new SSE(payloadData.server, {
|
||||
payload: JSON.stringify(payload),
|
||||
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
events.onmessage = (e: MessageEvent) => {
|
||||
const data = JSON.parse(e.data);
|
||||
|
||||
if (data.final) {
|
||||
const { plugins } = data;
|
||||
finalHandler(data, { ...submission, plugins, message });
|
||||
startupConfig?.checkBalance && balanceQuery.refetch();
|
||||
console.log('final', data);
|
||||
}
|
||||
if (data.created) {
|
||||
message = {
|
||||
...message,
|
||||
...data.message,
|
||||
overrideParentMessageId: message?.overrideParentMessageId,
|
||||
};
|
||||
createdHandler(data, { ...submission, message });
|
||||
} else if (data.sync) {
|
||||
/* synchronize messages to Assistants API as well as with real DB ID's */
|
||||
syncHandler(data, { ...submission, message });
|
||||
} else if (data.type) {
|
||||
const { text, index } = data;
|
||||
if (text && index !== textIndex) {
|
||||
textIndex = index;
|
||||
}
|
||||
|
||||
contentHandler({ data, submission });
|
||||
} else {
|
||||
const text = data.text || data.response;
|
||||
const { plugin, plugins } = data;
|
||||
|
||||
if (data.message) {
|
||||
messageHandler(text, { ...submission, plugin, plugins, message });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
events.onopen = () => console.log('connection is opened');
|
||||
|
||||
events.oncancel = async () => {
|
||||
const streamKey = submission?.initialResponse?.messageId;
|
||||
if (completed.has(streamKey)) {
|
||||
setIsSubmitting(false);
|
||||
setCompleted((prev) => {
|
||||
prev.delete(streamKey);
|
||||
return new Set(prev);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setCompleted((prev) => new Set(prev.add(streamKey)));
|
||||
return await abortConversation(
|
||||
message?.conversationId ?? submission?.conversationId,
|
||||
submission,
|
||||
);
|
||||
};
|
||||
|
||||
events.onerror = function (e: MessageEvent) {
|
||||
console.log('error in server stream.');
|
||||
startupConfig?.checkBalance && balanceQuery.refetch();
|
||||
events.close();
|
||||
|
||||
let data: TResData | undefined = undefined;
|
||||
try {
|
||||
data = JSON.parse(e.data) as TResData;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
console.log(e);
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
errorHandler({ data, submission: { ...submission, message } });
|
||||
};
|
||||
|
||||
setIsSubmitting(true);
|
||||
events.stream();
|
||||
|
||||
return () => {
|
||||
const isCancelled = events.readyState <= 1;
|
||||
events.close();
|
||||
// setSource(null);
|
||||
if (isCancelled) {
|
||||
const e = new Event('cancel');
|
||||
events.dispatchEvent(e);
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [submission]);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue