mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-18 01:10:14 +01:00
🧩 refactor: File Upload Options based on Ephemeral Agent (#9693)
* 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`
This commit is contained in:
parent
208be7c06c
commit
48ca1bfd88
8 changed files with 300 additions and 59 deletions
32
client/src/Providers/DragDropContext.tsx
Normal file
32
client/src/Providers/DragDropContext.tsx
Normal file
|
|
@ -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<DragDropContextValue | undefined>(undefined);
|
||||||
|
|
||||||
|
export function DragDropProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
const { conversation } = useChatContext();
|
||||||
|
|
||||||
|
/** Context value only created when conversation fields change */
|
||||||
|
const contextValue = useMemo<DragDropContextValue>(
|
||||||
|
() => ({
|
||||||
|
conversationId: conversation?.conversationId,
|
||||||
|
agentId: conversation?.agent_id,
|
||||||
|
}),
|
||||||
|
[conversation?.conversationId, conversation?.agent_id],
|
||||||
|
);
|
||||||
|
|
||||||
|
return <DragDropContext.Provider value={contextValue}>{children}</DragDropContext.Provider>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDragDropContext() {
|
||||||
|
const context = useContext(DragDropContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useDragDropContext must be used within DragDropProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
@ -23,6 +23,7 @@ export * from './SetConvoContext';
|
||||||
export * from './SearchContext';
|
export * from './SearchContext';
|
||||||
export * from './BadgeRowContext';
|
export * from './BadgeRowContext';
|
||||||
export * from './SidePanelContext';
|
export * from './SidePanelContext';
|
||||||
|
export * from './DragDropContext';
|
||||||
export * from './MCPPanelContext';
|
export * from './MCPPanelContext';
|
||||||
export * from './ArtifactsContext';
|
export * from './ArtifactsContext';
|
||||||
export * from './PromptGroupsContext';
|
export * from './PromptGroupsContext';
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useRef, useState, useMemo } from 'react';
|
import React, { useRef, useState, useMemo } from 'react';
|
||||||
import * as Ariakit from '@ariakit/react';
|
import * as Ariakit from '@ariakit/react';
|
||||||
import { useSetRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
import { FileSearch, ImageUpIcon, TerminalSquareIcon, FileType2Icon } from 'lucide-react';
|
import { FileSearch, ImageUpIcon, TerminalSquareIcon, FileType2Icon } from 'lucide-react';
|
||||||
import { EToolResources, EModelEndpoint, defaultAgentCapabilities } from 'librechat-data-provider';
|
import { EToolResources, EModelEndpoint, defaultAgentCapabilities } from 'librechat-data-provider';
|
||||||
import {
|
import {
|
||||||
|
|
@ -42,7 +42,9 @@ const AttachFileMenu = ({
|
||||||
const isUploadDisabled = disabled ?? false;
|
const isUploadDisabled = disabled ?? false;
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const [isPopoverActive, setIsPopoverActive] = useState(false);
|
const [isPopoverActive, setIsPopoverActive] = useState(false);
|
||||||
const setEphemeralAgent = useSetRecoilState(ephemeralAgentByConvoId(conversationId));
|
const [ephemeralAgent, setEphemeralAgent] = useRecoilState(
|
||||||
|
ephemeralAgentByConvoId(conversationId),
|
||||||
|
);
|
||||||
const [toolResource, setToolResource] = useState<EToolResources | undefined>();
|
const [toolResource, setToolResource] = useState<EToolResources | undefined>();
|
||||||
const { handleFileChange } = useFileHandling({
|
const { handleFileChange } = useFileHandling({
|
||||||
overrideEndpoint: EModelEndpoint.agents,
|
overrideEndpoint: EModelEndpoint.agents,
|
||||||
|
|
@ -64,7 +66,10 @@ const AttachFileMenu = ({
|
||||||
* */
|
* */
|
||||||
const capabilities = useAgentCapabilities(agentsConfig?.capabilities ?? defaultAgentCapabilities);
|
const capabilities = useAgentCapabilities(agentsConfig?.capabilities ?? defaultAgentCapabilities);
|
||||||
|
|
||||||
const { fileSearchAllowedByAgent, codeAllowedByAgent } = useAgentToolPermissions(agentId);
|
const { fileSearchAllowedByAgent, codeAllowedByAgent } = useAgentToolPermissions(
|
||||||
|
agentId,
|
||||||
|
ephemeralAgent,
|
||||||
|
);
|
||||||
|
|
||||||
const handleUploadClick = (isImage?: boolean) => {
|
const handleUploadClick = (isImage?: boolean) => {
|
||||||
if (!inputRef.current) {
|
if (!inputRef.current) {
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,16 @@
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
import { OGDialog, OGDialogTemplate } from '@librechat/client';
|
import { OGDialog, OGDialogTemplate } from '@librechat/client';
|
||||||
import { ImageUpIcon, FileSearch, TerminalSquareIcon, FileType2Icon } from 'lucide-react';
|
|
||||||
import { EToolResources, defaultAgentCapabilities } from 'librechat-data-provider';
|
import { EToolResources, defaultAgentCapabilities } from 'librechat-data-provider';
|
||||||
|
import { ImageUpIcon, FileSearch, TerminalSquareIcon, FileType2Icon } from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
useAgentToolPermissions,
|
useAgentToolPermissions,
|
||||||
useAgentCapabilities,
|
useAgentCapabilities,
|
||||||
useGetAgentsConfig,
|
useGetAgentsConfig,
|
||||||
useLocalize,
|
useLocalize,
|
||||||
} from '~/hooks';
|
} from '~/hooks';
|
||||||
import { useChatContext } from '~/Providers';
|
import { ephemeralAgentByConvoId } from '~/store';
|
||||||
|
import { useDragDropContext } from '~/Providers';
|
||||||
|
|
||||||
interface DragDropModalProps {
|
interface DragDropModalProps {
|
||||||
onOptionSelect: (option: EToolResources | undefined) => void;
|
onOptionSelect: (option: EToolResources | undefined) => void;
|
||||||
|
|
@ -32,9 +34,11 @@ const DragDropModal = ({ onOptionSelect, setShowModal, files, isVisible }: DragD
|
||||||
* Use definition for agents endpoint for ephemeral agents
|
* Use definition for agents endpoint for ephemeral agents
|
||||||
* */
|
* */
|
||||||
const capabilities = useAgentCapabilities(agentsConfig?.capabilities ?? defaultAgentCapabilities);
|
const capabilities = useAgentCapabilities(agentsConfig?.capabilities ?? defaultAgentCapabilities);
|
||||||
const { conversation } = useChatContext();
|
const { conversationId, agentId } = useDragDropContext();
|
||||||
|
const ephemeralAgent = useRecoilValue(ephemeralAgentByConvoId(conversationId ?? ''));
|
||||||
const { fileSearchAllowedByAgent, codeAllowedByAgent } = useAgentToolPermissions(
|
const { fileSearchAllowedByAgent, codeAllowedByAgent } = useAgentToolPermissions(
|
||||||
conversation?.agent_id,
|
agentId,
|
||||||
|
ephemeralAgent,
|
||||||
);
|
);
|
||||||
|
|
||||||
const options = useMemo(() => {
|
const options = useMemo(() => {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { useDragHelpers } from '~/hooks';
|
import { useDragHelpers } from '~/hooks';
|
||||||
import DragDropOverlay from '~/components/Chat/Input/Files/DragDropOverlay';
|
import DragDropOverlay from '~/components/Chat/Input/Files/DragDropOverlay';
|
||||||
import DragDropModal from '~/components/Chat/Input/Files/DragDropModal';
|
import DragDropModal from '~/components/Chat/Input/Files/DragDropModal';
|
||||||
|
import { DragDropProvider } from '~/Providers';
|
||||||
import { cn } from '~/utils';
|
import { cn } from '~/utils';
|
||||||
|
|
||||||
interface DragDropWrapperProps {
|
interface DragDropWrapperProps {
|
||||||
|
|
@ -19,12 +20,14 @@ export default function DragDropWrapper({ children, className }: DragDropWrapper
|
||||||
{children}
|
{children}
|
||||||
{/** Always render overlay to avoid mount/unmount overhead */}
|
{/** Always render overlay to avoid mount/unmount overhead */}
|
||||||
<DragDropOverlay isActive={isActive} />
|
<DragDropOverlay isActive={isActive} />
|
||||||
<DragDropModal
|
<DragDropProvider>
|
||||||
files={draggedFiles}
|
<DragDropModal
|
||||||
isVisible={showModal}
|
files={draggedFiles}
|
||||||
setShowModal={setShowModal}
|
isVisible={showModal}
|
||||||
onOptionSelect={handleOptionSelect}
|
setShowModal={setShowModal}
|
||||||
/>
|
onOptionSelect={handleOptionSelect}
|
||||||
|
/>
|
||||||
|
</DragDropProvider>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { renderHook } from '@testing-library/react';
|
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';
|
import useAgentToolPermissions from '../useAgentToolPermissions';
|
||||||
|
|
||||||
// Mock dependencies
|
// Mock dependencies
|
||||||
|
|
@ -15,57 +16,166 @@ jest.mock('~/Providers', () => ({
|
||||||
import { useGetAgentByIdQuery } from '~/data-provider';
|
import { useGetAgentByIdQuery } from '~/data-provider';
|
||||||
import { useAgentsMapContext } from '~/Providers';
|
import { useAgentsMapContext } from '~/Providers';
|
||||||
|
|
||||||
|
type HookProps = {
|
||||||
|
agentId?: string | null;
|
||||||
|
ephemeralAgent?: TEphemeralAgent | null;
|
||||||
|
};
|
||||||
|
|
||||||
describe('useAgentToolPermissions', () => {
|
describe('useAgentToolPermissions', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Ephemeral Agent Scenarios', () => {
|
describe('Ephemeral Agent Scenarios (without ephemeralAgent parameter)', () => {
|
||||||
it('should return true for all tools when agentId is null', () => {
|
it('should return false for all tools when agentId is null and no ephemeralAgent provided', () => {
|
||||||
(useAgentsMapContext as jest.Mock).mockReturnValue({});
|
(useAgentsMapContext as jest.Mock).mockReturnValue({});
|
||||||
(useGetAgentByIdQuery as jest.Mock).mockReturnValue({ data: undefined });
|
(useGetAgentByIdQuery as jest.Mock).mockReturnValue({ data: undefined });
|
||||||
|
|
||||||
const { result } = renderHook(() => useAgentToolPermissions(null));
|
const { result } = renderHook(() => useAgentToolPermissions(null));
|
||||||
|
|
||||||
expect(result.current.fileSearchAllowedByAgent).toBe(true);
|
expect(result.current.fileSearchAllowedByAgent).toBe(false);
|
||||||
expect(result.current.codeAllowedByAgent).toBe(true);
|
expect(result.current.codeAllowedByAgent).toBe(false);
|
||||||
expect(result.current.tools).toBeUndefined();
|
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({});
|
(useAgentsMapContext as jest.Mock).mockReturnValue({});
|
||||||
(useGetAgentByIdQuery as jest.Mock).mockReturnValue({ data: undefined });
|
(useGetAgentByIdQuery as jest.Mock).mockReturnValue({ data: undefined });
|
||||||
|
|
||||||
const { result } = renderHook(() => useAgentToolPermissions(undefined));
|
const { result } = renderHook(() => useAgentToolPermissions(undefined));
|
||||||
|
|
||||||
expect(result.current.fileSearchAllowedByAgent).toBe(true);
|
expect(result.current.fileSearchAllowedByAgent).toBe(false);
|
||||||
expect(result.current.codeAllowedByAgent).toBe(true);
|
expect(result.current.codeAllowedByAgent).toBe(false);
|
||||||
expect(result.current.tools).toBeUndefined();
|
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({});
|
(useAgentsMapContext as jest.Mock).mockReturnValue({});
|
||||||
(useGetAgentByIdQuery as jest.Mock).mockReturnValue({ data: undefined });
|
(useGetAgentByIdQuery as jest.Mock).mockReturnValue({ data: undefined });
|
||||||
|
|
||||||
const { result } = renderHook(() => useAgentToolPermissions(''));
|
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.fileSearchAllowedByAgent).toBe(true);
|
||||||
expect(result.current.codeAllowedByAgent).toBe(true);
|
expect(result.current.codeAllowedByAgent).toBe(true);
|
||||||
expect(result.current.tools).toBeUndefined();
|
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({});
|
(useAgentsMapContext as jest.Mock).mockReturnValue({});
|
||||||
(useGetAgentByIdQuery as jest.Mock).mockReturnValue({ data: undefined });
|
(useGetAgentByIdQuery as jest.Mock).mockReturnValue({ data: undefined });
|
||||||
|
|
||||||
const { result } = renderHook(() =>
|
const ephemeralAgent = {
|
||||||
useAgentToolPermissions(Constants.EPHEMERAL_AGENT_ID)
|
[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.fileSearchAllowedByAgent).toBe(false);
|
||||||
expect(result.current.codeAllowedByAgent).toBe(true);
|
expect(result.current.codeAllowedByAgent).toBe(false);
|
||||||
expect(result.current.tools).toBeUndefined();
|
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', () => {
|
describe('Regular Agent with Tools', () => {
|
||||||
|
|
@ -300,7 +410,7 @@ describe('useAgentToolPermissions', () => {
|
||||||
expect(firstResult.codeAllowedByAgent).toBe(secondResult.codeAllowedByAgent);
|
expect(firstResult.codeAllowedByAgent).toBe(secondResult.codeAllowedByAgent);
|
||||||
// Tools array reference should be the same since it comes from useMemo
|
// Tools array reference should be the same since it comes from useMemo
|
||||||
expect(firstResult.tools).toBe(secondResult.tools);
|
expect(firstResult.tools).toBe(secondResult.tools);
|
||||||
|
|
||||||
// Verify the actual values are correct
|
// Verify the actual values are correct
|
||||||
expect(secondResult.fileSearchAllowedByAgent).toBe(true);
|
expect(secondResult.fileSearchAllowedByAgent).toBe(true);
|
||||||
expect(secondResult.codeAllowedByAgent).toBe(false);
|
expect(secondResult.codeAllowedByAgent).toBe(false);
|
||||||
|
|
@ -318,10 +428,9 @@ describe('useAgentToolPermissions', () => {
|
||||||
(useAgentsMapContext as jest.Mock).mockReturnValue(mockAgents);
|
(useAgentsMapContext as jest.Mock).mockReturnValue(mockAgents);
|
||||||
(useGetAgentByIdQuery as jest.Mock).mockReturnValue({ data: undefined });
|
(useGetAgentByIdQuery as jest.Mock).mockReturnValue({ data: undefined });
|
||||||
|
|
||||||
const { result, rerender } = renderHook(
|
const { result, rerender } = renderHook(({ agentId }) => useAgentToolPermissions(agentId), {
|
||||||
({ agentId }) => useAgentToolPermissions(agentId),
|
initialProps: { agentId: agentId1 },
|
||||||
{ initialProps: { agentId: agentId1 } }
|
});
|
||||||
);
|
|
||||||
|
|
||||||
expect(result.current.fileSearchAllowedByAgent).toBe(true);
|
expect(result.current.fileSearchAllowedByAgent).toBe(true);
|
||||||
expect(result.current.codeAllowedByAgent).toBe(false);
|
expect(result.current.codeAllowedByAgent).toBe(false);
|
||||||
|
|
@ -345,24 +454,34 @@ describe('useAgentToolPermissions', () => {
|
||||||
});
|
});
|
||||||
(useGetAgentByIdQuery as jest.Mock).mockReturnValue({ data: undefined });
|
(useGetAgentByIdQuery as jest.Mock).mockReturnValue({ data: undefined });
|
||||||
|
|
||||||
|
const ephemeralAgent = {
|
||||||
|
[EToolResources.file_search]: true,
|
||||||
|
[EToolResources.execute_code]: true,
|
||||||
|
};
|
||||||
|
|
||||||
const { result, rerender } = renderHook(
|
const { result, rerender } = renderHook(
|
||||||
({ agentId }) => useAgentToolPermissions(agentId),
|
({ agentId, ephemeralAgent }) => useAgentToolPermissions(agentId, ephemeralAgent),
|
||||||
{ initialProps: { agentId: null } }
|
{ 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.fileSearchAllowedByAgent).toBe(true);
|
||||||
expect(result.current.codeAllowedByAgent).toBe(true);
|
expect(result.current.codeAllowedByAgent).toBe(true);
|
||||||
|
|
||||||
// Switch to regular agent
|
// Switch to regular agent
|
||||||
rerender({ agentId: regularAgentId });
|
rerender({ agentId: regularAgentId, ephemeralAgent });
|
||||||
expect(result.current.fileSearchAllowedByAgent).toBe(false);
|
expect(result.current.fileSearchAllowedByAgent).toBe(false);
|
||||||
expect(result.current.codeAllowedByAgent).toBe(false);
|
expect(result.current.codeAllowedByAgent).toBe(false);
|
||||||
|
|
||||||
// Switch back to ephemeral
|
// Switch back to ephemeral
|
||||||
rerender({ agentId: '' });
|
rerender({ agentId: '', ephemeralAgent });
|
||||||
expect(result.current.fileSearchAllowedByAgent).toBe(true);
|
expect(result.current.fileSearchAllowedByAgent).toBe(true);
|
||||||
expect(result.current.codeAllowedByAgent).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', () => {
|
it('should handle query loading state', () => {
|
||||||
const agentId = 'loading-agent';
|
const agentId = 'loading-agent';
|
||||||
|
|
||||||
(useAgentsMapContext as jest.Mock).mockReturnValue({});
|
(useAgentsMapContext as jest.Mock).mockReturnValue({});
|
||||||
(useGetAgentByIdQuery as jest.Mock).mockReturnValue({
|
(useGetAgentByIdQuery as jest.Mock).mockReturnValue({
|
||||||
data: undefined,
|
data: undefined,
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
error: null,
|
error: null,
|
||||||
|
|
@ -421,9 +540,9 @@ describe('useAgentToolPermissions', () => {
|
||||||
|
|
||||||
it('should handle query error state', () => {
|
it('should handle query error state', () => {
|
||||||
const agentId = 'error-agent';
|
const agentId = 'error-agent';
|
||||||
|
|
||||||
(useAgentsMapContext as jest.Mock).mockReturnValue({});
|
(useAgentsMapContext as jest.Mock).mockReturnValue({});
|
||||||
(useGetAgentByIdQuery as jest.Mock).mockReturnValue({
|
(useGetAgentByIdQuery as jest.Mock).mockReturnValue({
|
||||||
data: undefined,
|
data: undefined,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: new Error('Failed to fetch agent'),
|
error: new Error('Failed to fetch agent'),
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { renderHook } from '@testing-library/react';
|
import { renderHook } from '@testing-library/react';
|
||||||
import { Tools } from 'librechat-data-provider';
|
import { Tools, EToolResources } from 'librechat-data-provider';
|
||||||
import useAgentToolPermissions from '../useAgentToolPermissions';
|
import useAgentToolPermissions from '../useAgentToolPermissions';
|
||||||
|
|
||||||
// Mock the dependencies
|
// Mock the dependencies
|
||||||
|
|
@ -20,36 +20,36 @@ describe('useAgentToolPermissions', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when no agentId is provided', () => {
|
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({});
|
mockUseAgentsMapContext.mockReturnValue({});
|
||||||
mockUseGetAgentByIdQuery.mockReturnValue({ data: undefined });
|
mockUseGetAgentByIdQuery.mockReturnValue({ data: undefined });
|
||||||
|
|
||||||
const { result } = renderHook(() => useAgentToolPermissions(null));
|
const { result } = renderHook(() => useAgentToolPermissions(null));
|
||||||
|
|
||||||
expect(result.current.fileSearchAllowedByAgent).toBe(true);
|
expect(result.current.fileSearchAllowedByAgent).toBe(false);
|
||||||
expect(result.current.codeAllowedByAgent).toBe(true);
|
expect(result.current.codeAllowedByAgent).toBe(false);
|
||||||
expect(result.current.tools).toBeUndefined();
|
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({});
|
mockUseAgentsMapContext.mockReturnValue({});
|
||||||
mockUseGetAgentByIdQuery.mockReturnValue({ data: undefined });
|
mockUseGetAgentByIdQuery.mockReturnValue({ data: undefined });
|
||||||
|
|
||||||
const { result } = renderHook(() => useAgentToolPermissions(undefined));
|
const { result } = renderHook(() => useAgentToolPermissions(undefined));
|
||||||
|
|
||||||
expect(result.current.fileSearchAllowedByAgent).toBe(true);
|
expect(result.current.fileSearchAllowedByAgent).toBe(false);
|
||||||
expect(result.current.codeAllowedByAgent).toBe(true);
|
expect(result.current.codeAllowedByAgent).toBe(false);
|
||||||
expect(result.current.tools).toBeUndefined();
|
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({});
|
mockUseAgentsMapContext.mockReturnValue({});
|
||||||
mockUseGetAgentByIdQuery.mockReturnValue({ data: undefined });
|
mockUseGetAgentByIdQuery.mockReturnValue({ data: undefined });
|
||||||
|
|
||||||
const { result } = renderHook(() => useAgentToolPermissions(''));
|
const { result } = renderHook(() => useAgentToolPermissions(''));
|
||||||
|
|
||||||
expect(result.current.fileSearchAllowedByAgent).toBe(true);
|
expect(result.current.fileSearchAllowedByAgent).toBe(false);
|
||||||
expect(result.current.codeAllowedByAgent).toBe(true);
|
expect(result.current.codeAllowedByAgent).toBe(false);
|
||||||
expect(result.current.tools).toBeUndefined();
|
expect(result.current.tools).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -177,4 +177,74 @@ describe('useAgentToolPermissions', () => {
|
||||||
expect(result.current.tools).toBeUndefined();
|
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]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useMemo } from 'react';
|
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 { useGetAgentByIdQuery } from '~/data-provider';
|
||||||
import { useAgentsMapContext } from '~/Providers';
|
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.
|
* 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
|
* @returns Object with boolean flags for file_search and execute_code permissions, plus the tools array
|
||||||
*/
|
*/
|
||||||
export default function useAgentToolPermissions(
|
export default function useAgentToolPermissions(
|
||||||
agentId: string | null | undefined,
|
agentId: string | null | undefined,
|
||||||
|
ephemeralAgent?: TEphemeralAgent | null,
|
||||||
): AgentToolPermissionsResult {
|
): AgentToolPermissionsResult {
|
||||||
const agentsMap = useAgentsMapContext();
|
const agentsMap = useAgentsMapContext();
|
||||||
|
|
||||||
|
|
@ -37,22 +40,26 @@ export default function useAgentToolPermissions(
|
||||||
);
|
);
|
||||||
|
|
||||||
const fileSearchAllowedByAgent = useMemo(() => {
|
const fileSearchAllowedByAgent = useMemo(() => {
|
||||||
// Allow for ephemeral agents
|
// Check ephemeral agent settings
|
||||||
if (isEphemeralAgent(agentId)) return true;
|
if (isEphemeralAgent(agentId)) {
|
||||||
|
return ephemeralAgent?.[EToolResources.file_search] ?? false;
|
||||||
|
}
|
||||||
// If agentId exists but agent not found, disallow
|
// If agentId exists but agent not found, disallow
|
||||||
if (!selectedAgent) return false;
|
if (!selectedAgent) return false;
|
||||||
// Check if the agent has the file_search tool
|
// Check if the agent has the file_search tool
|
||||||
return tools?.includes(Tools.file_search) ?? false;
|
return tools?.includes(Tools.file_search) ?? false;
|
||||||
}, [agentId, selectedAgent, tools]);
|
}, [agentId, selectedAgent, tools, ephemeralAgent]);
|
||||||
|
|
||||||
const codeAllowedByAgent = useMemo(() => {
|
const codeAllowedByAgent = useMemo(() => {
|
||||||
// Allow for ephemeral agents
|
// Check ephemeral agent settings
|
||||||
if (isEphemeralAgent(agentId)) return true;
|
if (isEphemeralAgent(agentId)) {
|
||||||
|
return ephemeralAgent?.[EToolResources.execute_code] ?? false;
|
||||||
|
}
|
||||||
// If agentId exists but agent not found, disallow
|
// If agentId exists but agent not found, disallow
|
||||||
if (!selectedAgent) return false;
|
if (!selectedAgent) return false;
|
||||||
// Check if the agent has the execute_code tool
|
// Check if the agent has the execute_code tool
|
||||||
return tools?.includes(Tools.execute_code) ?? false;
|
return tools?.includes(Tools.execute_code) ?? false;
|
||||||
}, [agentId, selectedAgent, tools]);
|
}, [agentId, selectedAgent, tools, ephemeralAgent]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
fileSearchAllowedByAgent,
|
fileSearchAllowedByAgent,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue