diff --git a/api/models/File.js b/api/models/File.js index 1ee943131d..5e90c86fe4 100644 --- a/api/models/File.js +++ b/api/models/File.js @@ -42,7 +42,7 @@ const getToolFilesByIds = async (fileIds, toolResourceSet) => { $or: [], }; - if (toolResourceSet.has(EToolResources.ocr)) { + if (toolResourceSet.has(EToolResources.context)) { filter.$or.push({ text: { $exists: true, $ne: null }, context: FileContext.agents }); } if (toolResourceSet.has(EToolResources.file_search)) { diff --git a/api/server/controllers/agents/__tests__/v1.spec.js b/api/server/controllers/agents/__tests__/v1.spec.js index b097cd98ce..b7e7b67a22 100644 --- a/api/server/controllers/agents/__tests__/v1.spec.js +++ b/api/server/controllers/agents/__tests__/v1.spec.js @@ -158,7 +158,7 @@ describe('duplicateAgent', () => { }); }); - it('should handle tool_resources.ocr correctly', async () => { + it('should convert `tool_resources.ocr` to `tool_resources.context`', async () => { const mockAgent = { id: 'agent_123', name: 'Test Agent', @@ -178,7 +178,7 @@ describe('duplicateAgent', () => { expect(createAgent).toHaveBeenCalledWith( expect.objectContaining({ tool_resources: { - ocr: { enabled: true, config: 'test' }, + context: { enabled: true, config: 'test' }, }, }), ); diff --git a/api/server/controllers/agents/v1.js b/api/server/controllers/agents/v1.js index eb98c5adb0..0334d965db 100644 --- a/api/server/controllers/agents/v1.js +++ b/api/server/controllers/agents/v1.js @@ -2,7 +2,12 @@ const { z } = require('zod'); const fs = require('fs').promises; const { nanoid } = require('nanoid'); const { logger } = require('@librechat/data-schemas'); -const { agentCreateSchema, agentUpdateSchema } = require('@librechat/api'); +const { + agentCreateSchema, + agentUpdateSchema, + mergeAgentOcrConversion, + convertOcrToContextInPlace, +} = require('@librechat/api'); const { Tools, Constants, @@ -198,19 +203,32 @@ const getAgentHandler = async (req, res, expandProperties = false) => { * @param {object} req.params - Request params * @param {string} req.params.id - Agent identifier. * @param {AgentUpdateParams} req.body - The Agent update parameters. - * @returns {Agent} 200 - success response - application/json + * @returns {Promise} 200 - success response - application/json */ const updateAgentHandler = async (req, res) => { try { const id = req.params.id; const validatedData = agentUpdateSchema.parse(req.body); const { _id, ...updateData } = removeNullishValues(validatedData); + + // Convert OCR to context in incoming updateData + convertOcrToContextInPlace(updateData); + const existingAgent = await getAgent({ id }); if (!existingAgent) { return res.status(404).json({ error: 'Agent not found' }); } + // Convert legacy OCR tool resource to context format in existing agent + const ocrConversion = mergeAgentOcrConversion(existingAgent, updateData); + if (ocrConversion.tool_resources) { + updateData.tool_resources = ocrConversion.tool_resources; + } + if (ocrConversion.tools) { + updateData.tools = ocrConversion.tools; + } + let updatedAgent = Object.keys(updateData).length > 0 ? await updateAgent({ id }, updateData, { @@ -255,7 +273,7 @@ const updateAgentHandler = async (req, res) => { * @param {object} req - Express Request * @param {object} req.params - Request params * @param {string} req.params.id - Agent identifier. - * @returns {Agent} 201 - success response - application/json + * @returns {Promise} 201 - success response - application/json */ const duplicateAgentHandler = async (req, res) => { const { id } = req.params; @@ -288,9 +306,19 @@ const duplicateAgentHandler = async (req, res) => { hour12: false, })})`; + if (_tool_resources?.[EToolResources.context]) { + cloneData.tool_resources = { + [EToolResources.context]: _tool_resources[EToolResources.context], + }; + } + if (_tool_resources?.[EToolResources.ocr]) { cloneData.tool_resources = { - [EToolResources.ocr]: _tool_resources[EToolResources.ocr], + /** Legacy conversion from `ocr` to `context` */ + [EToolResources.context]: { + ...(_tool_resources[EToolResources.context] ?? {}), + ..._tool_resources[EToolResources.ocr], + }, }; } @@ -382,7 +410,7 @@ const duplicateAgentHandler = async (req, res) => { * @param {object} req - Express Request * @param {object} req.params - Request params * @param {string} req.params.id - Agent identifier. - * @returns {Agent} 200 - success response - application/json + * @returns {Promise} 200 - success response - application/json */ const deleteAgentHandler = async (req, res) => { try { @@ -484,7 +512,7 @@ const getListAgentsHandler = async (req, res) => { * @param {Express.Multer.File} req.file - The avatar image file. * @param {object} req.body - Request body * @param {string} [req.body.avatar] - Optional avatar for the agent's avatar. - * @returns {Object} 200 - success response - application/json + * @returns {Promise} 200 - success response - application/json */ const uploadAgentAvatarHandler = async (req, res) => { try { diff --git a/api/server/controllers/agents/v1.spec.js b/api/server/controllers/agents/v1.spec.js index c31839feb1..b8d4d50ee6 100644 --- a/api/server/controllers/agents/v1.spec.js +++ b/api/server/controllers/agents/v1.spec.js @@ -512,6 +512,7 @@ describe('Agent Controllers - Mass Assignment Protection', () => { mockReq.params.id = existingAgentId; mockReq.body = { tool_resources: { + /** Legacy conversion from `ocr` to `context` */ ocr: { file_ids: ['ocr1', 'ocr2'], }, @@ -531,7 +532,8 @@ describe('Agent Controllers - Mass Assignment Protection', () => { const updatedAgent = mockRes.json.mock.calls[0][0]; expect(updatedAgent.tool_resources).toBeDefined(); - expect(updatedAgent.tool_resources.ocr).toBeDefined(); + expect(updatedAgent.tool_resources.ocr).toBeUndefined(); + expect(updatedAgent.tool_resources.context).toBeDefined(); expect(updatedAgent.tool_resources.execute_code).toBeDefined(); expect(updatedAgent.tool_resources.invalid_tool).toBeUndefined(); }); diff --git a/api/server/middleware/accessResources/fileAccess.js b/api/server/middleware/accessResources/fileAccess.js index 3556a278a4..b26a512f5f 100644 --- a/api/server/middleware/accessResources/fileAccess.js +++ b/api/server/middleware/accessResources/fileAccess.js @@ -10,11 +10,12 @@ const { getFiles } = require('~/models/File'); */ const checkAgentBasedFileAccess = async ({ userId, role, fileId }) => { try { - // Find agents that have this file in their tool_resources + /** Agents that have this file in their tool_resources */ const agentsWithFile = await getAgents({ $or: [ - { 'tool_resources.file_search.file_ids': fileId }, { 'tool_resources.execute_code.file_ids': fileId }, + { 'tool_resources.file_search.file_ids': fileId }, + { 'tool_resources.context.file_ids': fileId }, { 'tool_resources.ocr.file_ids': fileId }, ], }); @@ -83,7 +84,6 @@ const fileAccess = async (req, res, next) => { }); } - // Get the file const [file] = await getFiles({ file_id: fileId }); if (!file) { return res.status(404).json({ @@ -92,20 +92,18 @@ const fileAccess = async (req, res, next) => { }); } - // Check if user owns the file if (file.user && file.user.toString() === userId) { req.fileAccess = { file }; return next(); } - // Check agent-based access (file inherits agent permissions) + /** Agent-based access (file inherits agent permissions) */ const hasAgentAccess = await checkAgentBasedFileAccess({ userId, role: userRole, fileId }); if (hasAgentAccess) { req.fileAccess = { file }; return next(); } - // No access logger.warn(`[fileAccess] User ${userId} denied access to file ${fileId}`); return res.status(403).json({ error: 'Forbidden', diff --git a/api/server/services/Files/process.js b/api/server/services/Files/process.js index 8b3b5fbcfb..367e7bf348 100644 --- a/api/server/services/Files/process.js +++ b/api/server/services/Files/process.js @@ -552,7 +552,7 @@ const processAgentFileUpload = async ({ req, res, metadata }) => { throw new Error('File search is not enabled for Agents'); } // Note: File search processing continues to dual storage logic below - } else if (tool_resource === EToolResources.ocr) { + } else if (tool_resource === EToolResources.context) { const { file_id, temp_file_id = null } = metadata; /** diff --git a/api/server/services/ToolService.js b/api/server/services/ToolService.js index 87005e64d8..174eae0788 100644 --- a/api/server/services/ToolService.js +++ b/api/server/services/ToolService.js @@ -353,7 +353,12 @@ async function processRequiredActions(client, requiredActions) { async function loadAgentTools({ req, res, agent, signal, tool_resources, openAIApiKey }) { if (!agent.tools || agent.tools.length === 0) { return {}; - } else if (agent.tools && agent.tools.length === 1 && agent.tools[0] === AgentCapabilities.ocr) { + } else if ( + agent.tools && + agent.tools.length === 1 && + /** Legacy handling for `ocr` as may still exist in existing Agents */ + (agent.tools[0] === AgentCapabilities.context || agent.tools[0] === AgentCapabilities.ocr) + ) { return {}; } diff --git a/client/src/components/Chat/Input/Files/AttachFileMenu.tsx b/client/src/components/Chat/Input/Files/AttachFileMenu.tsx index 2ed5a0a3e2..75398c2633 100644 --- a/client/src/components/Chat/Input/Files/AttachFileMenu.tsx +++ b/client/src/components/Chat/Input/Files/AttachFileMenu.tsx @@ -94,11 +94,11 @@ const AttachFileMenu = ({ }, ]; - if (capabilities.ocrEnabled) { + if (capabilities.contextEnabled) { items.push({ label: localize('com_ui_upload_ocr_text'), onClick: () => { - setToolResource(EToolResources.ocr); + setToolResource(EToolResources.context); onAction(); }, icon: , diff --git a/client/src/components/Chat/Input/Files/DragDropModal.tsx b/client/src/components/Chat/Input/Files/DragDropModal.tsx index 6f506c65fe..e9992c4dcb 100644 --- a/client/src/components/Chat/Input/Files/DragDropModal.tsx +++ b/client/src/components/Chat/Input/Files/DragDropModal.tsx @@ -64,10 +64,10 @@ const DragDropModal = ({ onOptionSelect, setShowModal, files, isVisible }: DragD icon: , }); } - if (capabilities.ocrEnabled) { + if (capabilities.contextEnabled) { _options.push({ label: localize('com_ui_upload_ocr_text'), - value: EToolResources.ocr, + value: EToolResources.context, icon: , }); } diff --git a/client/src/components/SidePanel/Agents/AgentConfig.tsx b/client/src/components/SidePanel/Agents/AgentConfig.tsx index a48a372591..7f296fe8c3 100644 --- a/client/src/components/SidePanel/Agents/AgentConfig.tsx +++ b/client/src/components/SidePanel/Agents/AgentConfig.tsx @@ -79,9 +79,9 @@ export default function AgentConfig({ createMutation }: Pick