🔁 refactor: Capabilities for Tools/File handling for Direct Endpoints (#8253)

* feat: add useAgentCapabilities hook to manage agent capabilities

* refactor: move  agents and endpoints configuration to AgentPanel context provider

* refactor: implement useGetAgentsConfig hook for consolidated agents and endpoints management

* refactor: enhance ToolsDropdown to utilize agent capabilities and streamline dropdown item rendering

* chore: reorder return values in useAgentCapabilities for improved clarity

* refactor: enhance agent capabilities handling in AttachFileMenu and update file handling logic to allow capabilities to be used for non-agents endpoints
This commit is contained in:
Danny Avila 2025-07-04 14:51:26 -04:00 committed by GitHub
parent a288ad1d9c
commit f5511e4a4e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 308 additions and 250 deletions

View file

@ -2,11 +2,10 @@ import { useSetRecoilState } from 'recoil';
import * as Ariakit from '@ariakit/react';
import React, { useRef, useState, useMemo } from 'react';
import { FileSearch, ImageUpIcon, TerminalSquareIcon, FileType2Icon } from 'lucide-react';
import { EToolResources, EModelEndpoint, defaultAgentCapabilities } from 'librechat-data-provider';
import type { EndpointFileConfig } from 'librechat-data-provider';
import { useLocalize, useGetAgentsConfig, useFileHandling, useAgentCapabilities } from '~/hooks';
import { FileUpload, TooltipAnchor, DropdownPopup, AttachmentIcon } from '~/components';
import { EToolResources, EModelEndpoint } from 'librechat-data-provider';
import { useGetEndpointsQuery } from '~/data-provider';
import { useLocalize, useFileHandling } from '~/hooks';
import { ephemeralAgentByConvoId } from '~/store';
import { cn } from '~/utils';
@ -23,20 +22,17 @@ const AttachFileMenu = ({ disabled, conversationId, endpointFileConfig }: Attach
const [isPopoverActive, setIsPopoverActive] = useState(false);
const setEphemeralAgent = useSetRecoilState(ephemeralAgentByConvoId(conversationId));
const [toolResource, setToolResource] = useState<EToolResources | undefined>();
const { data: endpointsConfig } = useGetEndpointsQuery();
const { handleFileChange } = useFileHandling({
overrideEndpoint: EModelEndpoint.agents,
overrideEndpointFileConfig: endpointFileConfig,
});
const { agentsConfig } = useGetAgentsConfig();
/** TODO: Ephemeral Agent Capabilities
* Allow defining agent capabilities on a per-endpoint basis
* Use definition for agents endpoint for ephemeral agents
* */
const capabilities = useMemo(
() => endpointsConfig?.[EModelEndpoint.agents]?.capabilities ?? [],
[endpointsConfig],
);
const capabilities = useAgentCapabilities(agentsConfig?.capabilities ?? defaultAgentCapabilities);
const handleUploadClick = (isImage?: boolean) => {
if (!inputRef.current) {
@ -60,7 +56,7 @@ const AttachFileMenu = ({ disabled, conversationId, endpointFileConfig }: Attach
},
];
if (capabilities.includes(EToolResources.ocr)) {
if (capabilities.ocrEnabled) {
items.push({
label: localize('com_ui_upload_ocr_text'),
onClick: () => {
@ -71,7 +67,7 @@ const AttachFileMenu = ({ disabled, conversationId, endpointFileConfig }: Attach
});
}
if (capabilities.includes(EToolResources.file_search)) {
if (capabilities.fileSearchEnabled) {
items.push({
label: localize('com_ui_upload_file_search'),
onClick: () => {
@ -83,7 +79,7 @@ const AttachFileMenu = ({ disabled, conversationId, endpointFileConfig }: Attach
});
}
if (capabilities.includes(EToolResources.execute_code)) {
if (capabilities.codeEnabled) {
items.push({
label: localize('com_ui_upload_code_files'),
onClick: () => {

View file

@ -2,12 +2,18 @@ import React, { useState, useMemo, useCallback } from 'react';
import * as Ariakit from '@ariakit/react';
import { Globe, Settings, Settings2, TerminalSquareIcon } from 'lucide-react';
import type { MenuItemProps } from '~/common';
import { Permissions, PermissionTypes, AuthType, ArtifactModes } from 'librechat-data-provider';
import {
AuthType,
Permissions,
ArtifactModes,
PermissionTypes,
defaultAgentCapabilities,
} from 'librechat-data-provider';
import { TooltipAnchor, DropdownPopup } from '~/components';
import { useLocalize, useHasAccess, useAgentCapabilities } from '~/hooks';
import ArtifactsSubMenu from '~/components/Chat/Input/ArtifactsSubMenu';
import MCPSubMenu from '~/components/Chat/Input/MCPSubMenu';
import { PinIcon, VectorIcon } from '~/components/svg';
import { useLocalize, useHasAccess } from '~/hooks';
import { useBadgeRowContext } from '~/Providers';
import { cn } from '~/utils';
@ -24,11 +30,15 @@ const ToolsDropdown = ({ disabled }: ToolsDropdownProps) => {
mcpSelect,
artifacts,
fileSearch,
agentsConfig,
startupConfig,
codeApiKeyForm,
codeInterpreter,
searchApiKeyForm,
} = useBadgeRowContext();
const { codeEnabled, webSearchEnabled, artifactsEnabled, fileSearchEnabled } =
useAgentCapabilities(agentsConfig?.capabilities ?? defaultAgentCapabilities);
const { setIsDialogOpen: setIsCodeDialogOpen, menuTriggerRef: codeMenuTriggerRef } =
codeApiKeyForm;
const { setIsDialogOpen: setIsSearchDialogOpen, menuTriggerRef: searchMenuTriggerRef } =
@ -128,9 +138,10 @@ const ToolsDropdown = ({ disabled }: ToolsDropdownProps) => {
const mcpPlaceholder = startupConfig?.interface?.mcpServers?.placeholder;
const dropdownItems = useMemo(() => {
const items: MenuItemProps[] = [];
items.push({
const dropdownItems: MenuItemProps[] = [];
if (fileSearchEnabled) {
dropdownItems.push({
onClick: handleFileSearchToggle,
hideOnClick: false,
render: (props) => (
@ -159,117 +170,118 @@ const ToolsDropdown = ({ disabled }: ToolsDropdownProps) => {
</div>
),
});
}
if (canUseWebSearch) {
items.push({
onClick: handleWebSearchToggle,
hideOnClick: false,
render: (props) => (
<div {...props}>
<div className="flex items-center gap-2">
<Globe className="icon-md" />
<span>{localize('com_ui_web_search')}</span>
</div>
<div className="flex items-center gap-1">
{showWebSearchSettings && (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
setIsSearchDialogOpen(true);
}}
className={cn(
'rounded p-1 transition-all duration-200',
'hover:bg-surface-secondary hover:shadow-sm',
'text-text-secondary hover:text-text-primary',
)}
aria-label="Configure web search"
ref={searchMenuTriggerRef}
>
<div className="h-4 w-4">
<Settings className="h-4 w-4" />
</div>
</button>
)}
if (canUseWebSearch && webSearchEnabled) {
dropdownItems.push({
onClick: handleWebSearchToggle,
hideOnClick: false,
render: (props) => (
<div {...props}>
<div className="flex items-center gap-2">
<Globe className="icon-md" />
<span>{localize('com_ui_web_search')}</span>
</div>
<div className="flex items-center gap-1">
{showWebSearchSettings && (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
setIsSearchPinned(!isSearchPinned);
setIsSearchDialogOpen(true);
}}
className={cn(
'rounded p-1 transition-all duration-200',
'hover:bg-surface-secondary hover:shadow-sm',
!isSearchPinned && 'text-text-secondary hover:text-text-primary',
'text-text-secondary hover:text-text-primary',
)}
aria-label={isSearchPinned ? 'Unpin' : 'Pin'}
aria-label="Configure web search"
ref={searchMenuTriggerRef}
>
<div className="h-4 w-4">
<PinIcon unpin={isSearchPinned} />
<Settings className="h-4 w-4" />
</div>
</button>
</div>
</div>
),
});
}
if (canRunCode) {
items.push({
onClick: handleCodeInterpreterToggle,
hideOnClick: false,
render: (props) => (
<div {...props}>
<div className="flex items-center gap-2">
<TerminalSquareIcon className="icon-md" />
<span>{localize('com_assistants_code_interpreter')}</span>
</div>
<div className="flex items-center gap-1">
{showCodeSettings && (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
setIsCodeDialogOpen(true);
}}
ref={codeMenuTriggerRef}
className={cn(
'rounded p-1 transition-all duration-200',
'hover:bg-surface-secondary hover:shadow-sm',
'text-text-secondary hover:text-text-primary',
)}
aria-label="Configure code interpreter"
>
<div className="h-4 w-4">
<Settings className="h-4 w-4" />
</div>
</button>
)}
<button
type="button"
onClick={(e) => {
e.stopPropagation();
setIsSearchPinned(!isSearchPinned);
}}
className={cn(
'rounded p-1 transition-all duration-200',
'hover:bg-surface-secondary hover:shadow-sm',
!isSearchPinned && 'text-text-secondary hover:text-text-primary',
)}
aria-label={isSearchPinned ? 'Unpin' : 'Pin'}
>
<div className="h-4 w-4">
<PinIcon unpin={isSearchPinned} />
</div>
</button>
</div>
</div>
),
});
}
if (canRunCode && codeEnabled) {
dropdownItems.push({
onClick: handleCodeInterpreterToggle,
hideOnClick: false,
render: (props) => (
<div {...props}>
<div className="flex items-center gap-2">
<TerminalSquareIcon className="icon-md" />
<span>{localize('com_assistants_code_interpreter')}</span>
</div>
<div className="flex items-center gap-1">
{showCodeSettings && (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
setIsCodePinned(!isCodePinned);
setIsCodeDialogOpen(true);
}}
ref={codeMenuTriggerRef}
className={cn(
'rounded p-1 transition-all duration-200',
'hover:bg-surface-secondary hover:shadow-sm',
!isCodePinned && 'text-text-primary hover:text-text-primary',
'text-text-secondary hover:text-text-primary',
)}
aria-label={isCodePinned ? 'Unpin' : 'Pin'}
aria-label="Configure code interpreter"
>
<div className="h-4 w-4">
<PinIcon unpin={isCodePinned} />
<Settings className="h-4 w-4" />
</div>
</button>
</div>
)}
<button
type="button"
onClick={(e) => {
e.stopPropagation();
setIsCodePinned(!isCodePinned);
}}
className={cn(
'rounded p-1 transition-all duration-200',
'hover:bg-surface-secondary hover:shadow-sm',
!isCodePinned && 'text-text-primary hover:text-text-primary',
)}
aria-label={isCodePinned ? 'Unpin' : 'Pin'}
>
<div className="h-4 w-4">
<PinIcon unpin={isCodePinned} />
</div>
</button>
</div>
),
});
}
</div>
),
});
}
// Add Artifacts option
items.push({
if (artifactsEnabled) {
dropdownItems.push({
hideOnClick: false,
render: (props) => (
<ArtifactsSubMenu
@ -283,57 +295,24 @@ const ToolsDropdown = ({ disabled }: ToolsDropdownProps) => {
/>
),
});
}
if (mcpServerNames && mcpServerNames.length > 0) {
items.push({
hideOnClick: false,
render: (props) => (
<MCPSubMenu
{...props}
mcpValues={mcpValues}
isMCPPinned={isMCPPinned}
placeholder={mcpPlaceholder}
mcpServerNames={mcpServerNames}
setIsMCPPinned={setIsMCPPinned}
handleMCPToggle={handleMCPToggle}
/>
),
});
}
return items;
}, [
localize,
mcpValues,
canRunCode,
isMCPPinned,
isCodePinned,
mcpPlaceholder,
mcpServerNames,
isSearchPinned,
setIsMCPPinned,
canUseWebSearch,
setIsCodePinned,
handleMCPToggle,
showCodeSettings,
setIsSearchPinned,
handleShadcnToggle,
handleCustomToggle,
isFileSearchPinned,
isArtifactsPinned,
codeMenuTriggerRef,
setIsCodeDialogOpen,
searchMenuTriggerRef,
showWebSearchSettings,
setIsFileSearchPinned,
artifacts.toggleState,
setIsArtifactsPinned,
handleWebSearchToggle,
setIsSearchDialogOpen,
handleFileSearchToggle,
handleArtifactsToggle,
handleCodeInterpreterToggle,
]);
if (mcpServerNames && mcpServerNames.length > 0) {
dropdownItems.push({
hideOnClick: false,
render: (props) => (
<MCPSubMenu
{...props}
mcpValues={mcpValues}
isMCPPinned={isMCPPinned}
placeholder={mcpPlaceholder}
mcpServerNames={mcpServerNames}
setIsMCPPinned={setIsMCPPinned}
handleMCPToggle={handleMCPToggle}
/>
),
});
}
const menuTrigger = (
<TooltipAnchor