From 48ca1bfd8818e6ae895b474d0d65926a2fbd4539 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Thu, 18 Sep 2025 14:44:55 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=A9=20refactor:=20File=20Upload=20Opti?= =?UTF-8?q?ons=20based=20on=20Ephemeral=20Agent=20(#9693)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: agent tool permissions to support ephemeral agent settings * ci: rename render tests and correct typing for `useAgentToolPermissions` hook * refactor: implement `DragDropContext` to minimize effect of `useChatContext` in `DragDropModal` --- client/src/Providers/DragDropContext.tsx | 32 ++++ client/src/Providers/index.ts | 1 + .../Chat/Input/Files/AttachFileMenu.tsx | 11 +- .../Chat/Input/Files/DragDropModal.tsx | 12 +- .../Chat/Input/Files/DragDropWrapper.tsx | 15 +- ...=> useAgentToolPermissions.render.test.ts} | 175 +++++++++++++++--- .../__tests__/useAgentToolPermissions.test.ts | 90 ++++++++- .../hooks/Agents/useAgentToolPermissions.ts | 23 ++- 8 files changed, 300 insertions(+), 59 deletions(-) create mode 100644 client/src/Providers/DragDropContext.tsx rename client/src/hooks/Agents/__tests__/{useAgentToolPermissions.test.tsx => useAgentToolPermissions.render.test.ts} (71%) diff --git a/client/src/Providers/DragDropContext.tsx b/client/src/Providers/DragDropContext.tsx new file mode 100644 index 000000000..a86af6510 --- /dev/null +++ b/client/src/Providers/DragDropContext.tsx @@ -0,0 +1,32 @@ +import React, { createContext, useContext, useMemo } from 'react'; +import { useChatContext } from './ChatContext'; + +interface DragDropContextValue { + conversationId: string | null | undefined; + agentId: string | null | undefined; +} + +const DragDropContext = createContext(undefined); + +export function DragDropProvider({ children }: { children: React.ReactNode }) { + const { conversation } = useChatContext(); + + /** Context value only created when conversation fields change */ + const contextValue = useMemo( + () => ({ + conversationId: conversation?.conversationId, + agentId: conversation?.agent_id, + }), + [conversation?.conversationId, conversation?.agent_id], + ); + + return {children}; +} + +export function useDragDropContext() { + const context = useContext(DragDropContext); + if (!context) { + throw new Error('useDragDropContext must be used within DragDropProvider'); + } + return context; +} diff --git a/client/src/Providers/index.ts b/client/src/Providers/index.ts index c12a4e184..d3607d291 100644 --- a/client/src/Providers/index.ts +++ b/client/src/Providers/index.ts @@ -23,6 +23,7 @@ export * from './SetConvoContext'; export * from './SearchContext'; export * from './BadgeRowContext'; export * from './SidePanelContext'; +export * from './DragDropContext'; export * from './MCPPanelContext'; export * from './ArtifactsContext'; export * from './PromptGroupsContext'; diff --git a/client/src/components/Chat/Input/Files/AttachFileMenu.tsx b/client/src/components/Chat/Input/Files/AttachFileMenu.tsx index 75f38b267..2ed5a0a3e 100644 --- a/client/src/components/Chat/Input/Files/AttachFileMenu.tsx +++ b/client/src/components/Chat/Input/Files/AttachFileMenu.tsx @@ -1,6 +1,6 @@ import React, { useRef, useState, useMemo } from 'react'; import * as Ariakit from '@ariakit/react'; -import { useSetRecoilState } from 'recoil'; +import { useRecoilState } from 'recoil'; import { FileSearch, ImageUpIcon, TerminalSquareIcon, FileType2Icon } from 'lucide-react'; import { EToolResources, EModelEndpoint, defaultAgentCapabilities } from 'librechat-data-provider'; import { @@ -42,7 +42,9 @@ const AttachFileMenu = ({ const isUploadDisabled = disabled ?? false; const inputRef = useRef(null); const [isPopoverActive, setIsPopoverActive] = useState(false); - const setEphemeralAgent = useSetRecoilState(ephemeralAgentByConvoId(conversationId)); + const [ephemeralAgent, setEphemeralAgent] = useRecoilState( + ephemeralAgentByConvoId(conversationId), + ); const [toolResource, setToolResource] = useState(); const { handleFileChange } = useFileHandling({ overrideEndpoint: EModelEndpoint.agents, @@ -64,7 +66,10 @@ const AttachFileMenu = ({ * */ const capabilities = useAgentCapabilities(agentsConfig?.capabilities ?? defaultAgentCapabilities); - const { fileSearchAllowedByAgent, codeAllowedByAgent } = useAgentToolPermissions(agentId); + const { fileSearchAllowedByAgent, codeAllowedByAgent } = useAgentToolPermissions( + agentId, + ephemeralAgent, + ); const handleUploadClick = (isImage?: boolean) => { if (!inputRef.current) { diff --git a/client/src/components/Chat/Input/Files/DragDropModal.tsx b/client/src/components/Chat/Input/Files/DragDropModal.tsx index 3263b05f1..6f506c65f 100644 --- a/client/src/components/Chat/Input/Files/DragDropModal.tsx +++ b/client/src/components/Chat/Input/Files/DragDropModal.tsx @@ -1,14 +1,16 @@ import React, { useMemo } from 'react'; +import { useRecoilValue } from 'recoil'; import { OGDialog, OGDialogTemplate } from '@librechat/client'; -import { ImageUpIcon, FileSearch, TerminalSquareIcon, FileType2Icon } from 'lucide-react'; import { EToolResources, defaultAgentCapabilities } from 'librechat-data-provider'; +import { ImageUpIcon, FileSearch, TerminalSquareIcon, FileType2Icon } from 'lucide-react'; import { useAgentToolPermissions, useAgentCapabilities, useGetAgentsConfig, useLocalize, } from '~/hooks'; -import { useChatContext } from '~/Providers'; +import { ephemeralAgentByConvoId } from '~/store'; +import { useDragDropContext } from '~/Providers'; interface DragDropModalProps { onOptionSelect: (option: EToolResources | undefined) => void; @@ -32,9 +34,11 @@ const DragDropModal = ({ onOptionSelect, setShowModal, files, isVisible }: DragD * Use definition for agents endpoint for ephemeral agents * */ const capabilities = useAgentCapabilities(agentsConfig?.capabilities ?? defaultAgentCapabilities); - const { conversation } = useChatContext(); + const { conversationId, agentId } = useDragDropContext(); + const ephemeralAgent = useRecoilValue(ephemeralAgentByConvoId(conversationId ?? '')); const { fileSearchAllowedByAgent, codeAllowedByAgent } = useAgentToolPermissions( - conversation?.agent_id, + agentId, + ephemeralAgent, ); const options = useMemo(() => { diff --git a/client/src/components/Chat/Input/Files/DragDropWrapper.tsx b/client/src/components/Chat/Input/Files/DragDropWrapper.tsx index 1a3698e09..4a01d8a2a 100644 --- a/client/src/components/Chat/Input/Files/DragDropWrapper.tsx +++ b/client/src/components/Chat/Input/Files/DragDropWrapper.tsx @@ -1,6 +1,7 @@ import { useDragHelpers } from '~/hooks'; import DragDropOverlay from '~/components/Chat/Input/Files/DragDropOverlay'; import DragDropModal from '~/components/Chat/Input/Files/DragDropModal'; +import { DragDropProvider } from '~/Providers'; import { cn } from '~/utils'; interface DragDropWrapperProps { @@ -19,12 +20,14 @@ export default function DragDropWrapper({ children, className }: DragDropWrapper {children} {/** Always render overlay to avoid mount/unmount overhead */} - + + + ); } diff --git a/client/src/hooks/Agents/__tests__/useAgentToolPermissions.test.tsx b/client/src/hooks/Agents/__tests__/useAgentToolPermissions.render.test.ts similarity index 71% rename from client/src/hooks/Agents/__tests__/useAgentToolPermissions.test.tsx rename to client/src/hooks/Agents/__tests__/useAgentToolPermissions.render.test.ts index 604ffef8d..8f582abe5 100644 --- a/client/src/hooks/Agents/__tests__/useAgentToolPermissions.test.tsx +++ b/client/src/hooks/Agents/__tests__/useAgentToolPermissions.render.test.ts @@ -1,5 +1,6 @@ import { renderHook } from '@testing-library/react'; -import { Tools, Constants } from 'librechat-data-provider'; +import { Tools, Constants, EToolResources } from 'librechat-data-provider'; +import type { TEphemeralAgent } from 'librechat-data-provider'; import useAgentToolPermissions from '../useAgentToolPermissions'; // Mock dependencies @@ -15,57 +16,166 @@ jest.mock('~/Providers', () => ({ import { useGetAgentByIdQuery } from '~/data-provider'; import { useAgentsMapContext } from '~/Providers'; +type HookProps = { + agentId?: string | null; + ephemeralAgent?: TEphemeralAgent | null; +}; + describe('useAgentToolPermissions', () => { beforeEach(() => { jest.clearAllMocks(); }); - describe('Ephemeral Agent Scenarios', () => { - it('should return true for all tools when agentId is null', () => { + describe('Ephemeral Agent Scenarios (without ephemeralAgent parameter)', () => { + it('should return false for all tools when agentId is null and no ephemeralAgent provided', () => { (useAgentsMapContext as jest.Mock).mockReturnValue({}); (useGetAgentByIdQuery as jest.Mock).mockReturnValue({ data: undefined }); const { result } = renderHook(() => useAgentToolPermissions(null)); - expect(result.current.fileSearchAllowedByAgent).toBe(true); - expect(result.current.codeAllowedByAgent).toBe(true); + expect(result.current.fileSearchAllowedByAgent).toBe(false); + expect(result.current.codeAllowedByAgent).toBe(false); expect(result.current.tools).toBeUndefined(); }); - it('should return true for all tools when agentId is undefined', () => { + it('should return false for all tools when agentId is undefined and no ephemeralAgent provided', () => { (useAgentsMapContext as jest.Mock).mockReturnValue({}); (useGetAgentByIdQuery as jest.Mock).mockReturnValue({ data: undefined }); const { result } = renderHook(() => useAgentToolPermissions(undefined)); - expect(result.current.fileSearchAllowedByAgent).toBe(true); - expect(result.current.codeAllowedByAgent).toBe(true); + expect(result.current.fileSearchAllowedByAgent).toBe(false); + expect(result.current.codeAllowedByAgent).toBe(false); expect(result.current.tools).toBeUndefined(); }); - it('should return true for all tools when agentId is empty string', () => { + it('should return false for all tools when agentId is empty string and no ephemeralAgent provided', () => { (useAgentsMapContext as jest.Mock).mockReturnValue({}); (useGetAgentByIdQuery as jest.Mock).mockReturnValue({ data: undefined }); const { result } = renderHook(() => useAgentToolPermissions('')); + expect(result.current.fileSearchAllowedByAgent).toBe(false); + expect(result.current.codeAllowedByAgent).toBe(false); + expect(result.current.tools).toBeUndefined(); + }); + + it('should return false for all tools when agentId is EPHEMERAL_AGENT_ID and no ephemeralAgent provided', () => { + (useAgentsMapContext as jest.Mock).mockReturnValue({}); + (useGetAgentByIdQuery as jest.Mock).mockReturnValue({ data: undefined }); + + const { result } = renderHook(() => useAgentToolPermissions(Constants.EPHEMERAL_AGENT_ID)); + + expect(result.current.fileSearchAllowedByAgent).toBe(false); + expect(result.current.codeAllowedByAgent).toBe(false); + expect(result.current.tools).toBeUndefined(); + }); + }); + + describe('Ephemeral Agent with Tool Settings', () => { + it('should return true for file_search when ephemeralAgent has file_search enabled', () => { + (useAgentsMapContext as jest.Mock).mockReturnValue({}); + (useGetAgentByIdQuery as jest.Mock).mockReturnValue({ data: undefined }); + + const ephemeralAgent = { + [EToolResources.file_search]: true, + }; + + const { result } = renderHook(() => useAgentToolPermissions(null, ephemeralAgent)); + + expect(result.current.fileSearchAllowedByAgent).toBe(true); + expect(result.current.codeAllowedByAgent).toBe(false); + expect(result.current.tools).toBeUndefined(); + }); + + it('should return true for execute_code when ephemeralAgent has execute_code enabled', () => { + (useAgentsMapContext as jest.Mock).mockReturnValue({}); + (useGetAgentByIdQuery as jest.Mock).mockReturnValue({ data: undefined }); + + const ephemeralAgent = { + [EToolResources.execute_code]: true, + }; + + const { result } = renderHook(() => useAgentToolPermissions(undefined, ephemeralAgent)); + + expect(result.current.fileSearchAllowedByAgent).toBe(false); + expect(result.current.codeAllowedByAgent).toBe(true); + expect(result.current.tools).toBeUndefined(); + }); + + it('should return true for both tools when ephemeralAgent has both enabled', () => { + (useAgentsMapContext as jest.Mock).mockReturnValue({}); + (useGetAgentByIdQuery as jest.Mock).mockReturnValue({ data: undefined }); + + const ephemeralAgent = { + [EToolResources.file_search]: true, + [EToolResources.execute_code]: true, + }; + + const { result } = renderHook(() => useAgentToolPermissions('', ephemeralAgent)); + expect(result.current.fileSearchAllowedByAgent).toBe(true); expect(result.current.codeAllowedByAgent).toBe(true); expect(result.current.tools).toBeUndefined(); }); - it('should return true for all tools when agentId is EPHEMERAL_AGENT_ID', () => { + it('should return false for tools when ephemeralAgent has them explicitly disabled', () => { (useAgentsMapContext as jest.Mock).mockReturnValue({}); (useGetAgentByIdQuery as jest.Mock).mockReturnValue({ data: undefined }); - const { result } = renderHook(() => - useAgentToolPermissions(Constants.EPHEMERAL_AGENT_ID) + const ephemeralAgent = { + [EToolResources.file_search]: false, + [EToolResources.execute_code]: false, + }; + + const { result } = renderHook(() => + useAgentToolPermissions(Constants.EPHEMERAL_AGENT_ID, ephemeralAgent), ); - expect(result.current.fileSearchAllowedByAgent).toBe(true); - expect(result.current.codeAllowedByAgent).toBe(true); + expect(result.current.fileSearchAllowedByAgent).toBe(false); + expect(result.current.codeAllowedByAgent).toBe(false); expect(result.current.tools).toBeUndefined(); }); + + it('should handle ephemeralAgent with ocr property without affecting other tools', () => { + (useAgentsMapContext as jest.Mock).mockReturnValue({}); + (useGetAgentByIdQuery as jest.Mock).mockReturnValue({ data: undefined }); + + const ephemeralAgent = { + [EToolResources.ocr]: true, + [EToolResources.file_search]: true, + }; + + const { result } = renderHook(() => useAgentToolPermissions(null, ephemeralAgent)); + + expect(result.current.fileSearchAllowedByAgent).toBe(true); + expect(result.current.codeAllowedByAgent).toBe(false); + expect(result.current.tools).toBeUndefined(); + }); + + it('should not affect regular agents when ephemeralAgent is provided', () => { + const agentId = 'regular-agent'; + const mockAgent = { + id: agentId, + tools: [Tools.file_search], + }; + + (useAgentsMapContext as jest.Mock).mockReturnValue({ + [agentId]: mockAgent, + }); + (useGetAgentByIdQuery as jest.Mock).mockReturnValue({ data: undefined }); + + const ephemeralAgent = { + [EToolResources.execute_code]: true, + }; + + const { result } = renderHook(() => useAgentToolPermissions(agentId, ephemeralAgent)); + + // Should use regular agent's tools, not ephemeralAgent + expect(result.current.fileSearchAllowedByAgent).toBe(true); + expect(result.current.codeAllowedByAgent).toBe(false); + expect(result.current.tools).toEqual([Tools.file_search]); + }); }); describe('Regular Agent with Tools', () => { @@ -300,7 +410,7 @@ describe('useAgentToolPermissions', () => { expect(firstResult.codeAllowedByAgent).toBe(secondResult.codeAllowedByAgent); // Tools array reference should be the same since it comes from useMemo expect(firstResult.tools).toBe(secondResult.tools); - + // Verify the actual values are correct expect(secondResult.fileSearchAllowedByAgent).toBe(true); expect(secondResult.codeAllowedByAgent).toBe(false); @@ -318,10 +428,9 @@ describe('useAgentToolPermissions', () => { (useAgentsMapContext as jest.Mock).mockReturnValue(mockAgents); (useGetAgentByIdQuery as jest.Mock).mockReturnValue({ data: undefined }); - const { result, rerender } = renderHook( - ({ agentId }) => useAgentToolPermissions(agentId), - { initialProps: { agentId: agentId1 } } - ); + const { result, rerender } = renderHook(({ agentId }) => useAgentToolPermissions(agentId), { + initialProps: { agentId: agentId1 }, + }); expect(result.current.fileSearchAllowedByAgent).toBe(true); expect(result.current.codeAllowedByAgent).toBe(false); @@ -345,24 +454,34 @@ describe('useAgentToolPermissions', () => { }); (useGetAgentByIdQuery as jest.Mock).mockReturnValue({ data: undefined }); + const ephemeralAgent = { + [EToolResources.file_search]: true, + [EToolResources.execute_code]: true, + }; + const { result, rerender } = renderHook( - ({ agentId }) => useAgentToolPermissions(agentId), - { initialProps: { agentId: null } } + ({ agentId, ephemeralAgent }) => useAgentToolPermissions(agentId, ephemeralAgent), + { initialProps: { agentId: null, ephemeralAgent } as HookProps }, ); - // Start with ephemeral agent (null) + // Start with ephemeral agent (null) with tools enabled expect(result.current.fileSearchAllowedByAgent).toBe(true); expect(result.current.codeAllowedByAgent).toBe(true); // Switch to regular agent - rerender({ agentId: regularAgentId }); + rerender({ agentId: regularAgentId, ephemeralAgent }); expect(result.current.fileSearchAllowedByAgent).toBe(false); expect(result.current.codeAllowedByAgent).toBe(false); // Switch back to ephemeral - rerender({ agentId: '' }); + rerender({ agentId: '', ephemeralAgent }); expect(result.current.fileSearchAllowedByAgent).toBe(true); expect(result.current.codeAllowedByAgent).toBe(true); + + // Switch to ephemeral without tools + rerender({ agentId: null, ephemeralAgent: undefined }); + expect(result.current.fileSearchAllowedByAgent).toBe(false); + expect(result.current.codeAllowedByAgent).toBe(false); }); }); @@ -403,9 +522,9 @@ describe('useAgentToolPermissions', () => { it('should handle query loading state', () => { const agentId = 'loading-agent'; - + (useAgentsMapContext as jest.Mock).mockReturnValue({}); - (useGetAgentByIdQuery as jest.Mock).mockReturnValue({ + (useGetAgentByIdQuery as jest.Mock).mockReturnValue({ data: undefined, isLoading: true, error: null, @@ -421,9 +540,9 @@ describe('useAgentToolPermissions', () => { it('should handle query error state', () => { const agentId = 'error-agent'; - + (useAgentsMapContext as jest.Mock).mockReturnValue({}); - (useGetAgentByIdQuery as jest.Mock).mockReturnValue({ + (useGetAgentByIdQuery as jest.Mock).mockReturnValue({ data: undefined, isLoading: false, error: new Error('Failed to fetch agent'), diff --git a/client/src/hooks/Agents/__tests__/useAgentToolPermissions.test.ts b/client/src/hooks/Agents/__tests__/useAgentToolPermissions.test.ts index 1d6da4ddb..ba6ee52ae 100644 --- a/client/src/hooks/Agents/__tests__/useAgentToolPermissions.test.ts +++ b/client/src/hooks/Agents/__tests__/useAgentToolPermissions.test.ts @@ -1,5 +1,5 @@ import { renderHook } from '@testing-library/react'; -import { Tools } from 'librechat-data-provider'; +import { Tools, EToolResources } from 'librechat-data-provider'; import useAgentToolPermissions from '../useAgentToolPermissions'; // Mock the dependencies @@ -20,36 +20,36 @@ describe('useAgentToolPermissions', () => { }); describe('when no agentId is provided', () => { - it('should allow all tools for ephemeral agents', () => { + it('should disallow all tools for ephemeral agents when no ephemeralAgent settings provided', () => { mockUseAgentsMapContext.mockReturnValue({}); mockUseGetAgentByIdQuery.mockReturnValue({ data: undefined }); const { result } = renderHook(() => useAgentToolPermissions(null)); - expect(result.current.fileSearchAllowedByAgent).toBe(true); - expect(result.current.codeAllowedByAgent).toBe(true); + expect(result.current.fileSearchAllowedByAgent).toBe(false); + expect(result.current.codeAllowedByAgent).toBe(false); expect(result.current.tools).toBeUndefined(); }); - it('should allow all tools when agentId is undefined', () => { + it('should disallow all tools when agentId is undefined and no ephemeralAgent settings', () => { mockUseAgentsMapContext.mockReturnValue({}); mockUseGetAgentByIdQuery.mockReturnValue({ data: undefined }); const { result } = renderHook(() => useAgentToolPermissions(undefined)); - expect(result.current.fileSearchAllowedByAgent).toBe(true); - expect(result.current.codeAllowedByAgent).toBe(true); + expect(result.current.fileSearchAllowedByAgent).toBe(false); + expect(result.current.codeAllowedByAgent).toBe(false); expect(result.current.tools).toBeUndefined(); }); - it('should allow all tools when agentId is empty string', () => { + it('should disallow all tools when agentId is empty string and no ephemeralAgent settings', () => { mockUseAgentsMapContext.mockReturnValue({}); mockUseGetAgentByIdQuery.mockReturnValue({ data: undefined }); const { result } = renderHook(() => useAgentToolPermissions('')); - expect(result.current.fileSearchAllowedByAgent).toBe(true); - expect(result.current.codeAllowedByAgent).toBe(true); + expect(result.current.fileSearchAllowedByAgent).toBe(false); + expect(result.current.codeAllowedByAgent).toBe(false); expect(result.current.tools).toBeUndefined(); }); }); @@ -177,4 +177,74 @@ describe('useAgentToolPermissions', () => { expect(result.current.tools).toBeUndefined(); }); }); + + describe('when ephemeralAgent settings are provided', () => { + it('should allow file_search when ephemeralAgent has file_search enabled', () => { + mockUseAgentsMapContext.mockReturnValue({}); + mockUseGetAgentByIdQuery.mockReturnValue({ data: undefined }); + + const ephemeralAgent = { + [EToolResources.file_search]: true, + }; + + const { result } = renderHook(() => useAgentToolPermissions(null, ephemeralAgent)); + + expect(result.current.fileSearchAllowedByAgent).toBe(true); + expect(result.current.codeAllowedByAgent).toBe(false); + expect(result.current.tools).toBeUndefined(); + }); + + it('should allow execute_code when ephemeralAgent has execute_code enabled', () => { + mockUseAgentsMapContext.mockReturnValue({}); + mockUseGetAgentByIdQuery.mockReturnValue({ data: undefined }); + + const ephemeralAgent = { + [EToolResources.execute_code]: true, + }; + + const { result } = renderHook(() => useAgentToolPermissions(undefined, ephemeralAgent)); + + expect(result.current.fileSearchAllowedByAgent).toBe(false); + expect(result.current.codeAllowedByAgent).toBe(true); + expect(result.current.tools).toBeUndefined(); + }); + + it('should allow both tools when ephemeralAgent has both enabled', () => { + mockUseAgentsMapContext.mockReturnValue({}); + mockUseGetAgentByIdQuery.mockReturnValue({ data: undefined }); + + const ephemeralAgent = { + [EToolResources.file_search]: true, + [EToolResources.execute_code]: true, + }; + + const { result } = renderHook(() => useAgentToolPermissions('', ephemeralAgent)); + + expect(result.current.fileSearchAllowedByAgent).toBe(true); + expect(result.current.codeAllowedByAgent).toBe(true); + expect(result.current.tools).toBeUndefined(); + }); + + it('should not affect regular agents when ephemeralAgent is provided', () => { + const agentId = 'regular-agent'; + const agent = { + id: agentId, + tools: [Tools.file_search], + }; + + mockUseAgentsMapContext.mockReturnValue({ [agentId]: agent }); + mockUseGetAgentByIdQuery.mockReturnValue({ data: undefined }); + + const ephemeralAgent = { + [EToolResources.execute_code]: true, + }; + + const { result } = renderHook(() => useAgentToolPermissions(agentId, ephemeralAgent)); + + // Should use regular agent's tools, not ephemeralAgent + expect(result.current.fileSearchAllowedByAgent).toBe(true); + expect(result.current.codeAllowedByAgent).toBe(false); + expect(result.current.tools).toEqual([Tools.file_search]); + }); + }); }); diff --git a/client/src/hooks/Agents/useAgentToolPermissions.ts b/client/src/hooks/Agents/useAgentToolPermissions.ts index c0b0699f9..c04866053 100644 --- a/client/src/hooks/Agents/useAgentToolPermissions.ts +++ b/client/src/hooks/Agents/useAgentToolPermissions.ts @@ -1,5 +1,6 @@ import { useMemo } from 'react'; -import { Tools, Constants } from 'librechat-data-provider'; +import { Tools, Constants, EToolResources } from 'librechat-data-provider'; +import type { TEphemeralAgent } from 'librechat-data-provider'; import { useGetAgentByIdQuery } from '~/data-provider'; import { useAgentsMapContext } from '~/Providers'; @@ -16,11 +17,13 @@ function isEphemeralAgent(agentId: string | null | undefined): boolean { /** * Hook to determine whether specific tools are allowed for a given agent. * - * @param agentId - The ID of the agent. If null/undefined/empty, returns true for all tools (ephemeral agent behavior) + * @param agentId - The ID of the agent. If null/undefined/empty, checks ephemeralAgent settings + * @param ephemeralAgent - Optional ephemeral agent settings for tool permissions * @returns Object with boolean flags for file_search and execute_code permissions, plus the tools array */ export default function useAgentToolPermissions( agentId: string | null | undefined, + ephemeralAgent?: TEphemeralAgent | null, ): AgentToolPermissionsResult { const agentsMap = useAgentsMapContext(); @@ -37,22 +40,26 @@ export default function useAgentToolPermissions( ); const fileSearchAllowedByAgent = useMemo(() => { - // Allow for ephemeral agents - if (isEphemeralAgent(agentId)) return true; + // Check ephemeral agent settings + if (isEphemeralAgent(agentId)) { + return ephemeralAgent?.[EToolResources.file_search] ?? false; + } // If agentId exists but agent not found, disallow if (!selectedAgent) return false; // Check if the agent has the file_search tool return tools?.includes(Tools.file_search) ?? false; - }, [agentId, selectedAgent, tools]); + }, [agentId, selectedAgent, tools, ephemeralAgent]); const codeAllowedByAgent = useMemo(() => { - // Allow for ephemeral agents - if (isEphemeralAgent(agentId)) return true; + // Check ephemeral agent settings + if (isEphemeralAgent(agentId)) { + return ephemeralAgent?.[EToolResources.execute_code] ?? false; + } // If agentId exists but agent not found, disallow if (!selectedAgent) return false; // Check if the agent has the execute_code tool return tools?.includes(Tools.execute_code) ?? false; - }, [agentId, selectedAgent, tools]); + }, [agentId, selectedAgent, tools, ephemeralAgent]); return { fileSearchAllowedByAgent,