🤖 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
This commit is contained in:
Danny Avila 2024-05-19 12:56:55 -04:00 committed by GitHub
parent af8bcb08d6
commit 1a452121fa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
158 changed files with 4184 additions and 1204 deletions

View file

@ -1,10 +1,10 @@
import { useEffect, useMemo } from 'react';
import { Combobox } from '~/components/ui';
import { EModelEndpoint, defaultOrderQuery, LocalStorageKeys } from 'librechat-data-provider';
import type { SwitcherProps } from '~/common';
import { useSetIndexOptions, useSelectAssistant, useLocalize } from '~/hooks';
import { isAssistantsEndpoint, LocalStorageKeys } from 'librechat-data-provider';
import type { AssistantsEndpoint } from 'librechat-data-provider';
import type { SwitcherProps, AssistantListItem } from '~/common';
import { useSetIndexOptions, useSelectAssistant, useLocalize, useAssistantListMap } from '~/hooks';
import { useChatContext, useAssistantsMapContext } from '~/Providers';
import { useListAssistantsQuery } from '~/data-provider';
import Icon from '~/components/Endpoints/Icon';
export default function AssistantSwitcher({ isCollapsed }: SwitcherProps) {
@ -15,26 +15,29 @@ export default function AssistantSwitcher({ isCollapsed }: SwitcherProps) {
/* `selectedAssistant` must be defined with `null` to cause re-render on update */
const { assistant_id: selectedAssistant = null, endpoint } = conversation ?? {};
const { data: assistants = [] } = useListAssistantsQuery(defaultOrderQuery, {
select: (res) => res.data.map(({ id, name, metadata }) => ({ id, name, metadata })),
});
const assistantListMap = useAssistantListMap((res) =>
res.data.map(({ id, name, metadata }) => ({ id, name, metadata })),
);
const assistants: Omit<AssistantListItem, 'model'>[] = useMemo(
() => assistantListMap[endpoint ?? ''] ?? [],
[endpoint, assistantListMap],
);
const assistantMap = useAssistantsMapContext();
const { onSelect } = useSelectAssistant();
const { onSelect } = useSelectAssistant(endpoint as AssistantsEndpoint);
useEffect(() => {
if (!selectedAssistant && assistants && assistants.length && assistantMap) {
const assistant_id =
localStorage.getItem(`${LocalStorageKeys.ASST_ID_PREFIX}${index}`) ??
localStorage.getItem(`${LocalStorageKeys.ASST_ID_PREFIX}${index}${endpoint}`) ??
assistants[0]?.id ??
'';
const assistant = assistantMap?.[assistant_id];
const assistant = assistantMap?.[endpoint ?? '']?.[assistant_id];
if (!assistant) {
return;
}
if (endpoint !== EModelEndpoint.assistants) {
if (!isAssistantsEndpoint(endpoint)) {
return;
}
@ -43,7 +46,7 @@ export default function AssistantSwitcher({ isCollapsed }: SwitcherProps) {
}
}, [index, assistants, selectedAssistant, assistantMap, endpoint, setOption]);
const currentAssistant = assistantMap?.[selectedAssistant ?? ''];
const currentAssistant = assistantMap?.[endpoint ?? '']?.[selectedAssistant ?? ''];
const assistantOptions = useMemo(() => {
return assistants.map((assistant) => {
@ -53,14 +56,14 @@ export default function AssistantSwitcher({ isCollapsed }: SwitcherProps) {
icon: (
<Icon
isCreatedByUser={false}
endpoint={EModelEndpoint.assistants}
endpoint={endpoint}
assistantName={assistant.name ?? ''}
iconURL={(assistant.metadata?.avatar as string) ?? ''}
/>
),
};
});
}, [assistants]);
}, [assistants, endpoint]);
return (
<Combobox
@ -78,7 +81,7 @@ export default function AssistantSwitcher({ isCollapsed }: SwitcherProps) {
SelectIcon={
<Icon
isCreatedByUser={false}
endpoint={EModelEndpoint.assistants}
endpoint={endpoint}
assistantName={currentAssistant?.name ?? ''}
iconURL={(currentAssistant?.metadata?.avatar as string) ?? ''}
/>

View file

@ -134,7 +134,7 @@ const ApiKey = () => {
<input
placeholder="<HIDDEN>"
type="password"
autoComplete="off"
autoComplete="new-password"
className="border-token-border-medium mb-2 h-9 w-full resize-none overflow-y-auto rounded-lg border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-400 dark:bg-gray-600"
{...register('api_key', { required: type === AuthTypeEnum.ServiceHttp })}
/>

View file

@ -7,10 +7,11 @@ import {
AuthTypeEnum,
} from 'librechat-data-provider';
import type {
ValidationResult,
Action,
FunctionTool,
ActionMetadata,
ValidationResult,
AssistantsEndpoint,
} from 'librechat-data-provider';
import type { ActionAuthForm } from '~/common';
import type { Spec } from './ActionsTable';
@ -32,10 +33,14 @@ const debouncedValidation = debounce(
export default function ActionsInput({
action,
assistant_id,
endpoint,
version,
setAction,
}: {
action?: Action;
assistant_id?: string;
endpoint: AssistantsEndpoint;
version: number | string;
setAction: React.Dispatch<React.SetStateAction<Action | undefined>>;
}) {
const handleResult = (result: ValidationResult) => {
@ -173,7 +178,9 @@ export default function ActionsInput({
metadata,
functions,
assistant_id,
model: assistantMap[assistant_id].model,
endpoint,
version,
model: assistantMap[endpoint][assistant_id].model,
});
});

View file

@ -18,9 +18,11 @@ import { Panel } from '~/common';
export default function ActionsPanel({
// activePanel,
action,
endpoint,
version,
setAction,
setActivePanel,
assistant_id,
setActivePanel,
}: AssistantPanelProps) {
const localize = useLocalize();
const { showToast } = useToastContext();
@ -130,9 +132,10 @@ export default function ActionsPanel({
const confirmed = confirm('Are you sure you want to delete this action?');
if (confirmed) {
deleteAction.mutate({
model: assistantMap[assistant_id].model,
model: assistantMap[endpoint][assistant_id].model,
action_id: action.action_id,
assistant_id,
endpoint,
});
}
}}
@ -185,7 +188,13 @@ export default function ActionsPanel({
</DialogTrigger>
<ActionsAuth setOpenAuthDialog={setOpenAuthDialog} />
</Dialog>
<ActionsInput action={action} assistant_id={assistant_id} setAction={setAction} />
<ActionsInput
action={action}
assistant_id={assistant_id}
setAction={setAction}
endpoint={endpoint}
version={version}
/>
</div>
</form>
</FormProvider>

View file

@ -10,9 +10,10 @@ import {
import type { UseMutationResult } from '@tanstack/react-query';
import type {
Metadata,
AssistantListResponse,
Assistant,
AssistantsEndpoint,
AssistantCreateParams,
AssistantListResponse,
} from 'librechat-data-provider';
import { useUploadAssistantAvatarMutation, useGetFileConfig } from '~/data-provider';
import { AssistantAvatar, NoImage, AvatarMenu } from './Images';
@ -22,10 +23,14 @@ import { useLocalize } from '~/hooks';
// import { cn } from '~/utils/';
function Avatar({
endpoint,
version,
assistant_id,
metadata,
createMutation,
}: {
endpoint: AssistantsEndpoint;
version: number | string;
assistant_id: string | null;
metadata: null | Metadata;
createMutation: UseMutationResult<Assistant, Error, AssistantCreateParams>;
@ -46,8 +51,8 @@ function Avatar({
const { showToast } = useToastContext();
const activeModel = useMemo(() => {
return assistantsMap[assistant_id ?? '']?.model ?? '';
}, [assistant_id, assistantsMap]);
return assistantsMap[endpoint][assistant_id ?? '']?.model ?? '';
}, [assistantsMap, endpoint, assistant_id]);
const { mutate: uploadAvatar } = useUploadAssistantAvatarMutation({
onMutate: () => {
@ -65,6 +70,7 @@ function Avatar({
const res = queryClient.getQueryData<AssistantListResponse>([
QueryKeys.assistants,
endpoint,
defaultOrderQuery,
]);
@ -83,10 +89,13 @@ function Avatar({
return assistant;
}) ?? [];
queryClient.setQueryData<AssistantListResponse>([QueryKeys.assistants, defaultOrderQuery], {
...res,
data: assistants,
});
queryClient.setQueryData<AssistantListResponse>(
[QueryKeys.assistants, endpoint, defaultOrderQuery],
{
...res,
data: assistants,
},
);
setProgress(1);
},
@ -149,9 +158,20 @@ function Avatar({
model: activeModel,
postCreation: true,
formData,
endpoint,
version,
});
}
}, [createMutation.data, createMutation.isSuccess, input, previewUrl, uploadAvatar, activeModel]);
}, [
createMutation.data,
createMutation.isSuccess,
input,
previewUrl,
uploadAvatar,
activeModel,
endpoint,
version,
]);
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
const file = event.target.files?.[0];
@ -183,6 +203,8 @@ function Avatar({
assistant_id,
model: activeModel,
formData,
endpoint,
version,
});
} else {
showToast({

View file

@ -1,23 +1,23 @@
import { useState, useMemo, useEffect } from 'react';
import { useState, useMemo } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { useForm, FormProvider, Controller, useWatch } from 'react-hook-form';
import { useGetModelsQuery, useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import { useGetModelsQuery } from 'librechat-data-provider/react-query';
import {
Tools,
QueryKeys,
Capabilities,
EModelEndpoint,
actionDelimiter,
ImageVisionTool,
defaultAssistantFormValues,
} from 'librechat-data-provider';
import type { FunctionTool, TConfig, 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';
import { useSelectAssistant, useLocalize } from '~/hooks';
import { ToolSelectDialog } from '~/components/Tools';
import CapabilitiesForm from './CapabilitiesForm';
import { SelectDropDown } from '~/components/ui';
import AssistantAvatar from './AssistantAvatar';
import AssistantSelect from './AssistantSelect';
import AssistantAction from './AssistantAction';
@ -35,17 +35,20 @@ const inputClass =
export default function AssistantPanel({
// index = 0,
setAction,
endpoint,
actions = [],
setActivePanel,
assistant_id: current_assistant_id,
setCurrentAssistantId,
}: AssistantPanelProps) {
assistantsConfig,
version,
}: AssistantPanelProps & { assistantsConfig?: TConfig | null }) {
const queryClient = useQueryClient();
const modelsQuery = useGetModelsQuery();
const assistantMap = useAssistantsMapContext();
const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery();
const allTools = queryClient.getQueryData<TPlugin[]>([QueryKeys.tools]) ?? [];
const { onSelect: onSelectAssistant } = useSelectAssistant();
const { onSelect: onSelectAssistant } = useSelectAssistant(endpoint);
const { showToast } = useToastContext();
const localize = useLocalize();
@ -55,44 +58,31 @@ export default function AssistantPanel({
const [showToolDialog, setShowToolDialog] = useState(false);
const { control, handleSubmit, reset, setValue, getValues } = methods;
const { control, handleSubmit, reset } = methods;
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]);
return assistantMap?.[endpoint]?.[assistant_id]?.model;
}, [assistantMap, endpoint, 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],
() => assistantsConfig?.capabilities?.includes(Capabilities.tools),
[assistantsConfig],
);
const actionsEnabled = useMemo(
() => assistants?.capabilities?.includes(Capabilities.actions),
[assistants],
() => assistantsConfig?.capabilities?.includes(Capabilities.actions),
[assistantsConfig],
);
const retrievalEnabled = useMemo(
() => assistants?.capabilities?.includes(Capabilities.retrieval),
[assistants],
() => assistantsConfig?.capabilities?.includes(Capabilities.retrieval),
[assistantsConfig],
);
const codeEnabled = useMemo(
() => assistants?.capabilities?.includes(Capabilities.code_interpreter),
[assistants],
() => assistantsConfig?.capabilities?.includes(Capabilities.code_interpreter),
[assistantsConfig],
);
const imageVisionEnabled = useMemo(
() => assistants?.capabilities?.includes(Capabilities.image_vision),
[assistants],
);
useEffect(() => {
if (model && !retrievalModels.has(model)) {
setValue(Capabilities.retrieval, false);
}
}, [model, setValue, retrievalModels]);
/* Mutations */
const update = useUpdateAssistantMutation({
@ -145,7 +135,7 @@ export default function AssistantPanel({
if (!functionName.includes(actionDelimiter)) {
return functionName;
} else {
const assistant = assistantMap?.[assistant_id];
const assistant = assistantMap?.[endpoint]?.[assistant_id];
const tool = assistant?.tools?.find((tool) => tool.function?.name === functionName);
if (assistant && tool) {
return tool;
@ -160,7 +150,7 @@ export default function AssistantPanel({
tools.push({ type: Tools.code_interpreter });
}
if (data.retrieval) {
tools.push({ type: Tools.retrieval });
tools.push({ type: version == 2 ? Tools.file_search : Tools.retrieval });
}
if (data.image_vision) {
tools.push(ImageVisionTool);
@ -183,6 +173,7 @@ export default function AssistantPanel({
instructions,
model,
tools,
endpoint,
},
});
return;
@ -194,6 +185,8 @@ export default function AssistantPanel({
instructions,
model,
tools,
endpoint,
version,
});
};
@ -211,6 +204,7 @@ export default function AssistantPanel({
<AssistantSelect
reset={reset}
value={field.value}
endpoint={endpoint}
setCurrentAssistantId={setCurrentAssistantId}
selectedAssistant={current_assistant_id ?? null}
createMutation={create}
@ -239,6 +233,8 @@ export default function AssistantPanel({
createMutation={create}
assistant_id={assistant_id ?? null}
metadata={assistant?.['metadata'] ?? null}
endpoint={endpoint}
version={version}
/>
<label className={labelClass} htmlFor="name">
{localize('com_ui_name')}
@ -324,7 +320,7 @@ export default function AssistantPanel({
emptyTitle={true}
value={field.value}
setValue={field.onChange}
availableValues={modelsQuery.data?.[EModelEndpoint.assistants] ?? []}
availableValues={modelsQuery.data?.[endpoint] ?? []}
showAbove={false}
showLabel={false}
className={cn(
@ -343,120 +339,17 @@ export default function AssistantPanel({
/>
</div>
{/* Knowledge */}
{(codeEnabled || retrievalEnabled) && (
<Knowledge assistant_id={assistant_id} files={files} />
{(codeEnabled || retrievalEnabled) && version == 1 && (
<Knowledge assistant_id={assistant_id} files={files} endpoint={endpoint} />
)}
{/* Capabilities */}
<div className="mb-6">
<div className="mb-1.5 flex items-center">
<span>
<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">
{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>
)}
{imageVisionEnabled && (
<div className="flex items-center">
<Controller
name={Capabilities.image_vision}
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.image_vision}
onClick={() =>
setValue(Capabilities.image_vision, !getValues(Capabilities.image_vision), {
shouldDirty: true,
})
}
>
<div className="flex items-center">
{localize('com_assistants_image_vision')}
<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>
<CapabilitiesForm
version={version}
endpoint={endpoint}
codeEnabled={codeEnabled}
assistantsConfig={assistantsConfig}
retrievalEnabled={retrievalEnabled}
/>
{/* Tools */}
<div className="mb-6">
<label className={labelClass}>
@ -520,6 +413,7 @@ export default function AssistantPanel({
activeModel={activeModel}
setCurrentAssistantId={setCurrentAssistantId}
createMutation={create}
endpoint={endpoint}
/>
{/* Secondary Select Button */}
{assistant_id && (
@ -554,6 +448,7 @@ export default function AssistantPanel({
isOpen={showToolDialog}
setIsOpen={setShowToolDialog}
assistant_id={assistant_id}
endpoint={endpoint}
/>
</form>
</FormProvider>

View file

@ -1,21 +1,22 @@
import { Plus } from 'lucide-react';
import { useCallback, useEffect, useRef } from 'react';
import {
defaultAssistantFormValues,
defaultOrderQuery,
isImageVisionTool,
EModelEndpoint,
Capabilities,
Tools,
FileSources,
Capabilities,
EModelEndpoint,
LocalStorageKeys,
isImageVisionTool,
defaultAssistantFormValues,
} from 'librechat-data-provider';
import type { UseFormReset } from 'react-hook-form';
import type { UseMutationResult } from '@tanstack/react-query';
import type { Assistant, AssistantCreateParams } from 'librechat-data-provider';
import type { Assistant, AssistantCreateParams, AssistantsEndpoint } from 'librechat-data-provider';
import type {
AssistantForm,
Actions,
TAssistantOption,
ExtendedFile,
AssistantForm,
TAssistantOption,
LastSelectedModels,
} from '~/common';
import SelectDropDown from '~/components/ui/SelectDropDown';
@ -29,12 +30,14 @@ const keys = new Set(['name', 'id', 'description', 'instructions', 'model']);
export default function AssistantSelect({
reset,
value,
endpoint,
selectedAssistant,
setCurrentAssistantId,
createMutation,
}: {
reset: UseFormReset<AssistantForm>;
value: TAssistantOption;
endpoint: AssistantsEndpoint;
selectedAssistant: string | null;
setCurrentAssistantId: React.Dispatch<React.SetStateAction<string | undefined>>;
createMutation: UseMutationResult<Assistant, Error, AssistantCreateParams>;
@ -43,42 +46,69 @@ export default function AssistantSelect({
const fileMap = useFileMapContext();
const lastSelectedAssistant = useRef<string | null>(null);
const [lastSelectedModels] = useLocalStorage<LastSelectedModels>(
'lastSelectedModel',
LocalStorageKeys.LAST_MODEL,
{} as LastSelectedModels,
);
const assistants = useListAssistantsQuery(defaultOrderQuery, {
const assistants = useListAssistantsQuery(endpoint, undefined, {
select: (res) =>
res.data.map((_assistant) => {
const source =
endpoint === EModelEndpoint.assistants ? FileSources.openai : FileSources.azure;
const assistant = {
..._assistant,
label: _assistant?.name ?? '',
value: _assistant.id,
files: _assistant?.file_ids ? ([] as Array<[string, ExtendedFile]>) : undefined,
code_files: _assistant?.tool_resources?.code_interpreter?.file_ids
? ([] as Array<[string, ExtendedFile]>)
: undefined,
};
const handleFile = (file_id: string, list?: Array<[string, ExtendedFile]>) => {
const file = fileMap?.[file_id];
if (file) {
list?.push([
file_id,
{
file_id: file.file_id,
type: file.type,
filepath: file.filepath,
filename: file.filename,
width: file.width,
height: file.height,
size: file.bytes,
preview: file.filepath,
progress: 1,
source,
},
]);
} else {
list?.push([
file_id,
{
file_id,
type: '',
filename: '',
size: 1,
progress: 1,
filepath: endpoint,
source,
},
]);
}
};
if (assistant.files && _assistant.file_ids) {
_assistant.file_ids.forEach((file_id) => {
const file = fileMap?.[file_id];
if (file) {
assistant.files?.push([
file_id,
{
file_id: file.file_id,
type: file.type,
filepath: file.filepath,
filename: file.filename,
width: file.width,
height: file.height,
size: file.bytes,
preview: file.filepath,
progress: 1,
source: FileSources.openai,
},
]);
}
});
_assistant.file_ids.forEach((file_id) => handleFile(file_id, assistant.files));
}
if (assistant.code_files && _assistant.tool_resources?.code_interpreter?.file_ids) {
_assistant.tool_resources?.code_interpreter?.file_ids?.forEach((file_id) =>
handleFile(file_id, assistant.code_files),
);
}
return assistant;
}),
});
@ -92,7 +122,7 @@ export default function AssistantSelect({
setCurrentAssistantId(undefined);
return reset({
...defaultAssistantFormValues,
model: lastSelectedModels?.[EModelEndpoint.assistants] ?? '',
model: lastSelectedModels?.[endpoint] ?? '',
});
}
@ -112,6 +142,9 @@ export default function AssistantSelect({
?.filter((tool) => tool.type !== 'function' || isImageVisionTool(tool))
?.map((tool) => tool?.function?.name || tool.type)
.forEach((tool) => {
if (tool === Tools.file_search) {
actions[Capabilities.retrieval] = true;
}
actions[tool] = true;
});
@ -141,7 +174,7 @@ export default function AssistantSelect({
reset(formValues);
setCurrentAssistantId(assistant?.id);
},
[assistants.data, reset, setCurrentAssistantId, createMutation, lastSelectedModels],
[assistants.data, reset, setCurrentAssistantId, createMutation, endpoint, lastSelectedModels],
);
useEffect(() => {

View file

@ -0,0 +1,51 @@
import { useMemo } from 'react';
import { Capabilities } from 'librechat-data-provider';
import type { TConfig, AssistantsEndpoint } from 'librechat-data-provider';
import ImageVision from './ImageVision';
import { useLocalize } from '~/hooks';
import Retrieval from './Retrieval';
import Code from './Code';
export default function CapabilitiesForm({
version,
endpoint,
codeEnabled,
retrievalEnabled,
assistantsConfig,
}: {
version: number | string;
codeEnabled?: boolean;
retrievalEnabled?: boolean;
endpoint: AssistantsEndpoint;
assistantsConfig?: TConfig | null;
}) {
const localize = useLocalize();
const retrievalModels = useMemo(
() => new Set(assistantsConfig?.retrievalModels ?? []),
[assistantsConfig],
);
const imageVisionEnabled = useMemo(
() => assistantsConfig?.capabilities?.includes(Capabilities.image_vision),
[assistantsConfig],
);
return (
<div className="mb-6">
<div className="mb-1.5 flex items-center">
<span>
<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">
{codeEnabled && <Code endpoint={endpoint} version={version} />}
{imageVisionEnabled && version == 1 && <ImageVision />}
{retrievalEnabled && (
<Retrieval endpoint={endpoint} version={version} retrievalModels={retrievalModels} />
)}
</div>
</div>
);
}

View file

@ -0,0 +1,70 @@
import { useMemo } from 'react';
import { Capabilities } from 'librechat-data-provider';
import { useFormContext, Controller, useWatch } from 'react-hook-form';
import type { AssistantsEndpoint } from 'librechat-data-provider';
import type { AssistantForm } from '~/common';
import { Checkbox, QuestionMark } from '~/components/ui';
import { useLocalize } from '~/hooks';
import CodeFiles from './CodeFiles';
export default function Code({
version,
endpoint,
}: {
version: number | string;
endpoint: AssistantsEndpoint;
}) {
const localize = useLocalize();
const methods = useFormContext<AssistantForm>();
const { control, setValue, getValues } = methods;
const assistant = useWatch({ control, name: 'assistant' });
const assistant_id = useWatch({ control, name: 'id' });
const files = useMemo(() => {
if (typeof assistant === 'string') {
return [];
}
return assistant.code_files;
}, [assistant]);
return (
<>
<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 select-none items-center">
{localize('com_assistants_code_interpreter')}
<QuestionMark />
</div>
</label>
</div>
{version == 2 && (
<CodeFiles
assistant_id={assistant_id}
version={version}
endpoint={endpoint}
files={files}
/>
)}
</>
);
}

View file

@ -0,0 +1,98 @@
import { useState, useRef, useEffect } from 'react';
import {
EToolResources,
mergeFileConfig,
fileConfig as defaultFileConfig,
} from 'librechat-data-provider';
import type { AssistantsEndpoint } from 'librechat-data-provider';
import type { ExtendedFile } from '~/common';
import FileRow from '~/components/Chat/Input/Files/FileRow';
import { useGetFileConfig } from '~/data-provider';
import { useFileHandling } from '~/hooks/Files';
import useLocalize from '~/hooks/useLocalize';
import { useChatContext } from '~/Providers';
const tool_resource = EToolResources.code_interpreter;
export default function CodeFiles({
endpoint,
assistant_id,
files: _files,
}: {
version: number | string;
endpoint: AssistantsEndpoint;
assistant_id: string;
files?: [string, ExtendedFile][];
}) {
const localize = useLocalize();
const { setFilesLoading } = useChatContext();
const fileInputRef = useRef<HTMLInputElement>(null);
const [files, setFiles] = useState<Map<string, ExtendedFile>>(new Map());
const { data: fileConfig = defaultFileConfig } = useGetFileConfig({
select: (data) => mergeFileConfig(data),
});
const { handleFileChange } = useFileHandling({
overrideEndpoint: endpoint,
additionalMetadata: { assistant_id, tool_resource },
fileSetter: setFiles,
});
useEffect(() => {
if (_files) {
setFiles(new Map(_files));
}
}, [_files]);
const endpointFileConfig = fileConfig.endpoints[endpoint];
if (endpointFileConfig?.disabled) {
return null;
}
const handleButtonClick = () => {
// necessary to reset the input
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
fileInputRef.current?.click();
};
return (
<div className={'mb-2'}>
<div className="flex flex-col gap-4">
<div className="text-token-text-tertiary rounded-lg text-xs">
{localize('com_assistants_code_interpreter_files')}
</div>
<FileRow
files={files}
setFiles={setFiles}
assistant_id={assistant_id}
tool_resource={tool_resource}
setFilesLoading={setFilesLoading}
Wrapper={({ children }) => <div className="flex flex-wrap gap-2">{children}</div>}
/>
<div>
<button
type="button"
disabled={!assistant_id}
className="btn btn-neutral border-token-border-light relative h-8 rounded-lg font-medium"
onClick={handleButtonClick}
>
<div className="flex w-full items-center justify-center gap-2">
<input
multiple={true}
type="file"
style={{ display: 'none' }}
tabIndex={-1}
ref={fileInputRef}
disabled={!assistant_id}
onChange={handleFileChange}
/>
{localize('com_ui_upload_files')}
</div>
</button>
</div>
</div>
</div>
);
}

View file

@ -1,26 +1,29 @@
import * as Popover from '@radix-ui/react-popover';
import type { Assistant, AssistantCreateParams } from 'librechat-data-provider';
import type { Assistant, AssistantCreateParams, AssistantsEndpoint } from 'librechat-data-provider';
import type { UseMutationResult } from '@tanstack/react-query';
import { Dialog, DialogTrigger, Label } from '~/components/ui';
import DialogTemplate from '~/components/ui/DialogTemplate';
import { useChatContext, useToastContext } from '~/Providers';
import { useDeleteAssistantMutation } from '~/data-provider';
import DialogTemplate from '~/components/ui/DialogTemplate';
import { useLocalize, useSetIndexOptions } from '~/hooks';
import { cn, removeFocusOutlines } from '~/utils/';
import { NewTrashIcon } from '~/components/svg';
import { useChatContext } from '~/Providers';
export default function ContextButton({
activeModel,
assistant_id,
setCurrentAssistantId,
createMutation,
endpoint,
}: {
activeModel: string;
assistant_id: string;
setCurrentAssistantId: React.Dispatch<React.SetStateAction<string | undefined>>;
createMutation: UseMutationResult<Assistant, Error, AssistantCreateParams>;
endpoint: AssistantsEndpoint;
}) {
const localize = useLocalize();
const { showToast } = useToastContext();
const { conversation } = useChatContext();
const { setOption } = useSetIndexOptions();
@ -31,6 +34,11 @@ export default function ContextButton({
return;
}
showToast({
message: localize('com_ui_assistant_deleted'),
status: 'success',
});
if (createMutation.data?.id) {
console.log('[deleteAssistant] resetting createMutation');
createMutation.reset();
@ -55,6 +63,13 @@ export default function ContextButton({
setCurrentAssistantId(firstAssistant.id);
},
onError: (error) => {
console.error(error);
showToast({
message: localize('com_ui_assistant_delete_error'),
status: 'error',
});
},
});
if (!assistant_id) {
@ -138,7 +153,8 @@ export default function ContextButton({
</>
}
selection={{
selectHandler: () => deleteAssistant.mutate({ assistant_id, model: activeModel }),
selectHandler: () =>
deleteAssistant.mutate({ assistant_id, model: activeModel, endpoint }),
selectClasses: 'bg-red-600 hover:bg-red-700 dark:hover:bg-red-800 text-white',
selectText: localize('com_ui_delete'),
}}

View file

@ -0,0 +1,43 @@
import { useFormContext, Controller } from 'react-hook-form';
import { Capabilities } from 'librechat-data-provider';
import type { AssistantForm } from '~/common';
import { Checkbox, QuestionMark } from '~/components/ui';
import { useLocalize } from '~/hooks';
export default function ImageVision() {
const localize = useLocalize();
const methods = useFormContext<AssistantForm>();
const { control, setValue, getValues } = methods;
return (
<div className="flex items-center">
<Controller
name={Capabilities.image_vision}
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.image_vision}
onClick={() =>
setValue(Capabilities.image_vision, !getValues(Capabilities.image_vision), {
shouldDirty: true,
})
}
>
<div className="flex items-center">
{localize('com_assistants_image_vision')}
<QuestionMark />
</div>
</label>
</div>
);
}

View file

@ -41,10 +41,10 @@ export const AssistantAvatar = ({
return (
<div>
<div className="relative overflow-hidden rounded-full">
<div className="relative h-20 w-20 overflow-hidden rounded-full">
<img
src={url}
className="bg-token-surface-secondary dark:bg-token-surface-tertiary h-full w-full"
className="bg-token-surface-secondary dark:bg-token-surface-tertiary h-full w-full rounded-full object-cover"
alt="GPT"
width="80"
height="80"

View file

@ -1,10 +1,10 @@
import { useState, useRef, useEffect } from 'react';
import {
EModelEndpoint,
mergeFileConfig,
retrievalMimeTypes,
fileConfig as defaultFileConfig,
mergeFileConfig,
} from 'librechat-data-provider';
import type { AssistantsEndpoint } from 'librechat-data-provider';
import type { ExtendedFile } from '~/common';
import FileRow from '~/components/Chat/Input/Files/FileRow';
import { useGetFileConfig } from '~/data-provider';
@ -26,9 +26,11 @@ const CodeInterpreterFiles = ({ children }: { children: React.ReactNode }) => {
};
export default function Knowledge({
endpoint,
assistant_id,
files: _files,
}: {
endpoint: AssistantsEndpoint;
assistant_id: string;
files?: [string, ExtendedFile][];
}) {
@ -40,7 +42,7 @@ export default function Knowledge({
select: (data) => mergeFileConfig(data),
});
const { handleFileChange } = useFileHandling({
overrideEndpoint: EModelEndpoint.assistants,
overrideEndpoint: endpoint,
additionalMetadata: { assistant_id },
fileSetter: setFiles,
});
@ -51,7 +53,7 @@ export default function Knowledge({
}
}, [_files]);
const endpointFileConfig = fileConfig.endpoints[EModelEndpoint.assistants];
const endpointFileConfig = fileConfig.endpoints[endpoint];
if (endpointFileConfig?.disabled) {
return null;

View file

@ -1,5 +1,7 @@
import { useState, useEffect } from 'react';
import type { Action } from 'librechat-data-provider';
import { useState, useEffect, useMemo } from 'react';
import { defaultAssistantsVersion } from 'librechat-data-provider';
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import type { Action, AssistantsEndpoint, TEndpointsConfig } from 'librechat-data-provider';
import { useGetActionsQuery } from '~/data-provider';
import AssistantPanel from './AssistantPanel';
import { useChatContext } from '~/Providers';
@ -9,11 +11,18 @@ import { Panel } from '~/common';
export default function PanelSwitch() {
const { conversation, index } = useChatContext();
const [activePanel, setActivePanel] = useState(Panel.builder);
const [action, setAction] = useState<Action | undefined>(undefined);
const [currentAssistantId, setCurrentAssistantId] = useState<string | undefined>(
conversation?.assistant_id,
);
const [action, setAction] = useState<Action | undefined>(undefined);
const { data: actions = [] } = useGetActionsQuery();
const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery();
const { data: actions = [] } = useGetActionsQuery(conversation?.endpoint as AssistantsEndpoint);
const assistantsConfig = useMemo(
() => endpointsConfig?.[conversation?.endpoint ?? ''],
[conversation?.endpoint, endpointsConfig],
);
useEffect(() => {
if (conversation?.assistant_id) {
@ -21,6 +30,12 @@ export default function PanelSwitch() {
}
}, [conversation?.assistant_id]);
if (!conversation?.endpoint) {
return null;
}
const version = assistantsConfig?.version ?? defaultAssistantsVersion[conversation.endpoint];
if (activePanel === Panel.actions || action) {
return (
<ActionsPanel
@ -32,6 +47,8 @@ export default function PanelSwitch() {
setActivePanel={setActivePanel}
assistant_id={currentAssistantId}
setCurrentAssistantId={setCurrentAssistantId}
endpoint={conversation.endpoint as AssistantsEndpoint}
version={version}
/>
);
} else if (activePanel === Panel.builder) {
@ -45,6 +62,9 @@ export default function PanelSwitch() {
setActivePanel={setActivePanel}
assistant_id={currentAssistantId}
setCurrentAssistantId={setCurrentAssistantId}
endpoint={conversation.endpoint as AssistantsEndpoint}
assistantsConfig={assistantsConfig}
version={version}
/>
);
}

View file

@ -0,0 +1,94 @@
import { useEffect, useMemo } from 'react';
import { useFormContext, Controller, useWatch } from 'react-hook-form';
import { Capabilities } from 'librechat-data-provider';
import type { AssistantsEndpoint } from 'librechat-data-provider';
import type { AssistantForm } from '~/common';
import OptionHover from '~/components/SidePanel/Parameters/OptionHover';
import { Checkbox, HoverCard, HoverCardTrigger } from '~/components/ui';
import { useLocalize } from '~/hooks';
import { ESide } from '~/common';
import { cn } from '~/utils/';
export default function Retrieval({
version,
retrievalModels,
}: {
version: number | string;
retrievalModels: Set<string>;
endpoint: AssistantsEndpoint;
}) {
const localize = useLocalize();
const methods = useFormContext<AssistantForm>();
const { control, setValue, getValues } = methods;
const model = useWatch({ control, name: 'model' });
const assistant = useWatch({ control, name: 'assistant' });
const vectorStores = useMemo(() => {
if (typeof assistant === 'string') {
return [];
}
return assistant.tool_resources?.file_search;
}, [assistant]);
const isDisabled = useMemo(() => !retrievalModels.has(model), [model, retrievalModels]);
useEffect(() => {
if (model && isDisabled) {
setValue(Capabilities.retrieval, false);
}
}, [model, setValue, isDisabled]);
return (
<>
<div className="flex items-center">
<Controller
name={Capabilities.retrieval}
control={control}
render={({ field }) => (
<Checkbox
{...field}
checked={field.value}
disabled={isDisabled}
onCheckedChange={field.onChange}
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
value={field?.value?.toString()}
/>
)}
/>
<HoverCard openDelay={50}>
<HoverCardTrigger asChild>
<label
className={cn(
'form-check-label text-token-text-primary w-full select-none',
isDisabled ? 'cursor-no-drop opacity-50' : 'cursor-pointer',
)}
htmlFor={Capabilities.retrieval}
onClick={() =>
retrievalModels.has(model) &&
setValue(Capabilities.retrieval, !getValues(Capabilities.retrieval), {
shouldDirty: true,
})
}
>
{version == 1
? localize('com_assistants_retrieval')
: localize('com_assistants_file_search')}
</label>
</HoverCardTrigger>
<OptionHover
side={ESide.Top}
disabled={!isDisabled}
description="com_assistants_non_retrieval_model"
langCode={true}
sideOffset={20}
/>
</HoverCard>
</div>
{version == 2 && (
<div className="text-token-text-tertiary rounded-lg text-xs">
{localize('com_assistants_file_search_info')}
</div>
)}
</>
);
}

View file

@ -1,8 +1,10 @@
import { useCallback } from 'react';
import {
fileConfig as defaultFileConfig,
checkOpenAIStorage,
mergeFileConfig,
megabyte,
isAssistantsEndpoint,
} from 'librechat-data-provider';
import type { Row } from '@tanstack/react-table';
import type { TFile } from 'librechat-data-provider';
@ -36,6 +38,18 @@ export default function PanelFileCell({ row }: { row: Row<TFile> }) {
return showToast({ message: localize('com_ui_attach_error'), status: 'error' });
}
if (checkOpenAIStorage(fileData?.source ?? '') && !isAssistantsEndpoint(endpoint)) {
return showToast({
message: localize('com_ui_attach_error_openai'),
status: 'error',
});
} else if (!checkOpenAIStorage(fileData?.source ?? '') && isAssistantsEndpoint(endpoint)) {
showToast({
message: localize('com_ui_attach_warn_endpoint'),
status: 'warning',
});
}
const { fileSizeLimit, supportedMimeTypes } =
fileConfig.endpoints[endpoint] ?? fileConfig.endpoints.default;
@ -81,7 +95,8 @@ export default function PanelFileCell({ row }: { row: Row<TFile> }) {
>
<ImagePreview
url={file.filepath}
className="h-10 w-10 shrink-0 overflow-hidden rounded-md"
className="relative h-10 w-10 shrink-0 overflow-hidden rounded-md"
source={file.source}
/>
<span className="self-center truncate text-xs">{file.filename}</span>
</div>
@ -94,7 +109,7 @@ export default function PanelFileCell({ row }: { row: Row<TFile> }) {
onClick={handleFileClick}
className="flex cursor-pointer gap-2 rounded-md dark:hover:bg-gray-700"
>
{fileType && <FilePreview fileType={fileType} />}
{fileType && <FilePreview fileType={fileType} className="relative" file={file} />}
<span className="self-center truncate">{file.filename}</span>
</div>
);

View file

@ -7,11 +7,21 @@ type TOptionHoverProps = {
description: string;
langCode?: boolean;
sideOffset?: number;
disabled?: boolean;
side: ESide;
};
function OptionHover({ side, description, langCode, sideOffset = 30 }: TOptionHoverProps) {
function OptionHover({
side,
description,
disabled,
langCode,
sideOffset = 30,
}: TOptionHoverProps) {
const localize = useLocalize();
if (disabled) {
return null;
}
const text = langCode ? localize(description) : description;
return (
<HoverCardPortal>

View file

@ -1,5 +1,5 @@
import throttle from 'lodash/throttle';
import { EModelEndpoint, getConfigDefaults } from 'librechat-data-provider';
import { getConfigDefaults } from 'librechat-data-provider';
import { useState, useRef, useCallback, useEffect, useMemo, memo } from 'react';
import {
useGetEndpointsQuery,
@ -61,7 +61,7 @@ const SidePanel = ({
return activePanel ? activePanel : undefined;
}, []);
const assistants = useMemo(() => endpointsConfig?.[EModelEndpoint.assistants], [endpointsConfig]);
const assistants = useMemo(() => endpointsConfig?.[endpoint ?? ''], [endpoint, endpointsConfig]);
const userProvidesKey = useMemo(
() => !!endpointsConfig?.[endpoint ?? '']?.userProvide,
[endpointsConfig, endpoint],

View file

@ -1,18 +1,18 @@
import { EModelEndpoint } from 'librechat-data-provider';
import { isAssistantsEndpoint } from 'librechat-data-provider';
import type { SwitcherProps } from '~/common';
import { Separator } from '~/components/ui/Separator';
import AssistantSwitcher from './AssistantSwitcher';
import ModelSwitcher from './ModelSwitcher';
export default function Switcher(props: SwitcherProps) {
if (props.endpoint === EModelEndpoint.assistants && props.endpointKeyProvided) {
if (isAssistantsEndpoint(props.endpoint) && props.endpointKeyProvided) {
return (
<>
<AssistantSwitcher {...props} />
<Separator className="bg-gray-100/50 dark:bg-gray-600" />
</>
);
} else if (props.endpoint === EModelEndpoint.assistants) {
} else if (isAssistantsEndpoint(props.endpoint)) {
return null;
}