LibreChat/client/src/hooks/Conversations/usePresets.ts
Danny Avila 1a452121fa
🤖 feat: OpenAI Assistants v2 (initial support) (#2781)
* 🤖 Assistants V2 Support: Part 1

- Separated Azure Assistants to its own endpoint
- File Search / Vector Store integration is incomplete, but can toggle and use storage from playground
- Code Interpreter resource files can be added but not deleted
- GPT-4o is supported
- Many improvements to the Assistants Endpoint overall

data-provider v2 changes

copy existing route as v1

chore: rename new endpoint to reduce comparison operations and add new azure filesource

api: add azureAssistants part 1

force use of version for assistants/assistantsAzure

chore: switch name back to azureAssistants

refactor type version: string | number

Ensure assistants endpoints have version set

fix: isArchived type issue in ConversationListParams

refactor: update assistants mutations/queries with endpoint/version definitions, update Assistants Map structure

chore:  FilePreview component ExtendedFile type assertion

feat: isAssistantsEndpoint helper

chore: remove unused useGenerations

chore(buildTree): type issue

chore(Advanced): type issue (unused component, maybe in future)

first pass for multi-assistant endpoint rewrite

fix(listAssistants): pass params correctly

feat: list separate assistants by endpoint

fix(useTextarea): access assistantMap correctly

fix: assistant endpoint switching, resetting ID

fix: broken during rewrite, selecting assistant mention

fix: set/invalidate assistants endpoint query data correctly

feat: Fix issue with assistant ID not being reset correctly

getOpenAIClient helper function

feat: add toast for assistant deletion

fix: assistants delete right after create issue for azure

fix: assistant patching

refactor: actions to use getOpenAIClient

refactor: consolidate logic into helpers file

fix: issue where conversation data was not initially available

v1 chat support

refactor(spendTokens): only early return if completionTokens isNaN

fix(OpenAIClient): ensure spendTokens has all necessary params

refactor: route/controller logic

fix(assistants/initializeClient): use defaultHeaders field

fix: sanitize default operation id

chore: bump openai package

first pass v2 action service

feat: retroactive domain parsing for actions added via v1

feat: delete db records of actions/assistants on openai assistant deletion

chore: remove vision tools from v2 assistants

feat: v2 upload and delete assistant vision images

WIP first pass, thread attachments

fix: show assistant vision files (save local/firebase copy)

v2 image continue

fix: annotations

fix: refine annotations

show analyze as error if is no longer submitting before progress reaches 1 and show file_search as retrieval tool

fix: abort run, undefined endpoint issue

refactor: consolidate capabilities logic and anticipate versioning

frontend version 2 changes

fix: query selection and filter

add endpoint to unknown filepath

add file ids to resource, deleting in progress

enable/disable file search

remove version log

* 🤖 Assistants V2 Support: Part 2

🎹 fix: Autocompletion Chrome Bug on Action API Key Input

chore: remove `useOriginNavigate`

chore: set correct OpenAI Storage Source

fix: azure file deletions, instantiate clients by source for deletion

update code interpret files info

feat: deleteResourceFileId

chore: increase poll interval as azure easily rate limits

fix: openai file deletions, TODO: evaluate rejected deletion settled promises to determine which to delete from db records

file source icons

update table file filters

chore: file search info and versioning

fix: retrieval update with necessary tool_resources if specified

fix(useMentions): add optional chaining in case listMap value is undefined

fix: force assistant avatar roundedness

fix: azure assistants, check correct flag

chore: bump data-provider

* fix: merge conflict

* ci: fix backend tests due to new updates

* chore: update .env.example

* meilisearch improvements

* localization updates

* chore: update comparisons

* feat: add additional metadata: endpoint, author ID

* chore: azureAssistants ENDPOINTS exclusion warning
2024-05-19 12:56:55 -04:00

253 lines
8.2 KiB
TypeScript

import filenamify from 'filenamify';
import exportFromJSON from 'export-from-json';
import { useCallback, useEffect, useRef } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { useRecoilState, useSetRecoilState, useRecoilValue } from 'recoil';
import { QueryKeys, modularEndpoints, isAssistantsEndpoint } from 'librechat-data-provider';
import { useCreatePresetMutation, useGetModelsQuery } from 'librechat-data-provider/react-query';
import type { TPreset, TEndpointsConfig } from 'librechat-data-provider';
import {
useUpdatePresetMutation,
useDeletePresetMutation,
useGetPresetsQuery,
} from '~/data-provider';
import { cleanupPreset, getEndpointField, removeUnavailableTools } from '~/utils';
import useDefaultConvo from '~/hooks/Conversations/useDefaultConvo';
import { useChatContext, useToastContext } from '~/Providers';
import { useAuthContext } from '~/hooks/AuthContext';
import { NotificationSeverity } from '~/common';
import useLocalize from '~/hooks/useLocalize';
import useNewConvo from '~/hooks/useNewConvo';
import store from '~/store';
export default function usePresets() {
const localize = useLocalize();
const hasLoaded = useRef(false);
const queryClient = useQueryClient();
const { showToast } = useToastContext();
const { user, isAuthenticated } = useAuthContext();
const modularChat = useRecoilValue(store.modularChat);
const availableTools = useRecoilValue(store.availableTools);
const setPresetModalVisible = useSetRecoilState(store.presetModalVisible);
const [_defaultPreset, setDefaultPreset] = useRecoilState(store.defaultPreset);
const presetsQuery = useGetPresetsQuery({ enabled: !!user && isAuthenticated });
const { preset, conversation, index, setPreset } = useChatContext();
const { data: modelsData } = useGetModelsQuery();
const { newConversation } = useNewConvo(index);
useEffect(() => {
if (modelsData?.initial) {
return;
}
const { data: presets } = presetsQuery;
if (_defaultPreset || !presets || hasLoaded.current) {
return;
}
if (presets && presets.length > 0 && user && presets[0].user !== user?.id) {
presetsQuery.refetch();
return;
}
const defaultPreset = presets.find((p) => p.defaultPreset);
if (!defaultPreset) {
hasLoaded.current = true;
return;
}
setDefaultPreset(defaultPreset);
if (!conversation?.conversationId || conversation.conversationId === 'new') {
newConversation({ preset: defaultPreset, modelsData });
}
hasLoaded.current = true;
// dependencies are stable and only needed once
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [presetsQuery.data, user, modelsData]);
const setPresets = useCallback(
(presets: TPreset[]) => {
queryClient.setQueryData<TPreset[]>([QueryKeys.presets], presets);
},
[queryClient],
);
const deletePresetsMutation = useDeletePresetMutation({
onMutate: (preset) => {
if (!preset) {
setPresets([]);
return;
}
const previousPresets = presetsQuery.data ?? [];
if (previousPresets) {
setPresets(previousPresets.filter((p) => p.presetId !== preset?.presetId));
}
},
onSuccess: () => {
queryClient.invalidateQueries([QueryKeys.presets]);
},
onError: (error) => {
queryClient.invalidateQueries([QueryKeys.presets]);
console.error('Error deleting the preset:', error);
showToast({
message: localize('com_endpoint_preset_delete_error'),
severity: NotificationSeverity.ERROR,
});
},
});
const createPresetMutation = useCreatePresetMutation();
const updatePreset = useUpdatePresetMutation({
onSuccess: (data, preset) => {
const toastTitle = data.title ? `"${data.title}"` : localize('com_endpoint_preset_title');
let message = `${toastTitle} ${localize('com_endpoint_preset_saved')}`;
if (data.defaultPreset && data.presetId !== _defaultPreset?.presetId) {
message = `${toastTitle} ${localize('com_endpoint_preset_default')}`;
setDefaultPreset(data);
newConversation({ preset: data });
} else if (preset?.defaultPreset === false) {
setDefaultPreset(null);
message = `${toastTitle} ${localize('com_endpoint_preset_default_removed')}`;
}
showToast({
message,
});
queryClient.invalidateQueries([QueryKeys.presets]);
},
onError: (error) => {
console.error('Error updating the preset:', error);
showToast({
message: localize('com_endpoint_preset_save_error'),
severity: NotificationSeverity.ERROR,
});
},
});
const getDefaultConversation = useDefaultConvo();
const { endpoint } = conversation ?? {};
const importPreset = (jsonPreset: TPreset) => {
createPresetMutation.mutate(
{ ...jsonPreset },
{
onSuccess: () => {
showToast({
message: localize('com_endpoint_preset_import'),
});
queryClient.invalidateQueries([QueryKeys.presets]);
},
onError: (error) => {
console.error('Error uploading the preset:', error);
showToast({
message: localize('com_endpoint_preset_import_error'),
severity: NotificationSeverity.ERROR,
});
},
},
);
};
const onFileSelected = (jsonData: Record<string, unknown>) => {
const jsonPreset = { ...cleanupPreset({ preset: jsonData }), presetId: null };
importPreset(jsonPreset);
};
const onSelectPreset = (_newPreset: TPreset) => {
if (!_newPreset) {
return;
}
const newPreset = removeUnavailableTools(_newPreset, availableTools);
const toastTitle = newPreset.title
? `"${newPreset.title}"`
: localize('com_endpoint_preset_title');
showToast({
message: `${toastTitle} ${localize('com_endpoint_preset_selected_title')}`,
showIcon: false,
duration: 750,
});
const endpointsConfig = queryClient.getQueryData<TEndpointsConfig>([QueryKeys.endpoints]);
const currentEndpointType = getEndpointField(endpointsConfig, endpoint, 'type');
const endpointType = getEndpointField(endpointsConfig, newPreset.endpoint, 'type');
const isAssistantSwitch =
isAssistantsEndpoint(newPreset.endpoint) &&
isAssistantsEndpoint(conversation?.endpoint) &&
conversation?.endpoint === newPreset.endpoint;
if (
(modularEndpoints.has(endpoint ?? '') ||
modularEndpoints.has(currentEndpointType ?? '') ||
isAssistantSwitch) &&
(modularEndpoints.has(newPreset?.endpoint ?? '') ||
modularEndpoints.has(endpointType ?? '') ||
isAssistantSwitch) &&
(endpoint === newPreset?.endpoint || modularChat || isAssistantSwitch)
) {
const currentConvo = getDefaultConversation({
/* target endpointType is necessary to avoid endpoint mixing */
conversation: { ...(conversation ?? {}), endpointType },
preset: { ...newPreset, endpointType },
});
/* We don't reset the latest message, only when changing settings mid-converstion */
newConversation({ template: currentConvo, preset: currentConvo, keepLatestMessage: true });
return;
}
newConversation({ preset: newPreset });
};
const onChangePreset = (preset: TPreset) => {
setPreset(preset);
setPresetModalVisible(true);
};
const clearAllPresets = () => deletePresetsMutation.mutate(undefined);
const onDeletePreset = (preset: TPreset) => {
if (!confirm(localize('com_endpoint_preset_delete_confirm'))) {
return;
}
deletePresetsMutation.mutate(preset);
};
const submitPreset = () => {
if (!preset) {
return;
}
updatePreset.mutate(cleanupPreset({ preset }));
};
const onSetDefaultPreset = (preset: TPreset, remove = false) => {
updatePreset.mutate({ ...preset, defaultPreset: !remove });
};
const exportPreset = () => {
if (!preset) {
return;
}
const fileName = filenamify(preset?.title || 'preset');
exportFromJSON({
data: cleanupPreset({ preset }),
fileName,
exportType: exportFromJSON.types.json,
});
};
return {
presetsQuery,
onSetDefaultPreset,
onFileSelected,
onSelectPreset,
onChangePreset,
clearAllPresets,
onDeletePreset,
submitPreset,
exportPreset,
};
}