From eef93024d5b91b91f73d08011583f15cfcb892e7 Mon Sep 17 00:00:00 2001 From: Ben Verhees Date: Fri, 5 Sep 2025 15:14:55 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=8D=20fix:=20Display=20File=20Search?= =?UTF-8?q?=20Citations=20Based=20on=20Permissions=20(#9454)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Make file search citations conditional * refactor: improve permission handling to avoid redundant checks by including it in artifact * chore: reorder imports for better organization and clarity --------- Co-authored-by: Danny Avila --- api/app/clients/tools/util/fileSearch.js | 19 +++++++---- api/app/clients/tools/util/handleTools.js | 36 ++++++++++++++++++-- api/server/services/Files/Citations/index.js | 16 +++++---- 3 files changed, 54 insertions(+), 17 deletions(-) diff --git a/api/app/clients/tools/util/fileSearch.js b/api/app/clients/tools/util/fileSearch.js index 00fa46d43a..f51004cd98 100644 --- a/api/app/clients/tools/util/fileSearch.js +++ b/api/app/clients/tools/util/fileSearch.js @@ -71,9 +71,10 @@ const primeFiles = async (options) => { * @param {ServerRequest} options.req * @param {Array<{ file_id: string; filename: string }>} options.files * @param {string} [options.entity_id] + * @param {boolean} [options.fileCitations=false] - Whether to include citation instructions * @returns */ -const createFileSearchTool = async ({ req, files, entity_id }) => { +const createFileSearchTool = async ({ req, files, entity_id, fileCitations = false }) => { return tool( async ({ query }) => { if (files.length === 0) { @@ -142,9 +143,9 @@ const createFileSearchTool = async ({ req, files, entity_id }) => { const formattedString = formattedResults .map( (result, index) => - `File: ${result.filename}\nAnchor: \\ue202turn0file${index} (${result.filename})\nRelevance: ${(1.0 - result.distance).toFixed(4)}\nContent: ${ - result.content - }\n`, + `File: ${result.filename}${ + fileCitations ? `\nAnchor: \\ue202turn0file${index} (${result.filename})` : '' + }\nRelevance: ${(1.0 - result.distance).toFixed(4)}\nContent: ${result.content}\n`, ) .join('\n---\n'); @@ -158,12 +159,14 @@ const createFileSearchTool = async ({ req, files, entity_id }) => { pageRelevance: result.page ? { [result.page]: 1.0 - result.distance } : {}, })); - return [formattedString, { [Tools.file_search]: { sources } }]; + return [formattedString, { [Tools.file_search]: { sources, fileCitations } }]; }, { name: Tools.file_search, responseFormat: 'content_and_artifact', - description: `Performs semantic search across attached "${Tools.file_search}" documents using natural language queries. This tool analyzes the content of uploaded files to find relevant information, quotes, and passages that best match your query. Use this to extract specific information or find relevant sections within the available documents. + description: `Performs semantic search across attached "${Tools.file_search}" documents using natural language queries. This tool analyzes the content of uploaded files to find relevant information, quotes, and passages that best match your query. Use this to extract specific information or find relevant sections within the available documents.${ + fileCitations + ? ` **CITE FILE SEARCH RESULTS:** Use anchor markers immediately after statements derived from file content. Reference the filename in your text: @@ -171,7 +174,9 @@ Use anchor markers immediately after statements derived from file content. Refer - Page reference: "According to report.docx... \\ue202turn0file1" - Multi-file: "Multiple sources confirm... \\ue200\\ue202turn0file0\\ue202turn0file1\\ue201" -**ALWAYS mention the filename in your text before the citation marker. NEVER use markdown links or footnotes.**`, +**ALWAYS mention the filename in your text before the citation marker. NEVER use markdown links or footnotes.**` + : '' + }`, schema: z.object({ query: z .string() diff --git a/api/app/clients/tools/util/handleTools.js b/api/app/clients/tools/util/handleTools.js index a6950b1262..999d7bfc79 100644 --- a/api/app/clients/tools/util/handleTools.js +++ b/api/app/clients/tools/util/handleTools.js @@ -1,9 +1,16 @@ const { logger } = require('@librechat/data-schemas'); const { SerpAPI } = require('@langchain/community/tools/serpapi'); const { Calculator } = require('@langchain/community/tools/calculator'); -const { mcpToolPattern, loadWebSearchAuth } = require('@librechat/api'); +const { mcpToolPattern, loadWebSearchAuth, checkAccess } = require('@librechat/api'); const { EnvVar, createCodeExecutionTool, createSearchTool } = require('@librechat/agents'); -const { Tools, Constants, EToolResources, replaceSpecialVars } = require('librechat-data-provider'); +const { + Tools, + Constants, + Permissions, + EToolResources, + PermissionTypes, + replaceSpecialVars, +} = require('librechat-data-provider'); const { availableTools, manifestToolMap, @@ -27,6 +34,7 @@ const { getUserPluginAuthValue } = require('~/server/services/PluginService'); const { createMCPTool, createMCPTools } = require('~/server/services/MCP'); const { loadAuthValues } = require('~/server/services/Tools/credentials'); const { getCachedTools } = require('~/server/services/Config'); +const { getRoleByName } = require('~/models/Role'); /** * Validates the availability and authentication of tools for a user based on environment variables or user-specific plugin authentication values. @@ -281,7 +289,29 @@ const loadTools = async ({ if (toolContext) { toolContextMap[tool] = toolContext; } - return createFileSearchTool({ req: options.req, files, entity_id: agent?.id }); + + /** @type {boolean | undefined} Check if user has FILE_CITATIONS permission */ + let fileCitations; + if (fileCitations == null && options.req?.user != null) { + try { + fileCitations = await checkAccess({ + user: options.req.user, + permissionType: PermissionTypes.FILE_CITATIONS, + permissions: [Permissions.USE], + getRoleByName, + }); + } catch (error) { + logger.error('[handleTools] FILE_CITATIONS permission check failed:', error); + fileCitations = false; + } + } + + return createFileSearchTool({ + req: options.req, + files, + entity_id: agent?.id, + fileCitations, + }); }; continue; } else if (tool === Tools.web_search) { diff --git a/api/server/services/Files/Citations/index.js b/api/server/services/Files/Citations/index.js index 08ebe448bf..7cb2ee6de0 100644 --- a/api/server/services/Files/Citations/index.js +++ b/api/server/services/Files/Citations/index.js @@ -17,7 +17,7 @@ const { Files } = require('~/models'); * @param {IUser} options.user - The user object * @param {AppConfig} options.appConfig - The app configuration object * @param {GraphRunnableConfig['configurable']} options.metadata - The metadata - * @param {any} options.toolArtifact - The tool artifact containing structured data + * @param {{ [Tools.file_search]: { sources: Object[]; fileCitations: boolean } }} options.toolArtifact - The tool artifact containing structured data * @param {string} options.toolCallId - The tool call ID * @returns {Promise} The file search attachment or null */ @@ -29,12 +29,14 @@ async function processFileCitations({ user, appConfig, toolArtifact, toolCallId, if (user) { try { - const hasFileCitationsAccess = await checkAccess({ - user, - permissionType: PermissionTypes.FILE_CITATIONS, - permissions: [Permissions.USE], - getRoleByName, - }); + const hasFileCitationsAccess = + toolArtifact?.[Tools.file_search]?.fileCitations ?? + (await checkAccess({ + user, + permissionType: PermissionTypes.FILE_CITATIONS, + permissions: [Permissions.USE], + getRoleByName, + })); if (!hasFileCitationsAccess) { logger.debug(