🤖 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

@ -3,8 +3,8 @@ import { useForm } from 'react-hook-form';
import { memo, useCallback, useRef, useMemo } from 'react';
import {
supportsFiles,
EModelEndpoint,
mergeFileConfig,
isAssistantsEndpoint,
fileConfig as defaultFileConfig,
} from 'librechat-data-provider';
import { useChatContext, useAssistantsMapContext } from '~/Providers';
@ -74,8 +74,9 @@ const ChatForm = ({ index = 0 }) => {
const endpointFileConfig = fileConfig.endpoints[endpoint ?? ''];
const invalidAssistant = useMemo(
() =>
conversation?.endpoint === EModelEndpoint.assistants &&
(!conversation?.assistant_id || !assistantMap?.[conversation?.assistant_id ?? '']),
isAssistantsEndpoint(conversation?.endpoint) &&
(!conversation?.assistant_id ||
!assistantMap?.[conversation?.endpoint ?? '']?.[conversation?.assistant_id ?? '']),
[conversation?.assistant_id, conversation?.endpoint, assistantMap],
);
const disableInputs = useMemo(

View file

@ -2,6 +2,7 @@ import type { TFile } from 'librechat-data-provider';
import type { ExtendedFile } from '~/common';
import FileIcon from '~/components/svg/Files/FileIcon';
import ProgressCircle from './ProgressCircle';
import SourceIcon from './SourceIcon';
import { useProgress } from '~/hooks';
import { cn } from '~/utils';
@ -20,8 +21,7 @@ const FilePreview = ({
}) => {
const radius = 55; // Radius of the SVG circle
const circumference = 2 * Math.PI * radius;
const progress = useProgress(file?.['progress'] ?? 1, 0.001, file?.size ?? 1);
console.log(progress);
const progress = useProgress(file?.['progress'] ?? 1, 0.001, (file as ExtendedFile)?.size ?? 1);
// Calculate the offset based on the loading progress
const offset = circumference - progress * circumference;
@ -32,6 +32,7 @@ const FilePreview = ({
return (
<div className={cn('h-10 w-10 shrink-0 overflow-hidden rounded-md', className)}>
<FileIcon file={file} fileType={fileType} />
<SourceIcon source={file?.source} />
{progress < 1 && (
<ProgressCircle
circumference={circumference}

View file

@ -1,4 +1,5 @@
import { useEffect } from 'react';
import { EToolResources } from 'librechat-data-provider';
import type { ExtendedFile } from '~/common';
import { useDeleteFilesMutation } from '~/data-provider';
import { useFileDeletion } from '~/hooks/Files';
@ -10,6 +11,7 @@ export default function FileRow({
setFiles,
setFilesLoading,
assistant_id,
tool_resource,
fileFilter,
Wrapper,
}: {
@ -18,6 +20,7 @@ export default function FileRow({
setFilesLoading: React.Dispatch<React.SetStateAction<boolean>>;
fileFilter?: (file: ExtendedFile) => boolean;
assistant_id?: string;
tool_resource?: EToolResources;
Wrapper?: React.FC<{ children: React.ReactNode }>;
}) {
const files = Array.from(_files.values()).filter((file) =>
@ -25,7 +28,8 @@ export default function FileRow({
);
const { mutateAsync } = useDeleteFilesMutation({
onMutate: async () => console.log('Deleting files: assistant_id', assistant_id),
onMutate: async () =>
console.log('Deleting files: assistant_id, tool_resource', assistant_id, tool_resource),
onSuccess: () => {
console.log('Files deleted');
},
@ -34,7 +38,7 @@ export default function FileRow({
},
});
const { deleteFile } = useFileDeletion({ mutateAsync, assistant_id });
const { deleteFile } = useFileDeletion({ mutateAsync, assistant_id, tool_resource });
useEffect(() => {
if (!files) {
@ -82,6 +86,7 @@ export default function FileRow({
url={file.preview}
onDelete={handleDelete}
progress={file.progress}
source={file.source}
/>
);
}

View file

@ -12,16 +12,9 @@ export default function Files({ open, onOpenChange }) {
const { data: files = [] } = useGetFiles<TFile[]>({
select: (files) =>
files.map((file) => {
if (file.source === FileSources.local || file.source === FileSources.openai) {
file.context = file.context ?? FileContext.unknown;
return file;
} else {
return {
...file,
context: file.context ?? FileContext.unknown,
source: FileSources.local,
};
}
file.context = file.context ?? FileContext.unknown;
file.filterSource = file.source === FileSources.firebase ? FileSources.local : file.source;
return file;
}),
});

View file

@ -1,3 +1,4 @@
import { FileSources } from 'librechat-data-provider';
import ImagePreview from './ImagePreview';
import RemoveFile from './RemoveFile';
@ -6,16 +7,18 @@ const Image = ({
url,
onDelete,
progress = 1,
source = FileSources.local,
}: {
imageBase64?: string;
url?: string;
onDelete: () => void;
progress: number; // between 0 and 1
source?: FileSources;
}) => {
return (
<div className="group relative inline-block text-sm text-black/70 dark:text-white/90">
<div className="relative overflow-hidden rounded-xl border border-gray-200 dark:border-gray-600">
<ImagePreview imageBase64={imageBase64} url={url} progress={progress} />
<ImagePreview source={source} imageBase64={imageBase64} url={url} progress={progress} />
</div>
<RemoveFile onRemove={onDelete} />
</div>

View file

@ -1,4 +1,6 @@
import { FileSources } from 'librechat-data-provider';
import ProgressCircle from './ProgressCircle';
import SourceIcon from './SourceIcon';
import { cn } from '~/utils';
type styleProps = {
@ -13,11 +15,13 @@ const ImagePreview = ({
url,
progress = 1,
className = '',
source,
}: {
imageBase64?: string;
url?: string;
progress?: number; // between 0 and 1
className?: string;
source?: FileSources;
}) => {
let style: styleProps = {
backgroundSize: 'cover',
@ -65,6 +69,7 @@ const ImagePreview = ({
circleCSSProperties={circleCSSProperties}
/>
)}
<SourceIcon source={source} />
</div>
);
};

View file

@ -0,0 +1,45 @@
import { EModelEndpoint, FileSources } from 'librechat-data-provider';
import { MinimalIcon } from '~/components/Endpoints';
import { cn } from '~/utils';
const sourceToEndpoint = {
[FileSources.openai]: EModelEndpoint.openAI,
[FileSources.azure]: EModelEndpoint.azureOpenAI,
};
const sourceToClassname = {
[FileSources.openai]: 'bg-black/65',
[FileSources.azure]: 'azure-bg-color opacity-85',
};
const defaultClassName =
'absolute right-0 bottom-0 rounded-full p-[0.15rem] text-gray-600 transition-colors';
export default function SourceIcon({
source,
className = defaultClassName,
}: {
source?: FileSources;
className?: string;
}) {
if (source === FileSources.local || source === FileSources.firebase) {
return null;
}
const endpoint = sourceToEndpoint[source ?? ''];
if (!endpoint) {
return null;
}
return (
<button type="button" className={cn(className, sourceToClassname[source ?? ''] ?? '')}>
<span className="flex items-center justify-center">
<MinimalIcon
endpoint={endpoint}
size={14}
isCreatedByUser={false}
iconClassName="h-3 w-3"
/>
</span>
</button>
);
}

View file

@ -7,6 +7,7 @@ import ImagePreview from '~/components/Chat/Input/Files/ImagePreview';
import FilePreview from '~/components/Chat/Input/Files/FilePreview';
import { SortFilterHeader } from './SortFilterHeader';
import { OpenAIMinimalIcon } from '~/components/svg';
import { AzureMinimalIcon } from '~/components/svg';
import { Button, Checkbox } from '~/components/ui';
import { formatDate, getFileType } from '~/utils';
import useLocalize from '~/hooks/useLocalize';
@ -71,10 +72,11 @@ export const columns: ColumnDef<TFile>[] = [
const file = row.original;
if (file.type?.startsWith('image')) {
return (
<div className="flex gap-2 ">
<div className="flex gap-2">
<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 ">{file.filename}</span>
</div>
@ -84,7 +86,7 @@ export const columns: ColumnDef<TFile>[] = [
const fileType = getFileType(file.type);
return (
<div className="flex gap-2">
{fileType && <FilePreview fileType={fileType} />}
{fileType && <FilePreview fileType={fileType} className="relative" file={file} />}
<span className="self-center truncate">{file.filename}</span>
</div>
);
@ -108,7 +110,7 @@ export const columns: ColumnDef<TFile>[] = [
cell: ({ row }) => formatDate(row.original.updatedAt),
},
{
accessorKey: 'source',
accessorKey: 'filterSource',
header: ({ column }) => {
const localize = useLocalize();
return (
@ -117,10 +119,14 @@ export const columns: ColumnDef<TFile>[] = [
title={localize('com_ui_storage')}
filters={{
Storage: Object.values(FileSources).filter(
(value) => value === FileSources.local || value === FileSources.openai,
(value) =>
value === FileSources.local ||
value === FileSources.openai ||
value === FileSources.azure,
),
}}
valueMap={{
[FileSources.azure]: 'Azure',
[FileSources.openai]: 'OpenAI',
[FileSources.local]: 'com_ui_host',
}}
@ -137,6 +143,13 @@ export const columns: ColumnDef<TFile>[] = [
{'OpenAI'}
</div>
);
} else if (source === FileSources.azure) {
return (
<div className="flex flex-wrap items-center gap-2">
<AzureMinimalIcon className="icon-sm text-cyan-700" />
{'Azure'}
</div>
);
}
return (
<div className="flex flex-wrap items-center gap-2">

View file

@ -48,7 +48,12 @@ const contextMap = {
[FileContext.bytes]: 'com_ui_size',
};
type Style = { width?: number | string; maxWidth?: number | string; minWidth?: number | string };
type Style = {
width?: number | string;
maxWidth?: number | string;
minWidth?: number | string;
zIndex?: number;
};
export default function DataTable<TData, TValue>({ columns, data }: DataTableProps<TData, TValue>) {
const localize = useLocalize();
@ -142,7 +147,7 @@ export default function DataTable<TData, TValue>({ columns, data }: DataTablePro
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header, index) => {
const style: Style = { maxWidth: '32px', minWidth: '125px' };
const style: Style = { maxWidth: '32px', minWidth: '125px', zIndex: 50 };
if (header.id === 'filename') {
style.maxWidth = '50%';
style.width = '50%';

View file

@ -17,7 +17,9 @@ export default function Mention({
}) {
const localize = useLocalize();
const assistantMap = useAssistantsMapContext();
const { options, modelsConfig, assistants, onSelectMention } = useMentions({ assistantMap });
const { options, modelsConfig, assistantListMap, onSelectMention } = useMentions({
assistantMap,
});
const [activeIndex, setActiveIndex] = useState(0);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
@ -47,7 +49,12 @@ export default function Mention({
if (mention.type === 'endpoint' && mention.value === EModelEndpoint.assistants) {
setSearchValue('');
setInputOptions(assistants);
setInputOptions(assistantListMap[EModelEndpoint.assistants]);
setActiveIndex(0);
inputRef.current?.focus();
} else if (mention.type === 'endpoint' && mention.value === EModelEndpoint.azureAssistants) {
setSearchValue('');
setInputOptions(assistantListMap[EModelEndpoint.azureAssistants]);
setActiveIndex(0);
inputRef.current?.focus();
} else if (mention.type === 'endpoint') {

View file

@ -1,4 +1,4 @@
import { EModelEndpoint } from 'librechat-data-provider';
import { EModelEndpoint, isAssistantsEndpoint } from 'librechat-data-provider';
import { useGetEndpointsQuery, useGetStartupConfig } from 'librechat-data-provider/react-query';
import type { ReactNode } from 'react';
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '~/components/ui';
@ -30,7 +30,8 @@ export default function Landing({ Header }: { Header?: ReactNode }) {
const iconURL = conversation?.iconURL;
endpoint = getIconEndpoint({ endpointsConfig, iconURL, endpoint });
const assistant = endpoint === EModelEndpoint.assistants && assistantMap?.[assistant_id ?? ''];
const isAssistant = isAssistantsEndpoint(endpoint);
const assistant = isAssistant && assistantMap?.[endpoint]?.[assistant_id ?? ''];
const assistantName = (assistant && assistant?.name) || '';
const assistantDesc = (assistant && assistant?.description) || '';
const avatar = (assistant && (assistant?.metadata?.avatar as string)) || '';
@ -77,7 +78,7 @@ export default function Landing({ Header }: { Header?: ReactNode }) {
</div>
) : (
<div className="mb-5 max-w-[75vh] px-12 text-center text-lg font-medium dark:text-white md:px-0 md:text-2xl">
{endpoint === EModelEndpoint.assistants
{isAssistant
? conversation?.greeting ?? localize('com_nav_welcome_assistant')
: conversation?.greeting ?? localize('com_nav_welcome_message')}
</div>

View file

@ -15,6 +15,24 @@ import {
import UnknownIcon from './UnknownIcon';
import { cn } from '~/utils';
const AssistantAvatar = ({ className = '', assistantName, avatar, size }: IconMapProps) => {
if (assistantName && avatar) {
return (
<img
src={avatar}
className="bg-token-surface-secondary dark:bg-token-surface-tertiary h-full w-full rounded-full object-cover"
alt={assistantName}
width="80"
height="80"
/>
);
} else if (assistantName) {
return <AssistantIcon className={cn('text-token-secondary', className)} size={size} />;
}
return <Sparkles className={cn(assistantName === '' ? 'icon-2xl' : '', className)} />;
};
export const icons = {
[EModelEndpoint.azureOpenAI]: AzureMinimalIcon,
[EModelEndpoint.openAI]: GPTIcon,
@ -24,22 +42,7 @@ export const icons = {
[EModelEndpoint.google]: GoogleMinimalIcon,
[EModelEndpoint.bingAI]: BingAIMinimalIcon,
[EModelEndpoint.custom]: CustomMinimalIcon,
[EModelEndpoint.assistants]: ({ className = '', assistantName, avatar, size }: IconMapProps) => {
if (assistantName && avatar) {
return (
<img
src={avatar}
className="bg-token-surface-secondary dark:bg-token-surface-tertiary h-full w-full rounded-full object-cover"
alt={assistantName}
width="80"
height="80"
/>
);
} else if (assistantName) {
return <AssistantIcon className={cn('text-token-secondary', className)} size={size} />;
}
return <Sparkles className={cn(assistantName === '' ? 'icon-2xl' : '', className)} />;
},
[EModelEndpoint.assistants]: AssistantAvatar,
[EModelEndpoint.azureAssistants]: AssistantAvatar,
unknown: UnknownIcon,
};

View file

@ -1,5 +1,5 @@
import { Content, Portal, Root } from '@radix-ui/react-popover';
import { alternateName, EModelEndpoint } from 'librechat-data-provider';
import { alternateName, isAssistantsEndpoint } from 'librechat-data-provider';
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import type { FC } from 'react';
import { useChatContext, useAssistantsMapContext } from '~/Providers';
@ -16,7 +16,8 @@ const EndpointsMenu: FC = () => {
const { endpoint = '', assistant_id = null } = conversation ?? {};
const assistantMap = useAssistantsMapContext();
const assistant = endpoint === EModelEndpoint.assistants && assistantMap?.[assistant_id ?? ''];
const assistant =
isAssistantsEndpoint(endpoint) && assistantMap?.[endpoint ?? '']?.[assistant_id ?? ''];
const assistantName = (assistant && assistant?.name) || 'Assistant';
if (!endpoint) {

View file

@ -1,6 +1,7 @@
import { useState } from 'react';
import { useRecoilValue } from 'recoil';
import ProgressCircle from './ProgressCircle';
import CancelledIcon from './CancelledIcon';
import ProgressText from './ProgressText';
import FinishedIcon from './FinishedIcon';
import MarkdownLite from './MarkdownLite';
@ -11,10 +12,12 @@ export default function CodeAnalyze({
initialProgress = 0.1,
code,
outputs = [],
isSubmitting,
}: {
initialProgress: number;
code: string;
outputs: Record<string, unknown>[];
isSubmitting: boolean;
}) {
const showCodeDefault = useRecoilValue(store.showCode);
const [showCode, setShowCode] = useState(showCodeDefault);
@ -35,7 +38,13 @@ export default function CodeAnalyze({
<div className="my-2.5 flex items-center gap-2.5">
<div className="relative h-5 w-5 shrink-0">
{progress < 1 ? (
<CodeInProgress offset={offset} circumference={circumference} radius={radius} />
<CodeInProgress
offset={offset}
radius={radius}
progress={progress}
isSubmitting={isSubmitting}
circumference={circumference}
/>
) : (
<FinishedIcon />
)}
@ -74,18 +83,25 @@ const CodeInProgress = ({
offset,
circumference,
radius,
isSubmitting,
progress,
}: {
progress: number;
offset: number;
circumference: number;
radius: number;
isSubmitting: boolean;
}) => {
if (progress < 1 && !isSubmitting) {
return <CancelledIcon />;
}
return (
<div
className="absolute left-0 top-0 flex h-full w-full items-center justify-center rounded-full bg-transparent text-white"
style={{ opacity: 1, transform: 'none' }}
data-projection-id="77"
>
<div className='absolute right-[1.5px] bottom-[1.5px]'>
<div className="absolute bottom-[1.5px] right-[1.5px]">
<svg
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"

View file

@ -79,11 +79,13 @@ export default function Part({
initialProgress={toolCall.progress ?? 0.1}
code={code_interpreter.input}
outputs={code_interpreter.outputs ?? []}
isSubmitting={isSubmitting}
/>
);
} else if (
part.type === ContentTypes.TOOL_CALL &&
part[ContentTypes.TOOL_CALL].type === ToolCallTypes.RETRIEVAL
(part[ContentTypes.TOOL_CALL].type === ToolCallTypes.RETRIEVAL ||
part[ContentTypes.TOOL_CALL].type === ToolCallTypes.FILE_SEARCH)
) {
const toolCall = part[ContentTypes.TOOL_CALL];
return <RetrievalCall initialProgress={toolCall.progress ?? 0.1} isSubmitting={isSubmitting} />;

View file

@ -1,5 +1,4 @@
import { useState } from 'react';
import { EModelEndpoint } from 'librechat-data-provider';
import type { TConversation, TMessage } from 'librechat-data-provider';
import { Clipboard, CheckMark, EditIcon, RegenerateIcon, ContinueIcon } from '~/components/svg';
import { useGenerationsByLatest, useLocalize } from '~/hooks';
@ -35,14 +34,19 @@ export default function HoverButtons({
const { endpoint: _endpoint, endpointType } = conversation ?? {};
const endpoint = endpointType ?? _endpoint;
const [isCopied, setIsCopied] = useState(false);
const { hideEditButton, regenerateEnabled, continueSupported, forkingSupported } =
useGenerationsByLatest({
isEditing,
isSubmitting,
message,
endpoint: endpoint ?? '',
latestMessage,
});
const {
hideEditButton,
regenerateEnabled,
continueSupported,
forkingSupported,
isEditableEndpoint,
} = useGenerationsByLatest({
isEditing,
isSubmitting,
message,
endpoint: endpoint ?? '',
latestMessage,
});
if (!conversation) {
return null;
}
@ -58,7 +62,7 @@ export default function HoverButtons({
return (
<div className="visible mt-0 flex justify-center gap-1 self-end text-gray-400 lg:justify-start">
{endpoint !== EModelEndpoint.assistants && (
{isEditableEndpoint && (
<button
className={cn(
'hover-button rounded-md p-1 text-gray-400 hover:text-gray-900 dark:text-gray-400/70 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:group-hover:visible md:group-[.final-completion]:visible',

View file

@ -1,8 +1,13 @@
import { EModelEndpoint } from 'librechat-data-provider';
import type { Assistant, TConversation, TEndpointsConfig, TPreset } from 'librechat-data-provider';
import { isAssistantsEndpoint } from 'librechat-data-provider';
import type {
TAssistantsMap,
TConversation,
TEndpointsConfig,
TPreset,
} from 'librechat-data-provider';
import { getEndpointField, getIconKey, getIconEndpoint } from '~/utils';
import { icons } from '~/components/Chat/Menus/Endpoints/Icons';
import ConvoIconURL from '~/components/Endpoints/ConvoIconURL';
import { getEndpointField, getIconKey, getIconEndpoint } from '~/utils';
export default function ConvoIcon({
conversation,
@ -15,7 +20,7 @@ export default function ConvoIcon({
}: {
conversation: TConversation | TPreset | null;
endpointsConfig: TEndpointsConfig;
assistantMap: Record<string, Assistant>;
assistantMap: TAssistantsMap;
containerClassName?: string;
context?: 'message' | 'nav' | 'landing' | 'menu-item';
className?: string;
@ -25,7 +30,7 @@ export default function ConvoIcon({
let endpoint = conversation?.endpoint;
endpoint = getIconEndpoint({ endpointsConfig, iconURL, endpoint });
const assistant =
endpoint === EModelEndpoint.assistants && assistantMap?.[conversation?.assistant_id ?? ''];
isAssistantsEndpoint(endpoint) && assistantMap?.[endpoint]?.[conversation?.assistant_id ?? ''];
const assistantName = (assistant && assistant?.name) || '';
const avatar = (assistant && (assistant?.metadata?.avatar as string)) || '';

View file

@ -1,5 +1,10 @@
import { EModelEndpoint } from 'librechat-data-provider';
import type { Assistant, TConversation, TEndpointsConfig, TPreset } from 'librechat-data-provider';
import { isAssistantsEndpoint } from 'librechat-data-provider';
import type {
TConversation,
TEndpointsConfig,
TPreset,
TAssistantsMap,
} from 'librechat-data-provider';
import ConvoIconURL from '~/components/Endpoints/ConvoIconURL';
import MinimalIcon from '~/components/Endpoints/MinimalIcon';
import { getEndpointField, getIconEndpoint } from '~/utils';
@ -15,7 +20,7 @@ export default function EndpointIcon({
endpointsConfig: TEndpointsConfig;
containerClassName?: string;
context?: 'message' | 'nav' | 'landing' | 'menu-item';
assistantMap?: Record<string, Assistant>;
assistantMap?: TAssistantsMap;
className?: string;
size?: number;
}) {
@ -27,7 +32,7 @@ export default function EndpointIcon({
const endpointIconURL = getEndpointField(endpointsConfig, endpoint, 'iconURL');
const assistant =
endpoint === EModelEndpoint.assistants && assistantMap?.[conversation?.assistant_id ?? ''];
isAssistantsEndpoint(endpoint) && assistantMap?.[endpoint]?.[conversation?.assistant_id ?? ''];
const assistantAvatar = (assistant && (assistant?.metadata?.avatar as string)) || '';
const assistantName = (assistant && assistant?.name) || '';

View file

@ -1,4 +1,4 @@
import { EModelEndpoint } from 'librechat-data-provider';
import { EModelEndpoint, isAssistantsEndpoint } from 'librechat-data-provider';
import UnknownIcon from '~/components/Chat/Menus/Endpoints/UnknownIcon';
import {
Plugin,
@ -27,35 +27,38 @@ const MessageEndpointIcon: React.FC<IconProps> = (props) => {
assistantName,
} = props;
const assistantsIcon = {
icon: props.iconURL ? (
<div className="relative flex h-6 w-6 items-center justify-center">
<div
title={assistantName}
style={{
width: size,
height: size,
}}
className={cn('overflow-hidden rounded-full', props.className ?? '')}
>
<img
className="shadow-stroke h-full w-full object-cover"
src={props.iconURL}
alt={assistantName}
style={{ height: '80', width: '80' }}
/>
</div>
</div>
) : (
<div className="h-6 w-6">
<div className="shadow-stroke flex h-6 w-6 items-center justify-center overflow-hidden rounded-full">
<AssistantIcon className="h-2/3 w-2/3 text-gray-400" />
</div>
</div>
),
name: endpoint,
};
const endpointIcons = {
[EModelEndpoint.assistants]: {
icon: props.iconURL ? (
<div className="relative flex h-6 w-6 items-center justify-center">
<div
title={assistantName}
style={{
width: size,
height: size,
}}
className={cn('overflow-hidden rounded-full', props.className ?? '')}
>
<img
className="shadow-stroke h-full w-full object-cover"
src={props.iconURL}
alt={assistantName}
style={{ height: '80', width: '80' }}
/>
</div>
</div>
) : (
<div className="h-6 w-6">
<div className="shadow-stroke flex h-6 w-6 items-center justify-center overflow-hidden rounded-full">
<AssistantIcon className="h-2/3 w-2/3 text-gray-400" />
</div>
</div>
),
name: endpoint,
},
[EModelEndpoint.assistants]: assistantsIcon,
[EModelEndpoint.azureAssistants]: assistantsIcon,
[EModelEndpoint.azureOpenAI]: {
icon: <AzureMinimalIcon size={size * 0.5555555555555556} />,
bg: 'linear-gradient(0.375turn, #61bde2, #4389d0)',
@ -136,7 +139,7 @@ const MessageEndpointIcon: React.FC<IconProps> = (props) => {
({ icon, bg, name } = endpointIcons[iconURL]);
}
if (endpoint === EModelEndpoint.assistants) {
if (isAssistantsEndpoint(endpoint)) {
return icon;
}

View file

@ -15,7 +15,7 @@ import { cn } from '~/utils';
import { IconProps } from '~/common';
const MinimalIcon: React.FC<IconProps> = (props) => {
const { size = 30, error } = props;
const { size = 30, iconClassName, error } = props;
let endpoint = 'default'; // Default value for endpoint
@ -25,10 +25,13 @@ const MinimalIcon: React.FC<IconProps> = (props) => {
const endpointIcons = {
[EModelEndpoint.azureOpenAI]: {
icon: <AzureMinimalIcon />,
icon: <AzureMinimalIcon className={iconClassName} />,
name: props.chatGptLabel || 'ChatGPT',
},
[EModelEndpoint.openAI]: {
icon: <OpenAIMinimalIcon className={iconClassName} />,
name: props.chatGptLabel || 'ChatGPT',
},
[EModelEndpoint.openAI]: { icon: <OpenAIMinimalIcon />, name: props.chatGptLabel || 'ChatGPT' },
[EModelEndpoint.gptPlugins]: { icon: <MinimalPlugin />, name: 'Plugins' },
[EModelEndpoint.google]: { icon: <GoogleMinimalIcon />, name: props.modelLabel || 'Google' },
[EModelEndpoint.anthropic]: {
@ -42,6 +45,7 @@ const MinimalIcon: React.FC<IconProps> = (props) => {
[EModelEndpoint.bingAI]: { icon: <BingAIMinimalIcon />, name: 'BingAI' },
[EModelEndpoint.chatGPTBrowser]: { icon: <LightningIcon />, name: 'ChatGPT' },
[EModelEndpoint.assistants]: { icon: <Sparkles className="icon-sm" />, name: 'Assistant' },
[EModelEndpoint.azureAssistants]: { icon: <Sparkles className="icon-sm" />, name: 'Assistant' },
default: {
icon: (
<UnknownIcon

View file

@ -1,5 +1,7 @@
import TextareaAutosize from 'react-textarea-autosize';
import { ImageDetail, imageDetailNumeric, imageDetailValue } from 'librechat-data-provider';
import type { ValueType } from '@rc-component/mini-decimal';
import type { TModelSelectProps } from '~/common';
import {
Input,
Label,
@ -11,7 +13,6 @@ import {
} from '~/components/ui';
import { cn, defaultTextProps, optionText, removeFocusOutlines } from '~/utils/';
import { useLocalize, useDebouncedInput } from '~/hooks';
import type { TModelSelectProps } from '~/common';
import OptionHover from './OptionHover';
import { ESide } from '~/common';
@ -127,7 +128,7 @@ export default function Settings({
id="temp-int"
disabled={readonly}
value={temperatureValue as number}
onChange={setTemperature}
onChange={setTemperature as (value: ValueType | null) => void}
max={2}
min={0}
step={0.01}

View file

@ -1,12 +1,10 @@
import { useState, useMemo, useEffect } from 'react';
import TextareaAutosize from 'react-textarea-autosize';
import { defaultOrderQuery } from 'librechat-data-provider';
import type { TPreset } from 'librechat-data-provider';
import type { TModelSelectProps, Option } from '~/common';
import { Label, HoverCard, SelectDropDown, HoverCardTrigger } from '~/components/ui';
import { cn, defaultTextProps, removeFocusOutlines, mapAssistants } from '~/utils';
import { useLocalize, useDebouncedInput } from '~/hooks';
import { useListAssistantsQuery } from '~/data-provider';
import { useLocalize, useDebouncedInput, useAssistantListMap } from '~/hooks';
import OptionHover from './OptionHover';
import { ESide } from '~/common';
@ -17,23 +15,21 @@ export default function Settings({ conversation, setOption, models, readonly }:
[localize],
);
const { data: assistants = [] } = useListAssistantsQuery(defaultOrderQuery, {
select: (res) =>
[
defaultOption,
...res.data.map(({ id, name }) => ({
label: name,
value: id,
})),
].filter(Boolean),
});
const { data: assistantMap = {} } = useListAssistantsQuery(defaultOrderQuery, {
select: (res) => mapAssistants(res.data),
});
const assistantListMap = useAssistantListMap((res) => mapAssistants(res.data));
const { model, endpoint, assistant_id, endpointType, promptPrefix, instructions } =
conversation ?? {};
const assistants = useMemo(() => {
return [
defaultOption,
...(assistantListMap[endpoint ?? ''] ?? []).map(({ id, name }) => ({
label: name,
value: id,
})),
].filter(Boolean);
}, [assistantListMap, endpoint, defaultOption]);
const [onPromptPrefixChange, promptPrefixValue] = useDebouncedInput({
setOption,
optionKey: 'promptPrefix',
@ -47,11 +43,11 @@ export default function Settings({ conversation, setOption, models, readonly }:
const activeAssistant = useMemo(() => {
if (assistant_id) {
return assistantMap[assistant_id];
return assistantListMap[endpoint ?? '']?.[assistant_id];
}
return null;
}, [assistant_id, assistantMap]);
}, [assistant_id, assistantListMap, endpoint]);
const modelOptions = useMemo(() => {
return models.map((model) => ({
@ -89,7 +85,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
return;
}
const assistant = assistantMap[value];
const assistant = assistantListMap[endpoint ?? '']?.[value];
if (!assistant) {
setAssistantValue(defaultOption);
return;

View file

@ -9,6 +9,7 @@ import OpenAISettings from './OpenAI';
const settings: { [key: string]: FC<TModelSelectProps> } = {
[EModelEndpoint.assistants]: AssistantsSettings,
[EModelEndpoint.azureAssistants]: AssistantsSettings,
[EModelEndpoint.openAI]: OpenAISettings,
[EModelEndpoint.custom]: OpenAISettings,
[EModelEndpoint.azureOpenAI]: OpenAISettings,

View file

@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { useForm, FormProvider } from 'react-hook-form';
import { EModelEndpoint, alternateName } from 'librechat-data-provider';
import { EModelEndpoint, alternateName, isAssistantsEndpoint } from 'librechat-data-provider';
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import type { TDialogProps } from '~/common';
import DialogTemplate from '~/components/ui/DialogTemplate';
@ -21,6 +21,7 @@ const endpointComponents = {
[EModelEndpoint.azureOpenAI]: OpenAIConfig,
[EModelEndpoint.gptPlugins]: OpenAIConfig,
[EModelEndpoint.assistants]: OpenAIConfig,
[EModelEndpoint.azureAssistants]: OpenAIConfig,
default: OtherConfig,
};
@ -30,6 +31,7 @@ const formSet: Set<string> = new Set([
EModelEndpoint.azureOpenAI,
EModelEndpoint.gptPlugins,
EModelEndpoint.assistants,
EModelEndpoint.azureAssistants,
]);
const EXPIRY = {
@ -97,7 +99,7 @@ const SetKeyDialog = ({
isAzure ||
endpoint === EModelEndpoint.openAI ||
endpoint === EModelEndpoint.gptPlugins ||
endpoint === EModelEndpoint.assistants;
isAssistantsEndpoint(endpoint);
if (isAzure) {
data.apiKey = 'n/a';
}

View file

@ -71,6 +71,7 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) =
}}
placeholder={localize('com_nav_search_placeholder')}
onKeyUp={handleKeyUp}
autoComplete="off"
/>
<X
className={cn(

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;
}

View file

@ -3,7 +3,7 @@ import { Search, X } from 'lucide-react';
import { Dialog } from '@headlessui/react';
import { useFormContext } from 'react-hook-form';
import { useUpdateUserPluginsMutation } from 'librechat-data-provider/react-query';
import type { TError, TPluginAction } from 'librechat-data-provider';
import type { AssistantsEndpoint, TError, TPluginAction } from 'librechat-data-provider';
import type { TPluginStoreDialogProps } from '~/common/types';
import { PluginPagination, PluginAuthForm } from '~/components/Plugins/Store';
import { useLocalize, usePluginDialogHelpers } from '~/hooks';
@ -13,10 +13,11 @@ import ToolItem from './ToolItem';
function ToolSelectDialog({
isOpen,
setIsOpen,
}: TPluginStoreDialogProps & { assistant_id?: string }) {
endpoint,
}: TPluginStoreDialogProps & { assistant_id?: string; endpoint: AssistantsEndpoint }) {
const localize = useLocalize();
const { getValues, setValue } = useFormContext();
const { data: tools = [] } = useAvailableToolsQuery();
const { data: tools = [] } = useAvailableToolsQuery(endpoint);
const {
maxPage,