LibreChat/client/src/data-provider/mutations.ts
Danny Avila 5cd5c3bef8
🅰️ feat: Azure OpenAI Assistants API Support (#1992)
* chore: rename dir from `assistant` to plural

* feat: `assistants` field for azure config, spread options in AppService

* refactor: rename constructAzureURL param for azure as `azureOptions`

* chore: bump openai and bun

* chore(loadDefaultModels): change naming of assistant -> assistants

* feat: load azure settings with currect baseURL for assistants' initializeClient

* refactor: add `assistants` flags to groups and model configs, add mapGroupToAzureConfig

* feat(loadConfigEndpoints): initialize assistants endpoint if azure flag `assistants` is enabled

* feat(AppService): determine assistant models on startup, throw Error if none

* refactor(useDeleteAssistantMutation): send model along with assistant id for delete mutations

* feat: support listing and deleting assistants with azure

* feat: add model query to assistant avatar upload

* feat: add azure support for retrieveRun method

* refactor: update OpenAIClient initialization

* chore: update README

* fix(ci): tests passing

* refactor(uploadOpenAIFile): improve logging and use more efficient REST API method

* refactor(useFileHandling): add model to metadata to target Azure region compatible with current model

* chore(files): add azure naming pattern for valid file id recognition

* fix(assistants): initialize openai with first available assistant model if none provided

* refactor(uploadOpenAIFile): add content type for azure, initialize formdata before azure options

* refactor(sleep): move sleep function out of Runs and into `~/server/utils`

* fix(azureOpenAI/assistants): make sure to only overwrite models with assistant models if `assistants` flag is enabled

* refactor(uploadOpenAIFile): revert to old method

* chore(uploadOpenAIFile): use enum for file purpose

* docs: azureOpenAI update guide with more info, examples

* feat: enable/disable assistant capabilities and specify retrieval models

* refactor: optional chain conditional statement in loadConfigModels.js

* docs: add assistants examples

* chore: update librechat.example.yaml

* docs(azure): update note of file upload behavior in Azure OpenAI Assistants

* chore: update docs and add descriptive message about assistant errors

* fix: prevent message submission with invalid assistant or if files loading

* style: update Landing icon & text when assistant is not selected

* chore: bump librechat-data-provider to 0.4.8

* fix(assistants/azure): assign req.body.model for proper azure init to abort runs
2024-03-14 17:21:42 -04:00

532 lines
16 KiB
TypeScript

import { useMutation, useQueryClient } from '@tanstack/react-query';
import type { UseMutationResult } from '@tanstack/react-query';
import type t from 'librechat-data-provider';
import type {
TFile,
BatchFile,
TFileUpload,
AssistantListResponse,
UploadMutationOptions,
DeleteFilesResponse,
DeleteFilesBody,
DeleteMutationOptions,
UpdatePresetOptions,
DeletePresetOptions,
PresetDeleteResponse,
LogoutOptions,
TPreset,
UploadAvatarOptions,
AvatarUploadResponse,
TConversation,
Assistant,
AssistantCreateParams,
AssistantUpdateParams,
UploadAssistantAvatarOptions,
AssistantAvatarVariables,
CreateAssistantMutationOptions,
UpdateAssistantMutationOptions,
DeleteAssistantMutationOptions,
DeleteAssistantBody,
DeleteConversationOptions,
UpdateActionOptions,
UpdateActionVariables,
UpdateActionResponse,
DeleteActionOptions,
DeleteActionVariables,
Action,
} from 'librechat-data-provider';
import { dataService, MutationKeys, QueryKeys, defaultOrderQuery } from 'librechat-data-provider';
import { updateConversation, deleteConversation, updateConvoFields } from '~/utils';
import { useSetRecoilState } from 'recoil';
import store from '~/store';
/** Conversations */
export const useGenTitleMutation = (): UseMutationResult<
t.TGenTitleResponse,
unknown,
t.TGenTitleRequest,
unknown
> => {
const queryClient = useQueryClient();
return useMutation((payload: t.TGenTitleRequest) => dataService.genTitle(payload), {
onSuccess: (response, vars) => {
queryClient.setQueryData(
[QueryKeys.conversation, vars.conversationId],
(convo: TConversation | undefined) => {
if (!convo) {
return convo;
}
return { ...convo, title: response.title };
},
);
queryClient.setQueryData<t.ConversationData>([QueryKeys.allConversations], (convoData) => {
if (!convoData) {
return convoData;
}
return updateConvoFields(convoData, {
conversationId: vars.conversationId,
title: response.title,
} as TConversation);
});
},
});
};
export const useUpdateConversationMutation = (
id: string,
): UseMutationResult<
t.TUpdateConversationResponse,
unknown,
t.TUpdateConversationRequest,
unknown
> => {
const queryClient = useQueryClient();
return useMutation(
(payload: t.TUpdateConversationRequest) => dataService.updateConversation(payload),
{
onSuccess: (updatedConvo) => {
queryClient.setQueryData([QueryKeys.conversation, id], updatedConvo);
queryClient.setQueryData<t.ConversationData>([QueryKeys.allConversations], (convoData) => {
if (!convoData) {
return convoData;
}
return updateConversation(convoData, updatedConvo);
});
},
},
);
};
export const useDeleteConversationMutation = (
options?: DeleteConversationOptions,
): UseMutationResult<
t.TDeleteConversationResponse,
unknown,
t.TDeleteConversationRequest,
unknown
> => {
const queryClient = useQueryClient();
const { onSuccess, ..._options } = options || {};
return useMutation(
(payload: t.TDeleteConversationRequest) => dataService.deleteConversation(payload),
{
onSuccess: (_data, vars, context) => {
if (!vars.conversationId) {
return;
}
queryClient.setQueryData([QueryKeys.conversation, vars.conversationId], null);
queryClient.setQueryData<t.ConversationData>([QueryKeys.allConversations], (convoData) => {
if (!convoData) {
return convoData;
}
const update = deleteConversation(convoData, vars.conversationId as string);
return update;
});
onSuccess?.(_data, vars, context);
},
...(_options || {}),
},
);
};
export const useUploadFileMutation = (
_options?: UploadMutationOptions,
): UseMutationResult<
TFileUpload, // response data
unknown, // error
FormData, // request
unknown // context
> => {
const queryClient = useQueryClient();
const { onSuccess, ...options } = _options || {};
return useMutation([MutationKeys.fileUpload], {
mutationFn: (body: FormData) => {
const height = body.get('height');
const width = body.get('width');
if (height && width) {
return dataService.uploadImage(body);
}
return dataService.uploadFile(body);
},
...(options || {}),
onSuccess: (data, formData, context) => {
queryClient.setQueryData<TFile[] | undefined>([QueryKeys.files], (_files) => [
data,
...(_files ?? []),
]);
const assistant_id = formData.get('assistant_id');
const message_file = formData.get('message_file');
if (!assistant_id || message_file === 'true') {
onSuccess?.(data, formData, context);
return;
}
queryClient.setQueryData<AssistantListResponse>(
[QueryKeys.assistants, defaultOrderQuery],
(prev) => {
if (!prev) {
return prev;
}
return {
...prev,
data: prev?.data.map((assistant) => {
if (assistant.id === assistant_id) {
return {
...assistant,
file_ids: [...assistant.file_ids, data.file_id],
};
}
return assistant;
}),
};
},
);
onSuccess?.(data, formData, context);
},
});
};
export const useDeleteFilesMutation = (
_options?: DeleteMutationOptions,
): UseMutationResult<
DeleteFilesResponse, // response data
unknown, // error
DeleteFilesBody, // request
unknown // context
> => {
const queryClient = useQueryClient();
const { onSuccess, ...options } = _options || {};
return useMutation([MutationKeys.fileDelete], {
mutationFn: (body: DeleteFilesBody) => dataService.deleteFiles(body.files, body.assistant_id),
...(options || {}),
onSuccess: (data, ...args) => {
queryClient.setQueryData<TFile[] | undefined>([QueryKeys.files], (cachefiles) => {
const { files: filesDeleted } = args[0];
const fileMap = filesDeleted.reduce((acc, file) => {
acc.set(file.file_id, file);
return acc;
}, new Map<string, BatchFile>());
return (cachefiles ?? []).filter((file) => !fileMap.has(file.file_id));
});
onSuccess?.(data, ...args);
},
});
};
export const useUpdatePresetMutation = (
options?: UpdatePresetOptions,
): UseMutationResult<
TPreset, // response data
unknown,
TPreset,
unknown
> => {
return useMutation([MutationKeys.updatePreset], {
mutationFn: (preset: TPreset) => dataService.updatePreset(preset),
...(options || {}),
});
};
export const useDeletePresetMutation = (
options?: DeletePresetOptions,
): UseMutationResult<
PresetDeleteResponse, // response data
unknown,
TPreset | undefined,
unknown
> => {
return useMutation([MutationKeys.deletePreset], {
mutationFn: (preset: TPreset | undefined) => dataService.deletePreset(preset),
...(options || {}),
});
};
/* login/logout */
export const useLogoutUserMutation = (
options?: LogoutOptions,
): UseMutationResult<unknown, unknown, undefined, unknown> => {
const queryClient = useQueryClient();
const setDefaultPreset = useSetRecoilState(store.defaultPreset);
return useMutation([MutationKeys.logoutUser], {
mutationFn: () => dataService.logout(),
...(options || {}),
onSuccess: (...args) => {
options?.onSuccess?.(...args);
},
onMutate: (...args) => {
setDefaultPreset(null);
queryClient.removeQueries();
localStorage.removeItem('lastConversationSetup');
localStorage.removeItem('lastSelectedModel');
localStorage.removeItem('lastSelectedTools');
localStorage.removeItem('filesToDelete');
// localStorage.removeItem('lastAssistant');
options?.onMutate?.(...args);
},
});
};
/* Avatar upload */
export const useUploadAvatarMutation = (
options?: UploadAvatarOptions,
): UseMutationResult<
AvatarUploadResponse, // response data
unknown, // error
FormData, // request
unknown // context
> => {
return useMutation([MutationKeys.avatarUpload], {
mutationFn: (variables: FormData) => dataService.uploadAvatar(variables),
...(options || {}),
});
};
/**
* ASSISTANTS
*/
/**
* Create a new assistant
*/
export const useCreateAssistantMutation = (
options?: CreateAssistantMutationOptions,
): UseMutationResult<Assistant, Error, AssistantCreateParams> => {
const queryClient = useQueryClient();
return useMutation(
(newAssistantData: AssistantCreateParams) => dataService.createAssistant(newAssistantData),
{
onMutate: (variables) => options?.onMutate?.(variables),
onError: (error, variables, context) => options?.onError?.(error, variables, context),
onSuccess: (newAssistant, variables, context) => {
const listRes = queryClient.getQueryData<AssistantListResponse>([
QueryKeys.assistants,
defaultOrderQuery,
]);
if (!listRes) {
return options?.onSuccess?.(newAssistant, variables, context);
}
const currentAssistants = listRes.data;
currentAssistants.push(newAssistant);
queryClient.setQueryData<AssistantListResponse>([QueryKeys.assistants, defaultOrderQuery], {
...listRes,
data: currentAssistants,
});
return options?.onSuccess?.(newAssistant, variables, context);
},
},
);
};
/**
* Hook for updating an assistant
*/
export const useUpdateAssistantMutation = (
options?: UpdateAssistantMutationOptions,
): UseMutationResult<Assistant, Error, { assistant_id: string; data: AssistantUpdateParams }> => {
const queryClient = useQueryClient();
return useMutation(
({ assistant_id, data }: { assistant_id: string; data: AssistantUpdateParams }) =>
dataService.updateAssistant(assistant_id, data),
{
onMutate: (variables) => options?.onMutate?.(variables),
onError: (error, variables, context) => options?.onError?.(error, variables, context),
onSuccess: (updatedAssistant, variables, context) => {
const listRes = queryClient.getQueryData<AssistantListResponse>([
QueryKeys.assistants,
defaultOrderQuery,
]);
if (!listRes) {
return options?.onSuccess?.(updatedAssistant, variables, context);
}
queryClient.setQueryData<AssistantListResponse>([QueryKeys.assistants, defaultOrderQuery], {
...listRes,
data: listRes.data.map((assistant) => {
if (assistant.id === variables.assistant_id) {
return updatedAssistant;
}
return assistant;
}),
});
return options?.onSuccess?.(updatedAssistant, variables, context);
},
},
);
};
/**
* Hook for deleting an assistant
*/
export const useDeleteAssistantMutation = (
options?: DeleteAssistantMutationOptions,
): UseMutationResult<void, Error, DeleteAssistantBody> => {
const queryClient = useQueryClient();
return useMutation(
({ assistant_id, model }: DeleteAssistantBody) =>
dataService.deleteAssistant(assistant_id, model),
{
onMutate: (variables) => options?.onMutate?.(variables),
onError: (error, variables, context) => options?.onError?.(error, variables, context),
onSuccess: (_data, variables, context) => {
const listRes = queryClient.getQueryData<AssistantListResponse>([
QueryKeys.assistants,
defaultOrderQuery,
]);
if (!listRes) {
return options?.onSuccess?.(_data, variables, context);
}
const data = listRes.data.filter((assistant) => assistant.id !== variables.assistant_id);
queryClient.setQueryData<AssistantListResponse>([QueryKeys.assistants, defaultOrderQuery], {
...listRes,
data,
});
return options?.onSuccess?.(_data, variables, data);
},
},
);
};
/**
* Hook for uploading an assistant avatar
*/
export const useUploadAssistantAvatarMutation = (
options?: UploadAssistantAvatarOptions,
): UseMutationResult<
Assistant, // response data
unknown, // error
AssistantAvatarVariables, // request
unknown // context
> => {
return useMutation([MutationKeys.assistantAvatarUpload], {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
mutationFn: ({ postCreation, ...variables }: AssistantAvatarVariables) =>
dataService.uploadAssistantAvatar(variables),
...(options || {}),
});
};
/**
* Hook for updating Assistant Actions
*/
export const useUpdateAction = (
options?: UpdateActionOptions,
): UseMutationResult<
UpdateActionResponse, // response data
unknown, // error
UpdateActionVariables, // request
unknown // context
> => {
const queryClient = useQueryClient();
return useMutation([MutationKeys.updateAction], {
mutationFn: (variables: UpdateActionVariables) => dataService.updateAction(variables),
onMutate: (variables) => options?.onMutate?.(variables),
onError: (error, variables, context) => options?.onError?.(error, variables, context),
onSuccess: (updateActionResponse, variables, context) => {
const listRes = queryClient.getQueryData<AssistantListResponse>([
QueryKeys.assistants,
defaultOrderQuery,
]);
if (!listRes) {
return options?.onSuccess?.(updateActionResponse, variables, context);
}
const updatedAssistant = updateActionResponse[1];
queryClient.setQueryData<AssistantListResponse>([QueryKeys.assistants, defaultOrderQuery], {
...listRes,
data: listRes.data.map((assistant) => {
if (assistant.id === variables.assistant_id) {
return updatedAssistant;
}
return assistant;
}),
});
queryClient.setQueryData<Action[]>([QueryKeys.actions], (prev) => {
return prev
?.map((action) => {
if (action.action_id === variables.action_id) {
return updateActionResponse[2];
}
return action;
})
.concat(variables.action_id ? [] : [updateActionResponse[2]]);
});
return options?.onSuccess?.(updateActionResponse, variables, context);
},
});
};
/**
* Hook for deleting an Assistant Action
*/
export const useDeleteAction = (
options?: DeleteActionOptions,
): UseMutationResult<
void, // response data for a delete operation is typically void
Error, // error type
DeleteActionVariables, // request variables
unknown // context
> => {
const queryClient = useQueryClient();
return useMutation([MutationKeys.deleteAction], {
mutationFn: (variables: DeleteActionVariables) =>
dataService.deleteAction(variables.assistant_id, variables.action_id),
onMutate: (variables) => options?.onMutate?.(variables),
onError: (error, variables, context) => options?.onError?.(error, variables, context),
onSuccess: (_data, variables, context) => {
let domain: string | undefined = '';
queryClient.setQueryData<Action[]>([QueryKeys.actions], (prev) => {
return prev?.filter((action) => {
domain = action.metadata.domain;
return action.action_id !== variables.action_id;
});
});
queryClient.setQueryData<AssistantListResponse>(
[QueryKeys.assistants, defaultOrderQuery],
(prev) => {
if (!prev) {
return prev;
}
return {
...prev,
data: prev?.data.map((assistant) => {
if (assistant.id === variables.assistant_id) {
return {
...assistant,
tools: assistant.tools.filter(
(tool) => !tool.function?.name.includes(domain ?? ''),
),
};
}
return assistant;
}),
};
},
);
return options?.onSuccess?.(_data, variables, context);
},
});
};