🤖 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
This commit is contained in:
Danny Avila 2024-05-14 13:36:33 -04:00
parent f0e8cca5df
commit 2bdbff5141
No known key found for this signature in database
GPG key ID: 2DD9CC89B9B50364
118 changed files with 3358 additions and 1039 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

@ -20,7 +20,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);
const progress = useProgress(file?.['progress'] ?? 1, 0.001, (file as ExtendedFile)?.size ?? 1);
console.log(progress);
// Calculate the offset based on the loading progress

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

@ -42,6 +42,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

@ -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

@ -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,21 @@
import { Plus } from 'lucide-react';
import { useCallback, useEffect, useRef } from 'react';
import {
defaultAssistantFormValues,
defaultOrderQuery,
isImageVisionTool,
EModelEndpoint,
Capabilities,
Tools,
FileSources,
Capabilities,
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 +29,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,11 +45,11 @@ 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 assistant = {
@ -55,30 +57,55 @@ export default function AssistantSelect({
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: FileSources.openai,
},
]);
} else {
list?.push([
file_id,
{
file_id,
type: '',
filename: '',
size: 1,
progress: 1,
filepath: endpoint,
source: FileSources.openai,
},
]);
}
};
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 +119,7 @@ export default function AssistantSelect({
setCurrentAssistantId(undefined);
return reset({
...defaultAssistantFormValues,
model: lastSelectedModels?.[EModelEndpoint.assistants] ?? '',
model: lastSelectedModels?.[endpoint] ?? '',
});
}
@ -112,6 +139,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 +171,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,71 @@
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}
tool_resource={Capabilities.code_interpreter}
/>
)}
</>
);
}

View file

@ -0,0 +1,93 @@
import { useState, useRef, useEffect } from 'react';
import { 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';
export default function Knowledge({
endpoint,
assistant_id,
files: _files,
tool_resource,
}: {
version: number | string;
endpoint: AssistantsEndpoint;
assistant_id: string;
files?: [string, ExtendedFile][];
tool_resource?: string;
}) {
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">
{assistant_id ? localize('com_assistants_knowledge_info') : ''}
</div>
<FileRow
files={files}
setFiles={setFiles}
setFilesLoading={setFilesLoading}
assistant_id={assistant_id}
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

@ -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,70 @@
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 { Checkbox } from '~/components/ui';
import { useLocalize } from '~/hooks';
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 files = useMemo(() => {
if (typeof assistant === 'string') {
return [];
}
return assistant.tool_resources?.file_search;
}, [assistant]);
useEffect(() => {
if (model && !retrievalModels.has(model)) {
setValue(Capabilities.retrieval, false);
}
}, [model, setValue, retrievalModels]);
return (
<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>
);
}

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,