diff --git a/api/package.json b/api/package.json index 9e134bd32a..5dcf531b99 100644 --- a/api/package.json +++ b/api/package.json @@ -46,7 +46,7 @@ "@googleapis/youtube": "^20.0.0", "@keyv/redis": "^4.3.3", "@langchain/core": "^0.3.80", - "@librechat/agents": "^3.0.66", + "@librechat/agents": "^3.0.75", "@librechat/api": "*", "@librechat/data-schemas": "*", "@microsoft/microsoft-graph-client": "^3.0.7", diff --git a/api/server/controllers/agents/__tests__/callbacks.spec.js b/api/server/controllers/agents/__tests__/callbacks.spec.js index 7922c31efa..103f9f3236 100644 --- a/api/server/controllers/agents/__tests__/callbacks.spec.js +++ b/api/server/controllers/agents/__tests__/callbacks.spec.js @@ -16,9 +16,7 @@ jest.mock('@librechat/data-schemas', () => ({ })); jest.mock('@librechat/agents', () => ({ - EnvVar: { CODE_API_KEY: 'CODE_API_KEY' }, - Providers: { GOOGLE: 'google' }, - GraphEvents: {}, + ...jest.requireActual('@librechat/agents'), getMessageId: jest.fn(), ToolEndHandler: jest.fn(), handleToolCalls: jest.fn(), diff --git a/api/server/controllers/agents/callbacks.js b/api/server/controllers/agents/callbacks.js index aee419577a..ff70d13d37 100644 --- a/api/server/controllers/agents/callbacks.js +++ b/api/server/controllers/agents/callbacks.js @@ -1,6 +1,7 @@ const { nanoid } = require('nanoid'); -const { sendEvent, GenerationJobManager } = require('@librechat/api'); +const { Constants } = require('@librechat/agents'); const { logger } = require('@librechat/data-schemas'); +const { sendEvent, GenerationJobManager } = require('@librechat/api'); const { Tools, StepTypes, FileContext, ErrorTypes } = require('librechat-data-provider'); const { EnvVar, @@ -441,10 +442,10 @@ function createToolEndCallback({ req, res, artifactPromises, streamId = null }) return; } - { - if (output.name !== Tools.execute_code) { - return; - } + const isCodeTool = + output.name === Tools.execute_code || output.name === Constants.PROGRAMMATIC_TOOL_CALLING; + if (!isCodeTool) { + return; } if (!output.artifact.files) { diff --git a/api/server/controllers/agents/client.js b/api/server/controllers/agents/client.js index 79e63d1c7f..f7f1f5161f 100644 --- a/api/server/controllers/agents/client.js +++ b/api/server/controllers/agents/client.js @@ -1019,6 +1019,7 @@ class AgentClient extends BaseClient { run = await createRun({ agents, + messages, indexTokenCountMap, runId: this.responseMessageId, signal: abortController.signal, diff --git a/api/server/services/Endpoints/agents/initialize.js b/api/server/services/Endpoints/agents/initialize.js index 626beed153..fc054c1e6f 100644 --- a/api/server/services/Endpoints/agents/initialize.js +++ b/api/server/services/Endpoints/agents/initialize.js @@ -43,13 +43,23 @@ function createToolLoader(signal, streamId = null) { * @param {string} params.model * @param {AgentToolResources} params.tool_resources * @returns {Promise<{ - * tools: StructuredTool[], - * toolContextMap: Record, - * userMCPAuthMap?: Record> + * tools: StructuredTool[], + * toolContextMap: Record, + * userMCPAuthMap?: Record>, + * toolRegistry?: import('@librechat/agents').LCToolRegistry * } | undefined>} */ - return async function loadTools({ req, res, agentId, tools, provider, model, tool_resources }) { - const agent = { id: agentId, tools, provider, model }; + return async function loadTools({ + req, + res, + tools, + model, + agentId, + provider, + tool_options, + tool_resources, + }) { + const agent = { id: agentId, tools, provider, model, tool_options }; try { return await loadAgentTools({ req, diff --git a/api/server/services/MCP.js b/api/server/services/MCP.js index 81d7107de4..220c1f7d7e 100644 --- a/api/server/services/MCP.js +++ b/api/server/services/MCP.js @@ -548,6 +548,7 @@ function createToolInstance({ }); toolInstance.mcp = true; toolInstance.mcpRawServerName = serverName; + toolInstance.mcpJsonSchema = parameters; return toolInstance; } diff --git a/api/server/services/ToolService.js b/api/server/services/ToolService.js index 1e2074cdf4..8fcb261b41 100644 --- a/api/server/services/ToolService.js +++ b/api/server/services/ToolService.js @@ -6,6 +6,7 @@ const { hasCustomUserVars, getUserMCPAuthMap, isActionDomainAllowed, + buildToolClassification, } = require('@librechat/api'); const { Tools, @@ -36,6 +37,7 @@ const { recordUsage } = require('~/server/services/Threads'); const { loadTools } = require('~/app/clients/tools/util'); const { redactMessage } = require('~/config/parsers'); const { findPluginAuthsByKeys } = require('~/models'); +const { loadAuthValues } = require('~/server/services/Tools/credentials'); /** * Processes the required actions by calling the appropriate tools and returning the outputs. * @param {OpenAIClient} client - OpenAI or StreamRunManager Client. @@ -367,7 +369,13 @@ async function processRequiredActions(client, requiredActions) { * @param {AbortSignal} params.signal * @param {Pick> }>} The agent tools. + * @returns {Promise<{ + * tools?: StructuredTool[]; + * toolContextMap?: Record; + * userMCPAuthMap?: Record>; + * toolRegistry?: Map; + * hasDeferredTools?: boolean; + * }>} The agent tools and registry. */ async function loadAgentTools({ req, @@ -510,11 +518,23 @@ async function loadAgentTools({ return map; }, {}); + /** Build tool registry from MCP tools and create PTC/tool search tools if configured */ + const { toolRegistry, additionalTools, hasDeferredTools } = await buildToolClassification({ + loadedTools, + userId: req.user.id, + agentId: agent.id, + agentToolOptions: agent.tool_options, + loadAuthValues, + }); + agentTools.push(...additionalTools); + if (!checkCapability(AgentCapabilities.actions)) { return { tools: agentTools, userMCPAuthMap, toolContextMap, + toolRegistry, + hasDeferredTools, }; } @@ -527,6 +547,8 @@ async function loadAgentTools({ tools: agentTools, userMCPAuthMap, toolContextMap, + toolRegistry, + hasDeferredTools, }; } @@ -654,6 +676,8 @@ async function loadAgentTools({ tools: agentTools, toolContextMap, userMCPAuthMap, + toolRegistry, + hasDeferredTools, }; } diff --git a/client/src/common/agents-types.ts b/client/src/common/agents-types.ts index 9ac6b440a3..c3832b7ff8 100644 --- a/client/src/common/agents-types.ts +++ b/client/src/common/agents-types.ts @@ -1,6 +1,7 @@ import { AgentCapabilities, ArtifactModes } from 'librechat-data-provider'; import type { AgentModelParameters, + AgentToolOptions, SupportContact, AgentProvider, GraphEdge, @@ -33,6 +34,8 @@ export type AgentForm = { model: string | null; model_parameters: AgentModelParameters; tools?: string[]; + /** Per-tool configuration options (deferred loading, allowed callers, etc.) */ + tool_options?: AgentToolOptions; provider?: AgentProvider | OptionWithIcon; /** @deprecated Use edges instead */ agent_ids?: string[]; diff --git a/client/src/components/Chat/Messages/Content/Part.tsx b/client/src/components/Chat/Messages/Content/Part.tsx index bfa2b28fac..4a74e3606f 100644 --- a/client/src/components/Chat/Messages/Content/Part.tsx +++ b/client/src/components/Chat/Messages/Content/Part.tsx @@ -91,7 +91,11 @@ const Part = memo( const isToolCall = 'args' in toolCall && (!toolCall.type || toolCall.type === ToolCallTypes.TOOL_CALL); - if (isToolCall && toolCall.name === Tools.execute_code) { + if ( + isToolCall && + (toolCall.name === Tools.execute_code || + toolCall.name === Constants.PROGRAMMATIC_TOOL_CALLING) + ) { return ( (0); const prevShowCodeRef = useRef(showCode); - const { lang, code } = useParseArgs(args) ?? ({} as ParsedArgs); + const { lang = 'py', code } = useParseArgs(args) ?? ({} as ParsedArgs); const progress = useProgress(initialProgress); useEffect(() => { diff --git a/client/src/components/SidePanel/Agents/AgentPanel.tsx b/client/src/components/SidePanel/Agents/AgentPanel.tsx index 86ec27dc5e..f74dcfddcc 100644 --- a/client/src/components/SidePanel/Agents/AgentPanel.tsx +++ b/client/src/components/SidePanel/Agents/AgentPanel.tsx @@ -23,6 +23,7 @@ import { import { createProviderOption, getDefaultAgentFormValues } from '~/utils'; import { useResourcePermissions } from '~/hooks/useResourcePermissions'; import { useSelectAgent, useLocalize, useAuthContext } from '~/hooks'; +import type { TranslationKeys } from '~/hooks/useLocalize'; import { useAgentPanelContext } from '~/Providers/AgentPanelContext'; import AgentPanelSkeleton from './AgentPanelSkeleton'; import AdvancedPanel from './Advanced/AdvancedPanel'; @@ -36,8 +37,8 @@ import ModelPanel from './ModelPanel'; function getUpdateToastMessage( noVersionChange: boolean, avatarActionState: AgentForm['avatar_action'], - name: string | undefined, - localize: (key: string, vars?: Record | Array) => string, + name: string | null | undefined, + localize: (key: TranslationKeys, vars?: Record) => string, ): string | null { // If only avatar upload is pending (separate endpoint), suppress the no-changes toast. if (noVersionChange && avatarActionState === 'upload') { @@ -72,6 +73,7 @@ export function composeAgentUpdatePayload(data: AgentForm, agent_id?: string | n recursion_limit, category, support_contact, + tool_options, avatar_action: avatarActionState, } = data; @@ -97,6 +99,7 @@ export function composeAgentUpdatePayload(data: AgentForm, agent_id?: string | n recursion_limit, category, support_contact, + tool_options, ...(shouldResetAvatar ? { avatar: null } : {}), }, provider, @@ -545,7 +548,7 @@ export default function AgentPanel() { (); + const { getValues, setValue, control } = useFormContext(); const { getServerStatusIconProps, getConfigDialogProps } = useMCPServerManager(); const [isFocused, setIsFocused] = useState(false); const [isHovering, setIsHovering] = useState(false); const [accordionValue, setAccordionValue] = useState(''); + const formToolOptions = useWatch({ control, name: 'tool_options' }); + + /** Check if a specific tool has defer_loading enabled */ + const isToolDeferred = useCallback( + (toolId: string): boolean => formToolOptions?.[toolId]?.defer_loading === true, + [formToolOptions], + ); + + /** Toggle defer_loading for a specific tool */ + const toggleToolDefer = useCallback( + (toolId: string) => { + const currentOptions = getValues('tool_options') || {}; + const currentToolOptions = currentOptions[toolId] || {}; + const newDeferred = !currentToolOptions.defer_loading; + + const updatedOptions: AgentToolOptions = { ...currentOptions }; + + if (newDeferred) { + updatedOptions[toolId] = { + ...currentToolOptions, + defer_loading: true, + }; + } else { + const { defer_loading: _, ...restOptions } = currentToolOptions; + if (Object.keys(restOptions).length === 0) { + delete updatedOptions[toolId]; + } else { + updatedOptions[toolId] = restOptions; + } + } + + setValue('tool_options', updatedOptions, { shouldDirty: true }); + }, + [getValues, setValue], + ); + + /** Check if all server tools are deferred */ + const areAllToolsDeferred = + serverInfo?.tools && + serverInfo.tools.length > 0 && + serverInfo.tools.every((tool) => formToolOptions?.[tool.tool_id]?.defer_loading === true); + + /** Toggle defer_loading for all tools from this server */ + const toggleDeferAll = useCallback(() => { + if (!serverInfo?.tools) return; + + const shouldDefer = !areAllToolsDeferred; + const currentOptions = getValues('tool_options') || {}; + const updatedOptions: AgentToolOptions = { ...currentOptions }; + + for (const tool of serverInfo.tools) { + if (shouldDefer) { + updatedOptions[tool.tool_id] = { + ...(updatedOptions[tool.tool_id] || {}), + defer_loading: true, + }; + } else { + if (updatedOptions[tool.tool_id]) { + delete updatedOptions[tool.tool_id].defer_loading; + if (Object.keys(updatedOptions[tool.tool_id]).length === 0) { + delete updatedOptions[tool.tool_id]; + } + } + } + } + + setValue('tool_options', updatedOptions, { shouldDirty: true }); + }, [serverInfo?.tools, getValues, setValue, areAllToolsDeferred]); + if (!serverInfo) { return null; } @@ -170,6 +241,47 @@ export default function MCPTool({ serverInfo }: { serverInfo?: MCPServerInfo }) /> + {/* Defer All toggle - icon only with tooltip */} + { + e.stopPropagation(); + toggleDeferAll(); + }} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + e.stopPropagation(); + toggleDeferAll(); + } + }} + > + + +
{/* Caret button for accordion */} @@ -230,52 +342,95 @@ export default function MCPTool({ serverInfo }: { serverInfo?: MCPServerInfo })
- {serverInfo.tools?.map((subTool) => ( -