mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-18 17:30:16 +01:00
🅰️ 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
This commit is contained in:
parent
1b243c6f8c
commit
5cd5c3bef8
60 changed files with 1044 additions and 300 deletions
|
|
@ -1,16 +1,17 @@
|
|||
import { useRecoilState } from 'recoil';
|
||||
import { memo, useCallback, useRef } from 'react';
|
||||
import TextareaAutosize from 'react-textarea-autosize';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import TextareaAutosize from 'react-textarea-autosize';
|
||||
import { memo, useCallback, useRef, useMemo } from 'react';
|
||||
import {
|
||||
supportsFiles,
|
||||
mergeFileConfig,
|
||||
fileConfig as defaultFileConfig,
|
||||
EModelEndpoint,
|
||||
} from 'librechat-data-provider';
|
||||
import { useChatContext, useAssistantsMapContext } from '~/Providers';
|
||||
import { useRequiresKey, useTextarea } from '~/hooks';
|
||||
import { useGetFileConfig } from '~/data-provider';
|
||||
import { cn, removeFocusOutlines } from '~/utils';
|
||||
import { useChatContext } from '~/Providers';
|
||||
import AttachFile from './Files/AttachFile';
|
||||
import StopButton from './StopButton';
|
||||
import SendButton from './SendButton';
|
||||
|
|
@ -37,6 +38,7 @@ const ChatForm = ({ index = 0 }) => {
|
|||
setFilesLoading,
|
||||
} = useChatContext();
|
||||
|
||||
const assistantMap = useAssistantsMapContext();
|
||||
const methods = useForm<{ text: string }>({
|
||||
defaultValues: { text: '' },
|
||||
});
|
||||
|
|
@ -61,6 +63,16 @@ const ChatForm = ({ index = 0 }) => {
|
|||
});
|
||||
|
||||
const endpointFileConfig = fileConfig.endpoints[endpoint ?? ''];
|
||||
const invalidAssistant = useMemo(
|
||||
() =>
|
||||
conversation?.endpoint === EModelEndpoint.assistants &&
|
||||
(!conversation?.assistant_id || !assistantMap?.[conversation?.assistant_id ?? '']),
|
||||
[conversation?.assistant_id, conversation?.endpoint, assistantMap],
|
||||
);
|
||||
const disableInputs = useMemo(
|
||||
() => !!(requiresKey || invalidAssistant),
|
||||
[requiresKey, invalidAssistant],
|
||||
);
|
||||
|
||||
return (
|
||||
<form
|
||||
|
|
@ -92,7 +104,7 @@ const ChatForm = ({ index = 0 }) => {
|
|||
ref={(e) => {
|
||||
textAreaRef.current = e;
|
||||
}}
|
||||
disabled={!!requiresKey}
|
||||
disabled={disableInputs}
|
||||
onPaste={handlePaste}
|
||||
onKeyUp={handleKeyUp}
|
||||
onKeyDown={handleKeyDown}
|
||||
|
|
@ -116,7 +128,7 @@ const ChatForm = ({ index = 0 }) => {
|
|||
<AttachFile
|
||||
endpoint={_endpoint ?? ''}
|
||||
endpointType={endpointType}
|
||||
disabled={requiresKey}
|
||||
disabled={disableInputs}
|
||||
/>
|
||||
{isSubmitting && showStopButton ? (
|
||||
<StopButton stop={handleStopGenerating} setShowStopButton={setShowStopButton} />
|
||||
|
|
@ -125,7 +137,7 @@ const ChatForm = ({ index = 0 }) => {
|
|||
<SendButton
|
||||
ref={submitButtonRef}
|
||||
control={methods.control}
|
||||
disabled={!!(filesLoading || isSubmitting || requiresKey)}
|
||||
disabled={!!(filesLoading || isSubmitting || disableInputs)}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -87,7 +87,9 @@ export default function Landing({ Header }: { Header?: ReactNode }) {
|
|||
</div>
|
||||
) : (
|
||||
<div className="mb-5 text-2xl font-medium dark:text-white">
|
||||
{localize('com_nav_welcome_message')}
|
||||
{endpoint === EModelEndpoint.assistants
|
||||
? localize('com_nav_welcome_assistant')
|
||||
: localize('com_nav_welcome_message')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ export const icons = {
|
|||
return <AssistantIcon className={cn('text-token-secondary', className)} size={size} />;
|
||||
}
|
||||
|
||||
return <Sparkles className={className} />;
|
||||
return <Sparkles className={cn(assistantName === '' ? 'icon-2xl' : '', className)} />;
|
||||
},
|
||||
unknown: UnknownIcon,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import * as Popover from '@radix-ui/react-popover';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useState, useEffect, useRef, useMemo } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
fileConfig as defaultFileConfig,
|
||||
|
|
@ -16,7 +16,7 @@ import type {
|
|||
} from 'librechat-data-provider';
|
||||
import { useUploadAssistantAvatarMutation, useGetFileConfig } from '~/data-provider';
|
||||
import { AssistantAvatar, NoImage, AvatarMenu } from './Images';
|
||||
import { useToastContext } from '~/Providers';
|
||||
import { useToastContext, useAssistantsMapContext } from '~/Providers';
|
||||
// import { Spinner } from '~/components/svg';
|
||||
import { useLocalize } from '~/hooks';
|
||||
// import { cn } from '~/utils/';
|
||||
|
|
@ -32,6 +32,7 @@ function Avatar({
|
|||
}) {
|
||||
// console.log('Avatar', assistant_id, metadata, createMutation);
|
||||
const queryClient = useQueryClient();
|
||||
const assistantsMap = useAssistantsMapContext();
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const [progress, setProgress] = useState<number>(1);
|
||||
const [input, setInput] = useState<File | null>(null);
|
||||
|
|
@ -44,6 +45,10 @@ function Avatar({
|
|||
const localize = useLocalize();
|
||||
const { showToast } = useToastContext();
|
||||
|
||||
const activeModel = useMemo(() => {
|
||||
return assistantsMap[assistant_id ?? '']?.model ?? '';
|
||||
}, [assistant_id, assistantsMap]);
|
||||
|
||||
const { mutate: uploadAvatar } = useUploadAssistantAvatarMutation({
|
||||
onMutate: () => {
|
||||
setProgress(0.4);
|
||||
|
|
@ -141,11 +146,12 @@ function Avatar({
|
|||
|
||||
uploadAvatar({
|
||||
assistant_id: createMutation.data.id,
|
||||
model: activeModel,
|
||||
postCreation: true,
|
||||
formData,
|
||||
});
|
||||
}
|
||||
}, [createMutation.data, createMutation.isSuccess, input, previewUrl, uploadAvatar]);
|
||||
}, [createMutation.data, createMutation.isSuccess, input, previewUrl, uploadAvatar, activeModel]);
|
||||
|
||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
const file = event.target.files?.[0];
|
||||
|
|
@ -175,6 +181,7 @@ function Avatar({
|
|||
|
||||
uploadAvatar({
|
||||
assistant_id,
|
||||
model: activeModel,
|
||||
formData,
|
||||
});
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
import { useState, useMemo, useEffect } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useGetModelsQuery } from 'librechat-data-provider/react-query';
|
||||
import { useForm, FormProvider, Controller, useWatch } from 'react-hook-form';
|
||||
import { useGetModelsQuery, useGetEndpointsQuery } from 'librechat-data-provider/react-query';
|
||||
import {
|
||||
Tools,
|
||||
QueryKeys,
|
||||
Capabilities,
|
||||
EModelEndpoint,
|
||||
actionDelimiter,
|
||||
supportsRetrieval,
|
||||
defaultAssistantFormValues,
|
||||
} from 'librechat-data-provider';
|
||||
import type { FunctionTool, TPlugin } from 'librechat-data-provider';
|
||||
import type { AssistantForm, AssistantPanelProps } from '~/common';
|
||||
import type { FunctionTool, TPlugin, TEndpointsConfig } from 'librechat-data-provider';
|
||||
import { useCreateAssistantMutation, useUpdateAssistantMutation } from '~/data-provider';
|
||||
import { SelectDropDown, Checkbox, QuestionMark } from '~/components/ui';
|
||||
import { useAssistantsMapContext, useToastContext } from '~/Providers';
|
||||
|
|
@ -42,7 +42,7 @@ export default function AssistantPanel({
|
|||
const queryClient = useQueryClient();
|
||||
const modelsQuery = useGetModelsQuery();
|
||||
const assistantMap = useAssistantsMapContext();
|
||||
const [showToolDialog, setShowToolDialog] = useState(false);
|
||||
const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery();
|
||||
const allTools = queryClient.getQueryData<TPlugin[]>([QueryKeys.tools]) ?? [];
|
||||
const { onSelect: onSelectAssistant } = useSelectAssistant();
|
||||
const { showToast } = useToastContext();
|
||||
|
|
@ -51,17 +51,43 @@ export default function AssistantPanel({
|
|||
const methods = useForm<AssistantForm>({
|
||||
defaultValues: defaultAssistantFormValues,
|
||||
});
|
||||
|
||||
const [showToolDialog, setShowToolDialog] = useState(false);
|
||||
|
||||
const { control, handleSubmit, reset, setValue, getValues } = methods;
|
||||
const assistant_id = useWatch({ control, name: 'id' });
|
||||
const assistant = useWatch({ control, name: 'assistant' });
|
||||
const functions = useWatch({ control, name: 'functions' });
|
||||
const assistant_id = useWatch({ control, name: 'id' });
|
||||
const model = useWatch({ control, name: 'model' });
|
||||
|
||||
const activeModel = useMemo(() => {
|
||||
return assistantMap?.[assistant_id]?.model;
|
||||
}, [assistantMap, assistant_id]);
|
||||
|
||||
const assistants = useMemo(() => endpointsConfig?.[EModelEndpoint.assistants], [endpointsConfig]);
|
||||
const retrievalModels = useMemo(() => new Set(assistants?.retrievalModels ?? []), [assistants]);
|
||||
const toolsEnabled = useMemo(
|
||||
() => assistants?.capabilities?.includes(Capabilities.tools),
|
||||
[assistants],
|
||||
);
|
||||
const actionsEnabled = useMemo(
|
||||
() => assistants?.capabilities?.includes(Capabilities.actions),
|
||||
[assistants],
|
||||
);
|
||||
const retrievalEnabled = useMemo(
|
||||
() => assistants?.capabilities?.includes(Capabilities.retrieval),
|
||||
[assistants],
|
||||
);
|
||||
const codeEnabled = useMemo(
|
||||
() => assistants?.capabilities?.includes(Capabilities.code_interpreter),
|
||||
[assistants],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (model && !supportsRetrieval.has(model)) {
|
||||
setValue('retrieval', false);
|
||||
if (model && !retrievalModels.has(model)) {
|
||||
setValue(Capabilities.retrieval, false);
|
||||
}
|
||||
}, [model, setValue]);
|
||||
}, [model, setValue, retrievalModels]);
|
||||
|
||||
/* Mutations */
|
||||
const update = useUpdateAssistantMutation({
|
||||
|
|
@ -300,78 +326,96 @@ export default function AssistantPanel({
|
|||
/>
|
||||
</div>
|
||||
{/* Knowledge */}
|
||||
<Knowledge assistant_id={assistant_id} files={files} />
|
||||
{(codeEnabled || retrievalEnabled) && (
|
||||
<Knowledge assistant_id={assistant_id} files={files} />
|
||||
)}
|
||||
{/* Capabilities */}
|
||||
<div className="mb-6">
|
||||
<div className="mb-1.5 flex items-center">
|
||||
<span>
|
||||
<label className="text-token-text-primary block font-medium">Capabilities</label>
|
||||
<label className="text-token-text-primary block font-medium">
|
||||
{localize('com_assistants_capabilities')}
|
||||
</label>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-start gap-2">
|
||||
<div className="flex items-center">
|
||||
<Controller
|
||||
name={'code_interpreter'}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
{...field}
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
|
||||
value={field?.value?.toString()}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<label
|
||||
className="form-check-label text-token-text-primary w-full cursor-pointer"
|
||||
htmlFor="code_interpreter"
|
||||
onClick={() =>
|
||||
setValue('code_interpreter', !getValues('code_interpreter'), {
|
||||
shouldDirty: true,
|
||||
})
|
||||
}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
{localize('com_assistants_code_interpreter')}
|
||||
<QuestionMark />
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Controller
|
||||
name={'retrieval'}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
{...field}
|
||||
checked={field.value}
|
||||
disabled={!supportsRetrieval.has(model)}
|
||||
onCheckedChange={field.onChange}
|
||||
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
|
||||
value={field?.value?.toString()}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<label
|
||||
className={cn(
|
||||
'form-check-label text-token-text-primary w-full',
|
||||
!supportsRetrieval.has(model) ? 'cursor-no-drop opacity-50' : 'cursor-pointer',
|
||||
)}
|
||||
htmlFor="retrieval"
|
||||
onClick={() =>
|
||||
supportsRetrieval.has(model) &&
|
||||
setValue('retrieval', !getValues('retrieval'), { shouldDirty: true })
|
||||
}
|
||||
>
|
||||
{localize('com_assistants_retrieval')}
|
||||
</label>
|
||||
</div>
|
||||
{codeEnabled && (
|
||||
<div className="flex items-center">
|
||||
<Controller
|
||||
name={Capabilities.code_interpreter}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
{...field}
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
|
||||
value={field?.value?.toString()}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<label
|
||||
className="form-check-label text-token-text-primary w-full cursor-pointer"
|
||||
htmlFor={Capabilities.code_interpreter}
|
||||
onClick={() =>
|
||||
setValue(
|
||||
Capabilities.code_interpreter,
|
||||
!getValues(Capabilities.code_interpreter),
|
||||
{
|
||||
shouldDirty: true,
|
||||
},
|
||||
)
|
||||
}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
{localize('com_assistants_code_interpreter')}
|
||||
<QuestionMark />
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
{retrievalEnabled && (
|
||||
<div className="flex items-center">
|
||||
<Controller
|
||||
name={Capabilities.retrieval}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
{...field}
|
||||
checked={field.value}
|
||||
disabled={!retrievalModels.has(model)}
|
||||
onCheckedChange={field.onChange}
|
||||
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
|
||||
value={field?.value?.toString()}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<label
|
||||
className={cn(
|
||||
'form-check-label text-token-text-primary w-full',
|
||||
!retrievalModels.has(model) ? 'cursor-no-drop opacity-50' : 'cursor-pointer',
|
||||
)}
|
||||
htmlFor={Capabilities.retrieval}
|
||||
onClick={() =>
|
||||
retrievalModels.has(model) &&
|
||||
setValue(Capabilities.retrieval, !getValues(Capabilities.retrieval), {
|
||||
shouldDirty: true,
|
||||
})
|
||||
}
|
||||
>
|
||||
{localize('com_assistants_retrieval')}
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* Tools */}
|
||||
<div className="mb-6">
|
||||
<label className={labelClass}>{localize('com_assistants_tools_section')}</label>
|
||||
<label className={labelClass}>
|
||||
{`${toolsEnabled ? localize('com_assistants_tools') : ''}
|
||||
${toolsEnabled && actionsEnabled ? ' + ' : ''}
|
||||
${actionsEnabled ? localize('com_assistants_actions') : ''}`}
|
||||
</label>
|
||||
<div className="space-y-1">
|
||||
{functions.map((func) => (
|
||||
<AssistantTool
|
||||
|
|
@ -388,39 +432,44 @@ export default function AssistantPanel({
|
|||
<AssistantAction key={i} action={action} onClick={() => setAction(action)} />
|
||||
);
|
||||
})}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowToolDialog(true)}
|
||||
className="btn border-token-border-light relative mx-1 mt-2 h-8 rounded-lg bg-transparent font-medium hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
>
|
||||
<div className="flex w-full items-center justify-center gap-2">
|
||||
{localize('com_assistants_add_tools')}
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!assistant_id}
|
||||
onClick={() => {
|
||||
if (!assistant_id) {
|
||||
return showToast({
|
||||
message: localize('com_assistants_actions_disabled'),
|
||||
status: 'warning',
|
||||
});
|
||||
}
|
||||
setActivePanel(Panel.actions);
|
||||
}}
|
||||
className="btn border-token-border-light relative mt-2 h-8 rounded-lg bg-transparent font-medium hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
>
|
||||
<div className="flex w-full items-center justify-center gap-2">
|
||||
{localize('com_assistants_add_actions')}
|
||||
</div>
|
||||
</button>
|
||||
{toolsEnabled && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowToolDialog(true)}
|
||||
className="btn border-token-border-light relative mx-1 mt-2 h-8 rounded-lg bg-transparent font-medium hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
>
|
||||
<div className="flex w-full items-center justify-center gap-2">
|
||||
{localize('com_assistants_add_tools')}
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
{actionsEnabled && (
|
||||
<button
|
||||
type="button"
|
||||
disabled={!assistant_id}
|
||||
onClick={() => {
|
||||
if (!assistant_id) {
|
||||
return showToast({
|
||||
message: localize('com_assistants_actions_disabled'),
|
||||
status: 'warning',
|
||||
});
|
||||
}
|
||||
setActivePanel(Panel.actions);
|
||||
}}
|
||||
className="btn border-token-border-light relative mt-2 h-8 rounded-lg bg-transparent font-medium hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
>
|
||||
<div className="flex w-full items-center justify-center gap-2">
|
||||
{localize('com_assistants_add_actions')}
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
{/* Context Button */}
|
||||
<ContextButton
|
||||
assistant_id={assistant_id}
|
||||
activeModel={activeModel}
|
||||
setCurrentAssistantId={setCurrentAssistantId}
|
||||
createMutation={create}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -10,10 +10,12 @@ import { NewTrashIcon } from '~/components/svg';
|
|||
import { useChatContext } from '~/Providers';
|
||||
|
||||
export default function ContextButton({
|
||||
activeModel,
|
||||
assistant_id,
|
||||
setCurrentAssistantId,
|
||||
createMutation,
|
||||
}: {
|
||||
activeModel: string;
|
||||
assistant_id: string;
|
||||
setCurrentAssistantId: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
createMutation: UseMutationResult<Assistant, Error, AssistantCreateParams>;
|
||||
|
|
@ -136,7 +138,7 @@ export default function ContextButton({
|
|||
</>
|
||||
}
|
||||
selection={{
|
||||
selectHandler: () => deleteAssistant.mutate({ assistant_id }),
|
||||
selectHandler: () => deleteAssistant.mutate({ assistant_id, model: activeModel }),
|
||||
selectClasses: 'bg-red-600 hover:bg-red-700 dark:hover:bg-red-800 text-white',
|
||||
selectText: localize('com_ui_delete'),
|
||||
}}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue