mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-01-18 08:25:30 +01:00
🦥 feat: Add Deferred Tools as Agents Capability (#11295)
This commit is contained in:
parent
70a218ff82
commit
9cb9f42f52
8 changed files with 819 additions and 79 deletions
|
|
@ -1,9 +1,8 @@
|
|||
import React, { useState, useCallback } from 'react';
|
||||
import { ChevronDown, Clock } from 'lucide-react';
|
||||
import { useFormContext, useWatch } from 'react-hook-form';
|
||||
import { Constants } from 'librechat-data-provider';
|
||||
import { useFormContext, useWatch } from 'react-hook-form';
|
||||
import * as AccordionPrimitive from '@radix-ui/react-accordion';
|
||||
import type { AgentToolOptions } from 'librechat-data-provider';
|
||||
import {
|
||||
Label,
|
||||
ESide,
|
||||
|
|
@ -18,8 +17,15 @@ import {
|
|||
AccordionContent,
|
||||
OGDialogTemplate,
|
||||
} from '@librechat/client';
|
||||
import type { AgentToolOptions } from 'librechat-data-provider';
|
||||
import type { AgentForm, MCPServerInfo } from '~/common';
|
||||
import { useLocalize, useMCPServerManager, useRemoveMCPTool } from '~/hooks';
|
||||
import {
|
||||
useAgentCapabilities,
|
||||
useMCPServerManager,
|
||||
useGetAgentsConfig,
|
||||
useRemoveMCPTool,
|
||||
useLocalize,
|
||||
} from '~/hooks';
|
||||
import MCPServerStatusIcon from '~/components/MCP/MCPServerStatusIcon';
|
||||
import MCPConfigDialog from '~/components/MCP/MCPConfigDialog';
|
||||
import { cn } from '~/utils';
|
||||
|
|
@ -29,6 +35,8 @@ export default function MCPTool({ serverInfo }: { serverInfo?: MCPServerInfo })
|
|||
const { removeTool } = useRemoveMCPTool();
|
||||
const { getValues, setValue, control } = useFormContext<AgentForm>();
|
||||
const { getServerStatusIconProps, getConfigDialogProps } = useMCPServerManager();
|
||||
const { agentsConfig } = useGetAgentsConfig();
|
||||
const { deferredToolsEnabled } = useAgentCapabilities(agentsConfig?.capabilities);
|
||||
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
const [isHovering, setIsHovering] = useState(false);
|
||||
|
|
@ -242,45 +250,47 @@ export default function MCPTool({ serverInfo }: { serverInfo?: MCPServerInfo })
|
|||
</div>
|
||||
|
||||
{/* Defer All toggle - icon only with tooltip */}
|
||||
<TooltipAnchor
|
||||
description={
|
||||
areAllToolsDeferred
|
||||
? localize('com_ui_mcp_undefer_all')
|
||||
: localize('com_ui_mcp_defer_all')
|
||||
}
|
||||
side="top"
|
||||
role="button"
|
||||
tabIndex={isExpanded ? 0 : -1}
|
||||
aria-label={
|
||||
areAllToolsDeferred
|
||||
? localize('com_ui_mcp_undefer_all')
|
||||
: localize('com_ui_mcp_defer_all')
|
||||
}
|
||||
aria-pressed={areAllToolsDeferred}
|
||||
className={cn(
|
||||
'flex h-7 w-7 items-center justify-center rounded transition-colors duration-200',
|
||||
'focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-1',
|
||||
isExpanded ? 'visible' : 'pointer-events-none invisible',
|
||||
areAllToolsDeferred
|
||||
? 'bg-amber-500/20 text-amber-500 hover:bg-amber-500/30'
|
||||
: 'text-text-tertiary hover:bg-surface-hover hover:text-text-primary',
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleDeferAll();
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
{deferredToolsEnabled && (
|
||||
<TooltipAnchor
|
||||
description={
|
||||
areAllToolsDeferred
|
||||
? localize('com_ui_mcp_undefer_all')
|
||||
: localize('com_ui_mcp_defer_all')
|
||||
}
|
||||
side="top"
|
||||
role="button"
|
||||
tabIndex={isExpanded ? 0 : -1}
|
||||
aria-label={
|
||||
areAllToolsDeferred
|
||||
? localize('com_ui_mcp_undefer_all')
|
||||
: localize('com_ui_mcp_defer_all')
|
||||
}
|
||||
aria-pressed={areAllToolsDeferred}
|
||||
className={cn(
|
||||
'flex h-7 w-7 items-center justify-center rounded transition-colors duration-200',
|
||||
'focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-1',
|
||||
isExpanded ? 'visible' : 'pointer-events-none invisible',
|
||||
areAllToolsDeferred
|
||||
? 'bg-amber-500/20 text-amber-500 hover:bg-amber-500/30'
|
||||
: 'text-text-tertiary hover:bg-surface-hover hover:text-text-primary',
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleDeferAll();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Clock
|
||||
className={cn('h-4 w-4', areAllToolsDeferred && 'fill-amber-500/30')}
|
||||
/>
|
||||
</TooltipAnchor>
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
toggleDeferAll();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Clock
|
||||
className={cn('h-4 w-4', areAllToolsDeferred && 'fill-amber-500/30')}
|
||||
/>
|
||||
</TooltipAnchor>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
{/* Caret button for accordion */}
|
||||
|
|
@ -343,7 +353,7 @@ export default function MCPTool({ serverInfo }: { serverInfo?: MCPServerInfo })
|
|||
<AccordionContent className="relative ml-1 pt-1 before:absolute before:bottom-2 before:left-0 before:top-0 before:w-0.5 before:bg-border-medium">
|
||||
<div className="space-y-1">
|
||||
{serverInfo.tools?.map((subTool) => {
|
||||
const isDeferred = isToolDeferred(subTool.tool_id);
|
||||
const isDeferred = deferredToolsEnabled && isToolDeferred(subTool.tool_id);
|
||||
return (
|
||||
<label
|
||||
key={subTool.tool_id}
|
||||
|
|
@ -388,42 +398,44 @@ export default function MCPTool({ serverInfo }: { serverInfo?: MCPServerInfo })
|
|||
</span>
|
||||
<div className="ml-auto flex items-center gap-1">
|
||||
{/* Per-tool defer toggle - icon only */}
|
||||
<TooltipAnchor
|
||||
description={
|
||||
isDeferred
|
||||
? localize('com_ui_mcp_click_to_undefer')
|
||||
: localize('com_ui_mcp_click_to_defer')
|
||||
}
|
||||
side="top"
|
||||
role="button"
|
||||
aria-label={
|
||||
isDeferred
|
||||
? localize('com_ui_mcp_undefer')
|
||||
: localize('com_ui_mcp_defer_loading')
|
||||
}
|
||||
aria-pressed={isDeferred}
|
||||
className={cn(
|
||||
'flex h-6 w-6 items-center justify-center rounded transition-all duration-200',
|
||||
'focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-1',
|
||||
isDeferred
|
||||
? 'bg-amber-500/20 text-amber-500 hover:bg-amber-500/30'
|
||||
: 'text-text-tertiary opacity-0 hover:bg-surface-hover hover:text-text-primary group-focus-within/item:opacity-100 group-hover/item:opacity-100',
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
toggleToolDefer(subTool.tool_id);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
e.stopPropagation();
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
{deferredToolsEnabled && (
|
||||
<TooltipAnchor
|
||||
description={
|
||||
isDeferred
|
||||
? localize('com_ui_mcp_click_to_undefer')
|
||||
: localize('com_ui_mcp_click_to_defer')
|
||||
}
|
||||
side="top"
|
||||
role="button"
|
||||
aria-label={
|
||||
isDeferred
|
||||
? localize('com_ui_mcp_undefer')
|
||||
: localize('com_ui_mcp_defer_loading')
|
||||
}
|
||||
aria-pressed={isDeferred}
|
||||
className={cn(
|
||||
'flex h-6 w-6 items-center justify-center rounded transition-all duration-200',
|
||||
'focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-1',
|
||||
isDeferred
|
||||
? 'bg-amber-500/20 text-amber-500 hover:bg-amber-500/30'
|
||||
: 'text-text-tertiary opacity-0 hover:bg-surface-hover hover:text-text-primary group-focus-within/item:opacity-100 group-hover/item:opacity-100',
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
toggleToolDefer(subTool.tool_id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Clock className={cn('h-3.5 w-3.5', isDeferred && 'fill-amber-500/30')} />
|
||||
</TooltipAnchor>
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
e.stopPropagation();
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
toggleToolDefer(subTool.tool_id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Clock className={cn('h-3.5 w-3.5', isDeferred && 'fill-amber-500/30')} />
|
||||
</TooltipAnchor>
|
||||
)}
|
||||
{subTool.metadata.description && (
|
||||
<InfoHoverCard side={ESide.Left} text={subTool.metadata.description} />
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
import { renderHook } from '@testing-library/react';
|
||||
import { AgentCapabilities } from 'librechat-data-provider';
|
||||
import useAgentCapabilities from '../useAgentCapabilities';
|
||||
|
||||
describe('useAgentCapabilities', () => {
|
||||
it('should return all capabilities as false when capabilities is undefined', () => {
|
||||
const { result } = renderHook(() => useAgentCapabilities(undefined));
|
||||
|
||||
expect(result.current.toolsEnabled).toBe(false);
|
||||
expect(result.current.actionsEnabled).toBe(false);
|
||||
expect(result.current.artifactsEnabled).toBe(false);
|
||||
expect(result.current.ocrEnabled).toBe(false);
|
||||
expect(result.current.contextEnabled).toBe(false);
|
||||
expect(result.current.fileSearchEnabled).toBe(false);
|
||||
expect(result.current.webSearchEnabled).toBe(false);
|
||||
expect(result.current.codeEnabled).toBe(false);
|
||||
expect(result.current.deferredToolsEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('should return all capabilities as false when capabilities is empty array', () => {
|
||||
const { result } = renderHook(() => useAgentCapabilities([]));
|
||||
|
||||
expect(result.current.toolsEnabled).toBe(false);
|
||||
expect(result.current.deferredToolsEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for enabled capabilities', () => {
|
||||
const capabilities = [
|
||||
AgentCapabilities.tools,
|
||||
AgentCapabilities.deferred_tools,
|
||||
AgentCapabilities.file_search,
|
||||
];
|
||||
|
||||
const { result } = renderHook(() => useAgentCapabilities(capabilities));
|
||||
|
||||
expect(result.current.toolsEnabled).toBe(true);
|
||||
expect(result.current.deferredToolsEnabled).toBe(true);
|
||||
expect(result.current.fileSearchEnabled).toBe(true);
|
||||
expect(result.current.actionsEnabled).toBe(false);
|
||||
expect(result.current.webSearchEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('should return deferredToolsEnabled as true when deferred_tools is in capabilities', () => {
|
||||
const capabilities = [AgentCapabilities.deferred_tools];
|
||||
|
||||
const { result } = renderHook(() => useAgentCapabilities(capabilities));
|
||||
|
||||
expect(result.current.deferredToolsEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it('should return deferredToolsEnabled as false when deferred_tools is not in capabilities', () => {
|
||||
const capabilities = [
|
||||
AgentCapabilities.tools,
|
||||
AgentCapabilities.actions,
|
||||
AgentCapabilities.artifacts,
|
||||
];
|
||||
|
||||
const { result } = renderHook(() => useAgentCapabilities(capabilities));
|
||||
|
||||
expect(result.current.deferredToolsEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle all capabilities being enabled', () => {
|
||||
const capabilities = [
|
||||
AgentCapabilities.tools,
|
||||
AgentCapabilities.actions,
|
||||
AgentCapabilities.artifacts,
|
||||
AgentCapabilities.ocr,
|
||||
AgentCapabilities.context,
|
||||
AgentCapabilities.file_search,
|
||||
AgentCapabilities.web_search,
|
||||
AgentCapabilities.execute_code,
|
||||
AgentCapabilities.deferred_tools,
|
||||
];
|
||||
|
||||
const { result } = renderHook(() => useAgentCapabilities(capabilities));
|
||||
|
||||
expect(result.current.toolsEnabled).toBe(true);
|
||||
expect(result.current.actionsEnabled).toBe(true);
|
||||
expect(result.current.artifactsEnabled).toBe(true);
|
||||
expect(result.current.ocrEnabled).toBe(true);
|
||||
expect(result.current.contextEnabled).toBe(true);
|
||||
expect(result.current.fileSearchEnabled).toBe(true);
|
||||
expect(result.current.webSearchEnabled).toBe(true);
|
||||
expect(result.current.codeEnabled).toBe(true);
|
||||
expect(result.current.deferredToolsEnabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
@ -10,6 +10,7 @@ interface AgentCapabilitiesResult {
|
|||
fileSearchEnabled: boolean;
|
||||
webSearchEnabled: boolean;
|
||||
codeEnabled: boolean;
|
||||
deferredToolsEnabled: boolean;
|
||||
}
|
||||
|
||||
export default function useAgentCapabilities(
|
||||
|
|
@ -55,6 +56,11 @@ export default function useAgentCapabilities(
|
|||
[capabilities],
|
||||
);
|
||||
|
||||
const deferredToolsEnabled = useMemo(
|
||||
() => capabilities?.includes(AgentCapabilities.deferred_tools) ?? false,
|
||||
[capabilities],
|
||||
);
|
||||
|
||||
return {
|
||||
ocrEnabled,
|
||||
codeEnabled,
|
||||
|
|
@ -64,5 +70,6 @@ export default function useAgentCapabilities(
|
|||
artifactsEnabled,
|
||||
webSearchEnabled,
|
||||
fileSearchEnabled,
|
||||
deferredToolsEnabled,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue