From 21d98b85bde420026e5a98ef818d3b9d546a1dd0 Mon Sep 17 00:00:00 2001 From: Matheus Serpa <6334934+msserpa@users.noreply.github.com> Date: Sat, 20 Jun 2026 11:18:25 -0300 Subject: [PATCH] =?UTF-8?q?=F0=9F=8F=B7=EF=B8=8F=20fix:=20Scope=20File=20S?= =?UTF-8?q?earch=20entity=5Fid=20to=20Agent=20Knowledge-Base=20Files=20Onl?= =?UTF-8?q?y=20(#13693)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User-attached files are embedded by the RAG API under the user id (no entity), while only agent knowledge-base files are embedded under the agent's entity_id. Sending entity_id in every /query request made the RAG API's entity filter return no results for user attachments — with a shared agent, files attached to the message were effectively invisible to the file_search tool, while knowledge-base files kept working (which masked the bug). primeFiles now tags each file with fromAgent (whether it belongs to the agent's file_search.file_ids) and createQueryBody only includes entity_id when fromAgent === true — the safe default for callers that omit the flag is to query without entity scoping. Tests cover KB files, user attachments, the omitted-flag default, and restore RAG_API_URL. --- api/app/clients/tools/util/fileSearch.js | 16 +++-- .../app/clients/tools/util/fileSearch.test.js | 60 +++++++++++++++++++ 2 files changed, 72 insertions(+), 4 deletions(-) diff --git a/api/app/clients/tools/util/fileSearch.js b/api/app/clients/tools/util/fileSearch.js index a9faf71c54..be7589a826 100644 --- a/api/app/clients/tools/util/fileSearch.js +++ b/api/app/clients/tools/util/fileSearch.js @@ -25,7 +25,7 @@ const fileSearchJsonSchema = { * @param {Agent['tool_resources']} options.tool_resources * @param {string} [options.agentId] - The agent ID for file access control * @returns {Promise<{ - * files: Array<{ file_id: string; filename: string }>, + * files: Array<{ file_id: string; filename: string; fromAgent: boolean }>, * toolContext: string * }>} */ @@ -70,6 +70,7 @@ const primeFiles = async (options) => { files.push({ file_id: file.file_id, filename: file.filename, + fromAgent: agentResourceIds.has(file.file_id), }); } @@ -80,7 +81,7 @@ const primeFiles = async (options) => { * * @param {Object} options * @param {string} options.userId - * @param {Array<{ file_id: string; filename: string }>} options.files + * @param {Array<{ file_id: string; filename: string; fromAgent?: boolean }>} options.files * @param {string} [options.entity_id] * @param {boolean} [options.fileCitations=false] - Whether to include citation instructions * @returns @@ -97,7 +98,7 @@ const createFileSearchTool = async ({ userId, files, entity_id, fileCitations = } /** - * @param {import('librechat-data-provider').TFile} file + * @param {import('librechat-data-provider').TFile & { fromAgent?: boolean }} file * @returns {{ file_id: string, query: string, k: number, entity_id?: string }} */ const createQueryBody = (file) => { @@ -106,7 +107,14 @@ const createFileSearchTool = async ({ userId, files, entity_id, fileCitations = query, k: 5, }; - if (!entity_id) { + // User-attached files are embedded under the user id (no entity); + // only agent knowledge-base files carry the agent's entity_id. + // Sending entity_id for user attachments makes the RAG API's entity + // filter return no results for them. When files are provided by + // primeFiles, fromAgent is always set; for callers that pass files + // directly without the flag, the safe default is unscoped (no + // entity_id). + if (!entity_id || file.fromAgent !== true) { return body; } body.entity_id = entity_id; diff --git a/api/test/app/clients/tools/util/fileSearch.test.js b/api/test/app/clients/tools/util/fileSearch.test.js index 782e48f720..d9b5edb64c 100644 --- a/api/test/app/clients/tools/util/fileSearch.test.js +++ b/api/test/app/clients/tools/util/fileSearch.test.js @@ -230,3 +230,63 @@ describe('fileSearch.js - tuple return validation', () => { }); }); }); + +describe('entity_id scoping by file origin', () => { + const ORIGINAL_RAG_API_URL = process.env.RAG_API_URL; + + beforeEach(() => { + jest.clearAllMocks(); + process.env.RAG_API_URL = 'http://localhost:8000'; + generateShortLivedToken.mockReturnValue('mock-jwt-token'); + axios.post.mockResolvedValue({ data: [] }); + }); + + afterEach(() => { + if (ORIGINAL_RAG_API_URL === undefined) { + delete process.env.RAG_API_URL; + } else { + process.env.RAG_API_URL = ORIGINAL_RAG_API_URL; + } + }); + + function bodiesSent() { + return axios.post.mock.calls + .filter(([url]) => String(url).endsWith('/query')) + .map(([, body]) => body); + } + + it('sends entity_id only for agent knowledge-base files', async () => { + const tool = await createFileSearchTool({ + userId: 'user1', + entity_id: 'agent_123', + files: [ + { file_id: 'kb-1', filename: 'kb.pdf', fromAgent: true }, + { file_id: 'user-1', filename: 'attachment.txt', fromAgent: false }, + ], + }); + await tool.func({ query: 'q' }); + + const bodies = bodiesSent(); + expect(bodies.find((b) => b.file_id === 'kb-1').entity_id).toBe('agent_123'); + expect(bodies.find((b) => b.file_id === 'user-1').entity_id).toBeUndefined(); + }); + + it('omits entity_id when fromAgent is not set (safe default)', async () => { + const tool = await createFileSearchTool({ + userId: 'user1', + entity_id: 'agent_123', + files: [{ file_id: 'legacy-1', filename: 'legacy.pdf' }], + }); + await tool.func({ query: 'q' }); + expect(bodiesSent()[0].entity_id).toBeUndefined(); + }); + + it('sends no entity_id when none is provided', async () => { + const tool = await createFileSearchTool({ + userId: 'user1', + files: [{ file_id: 'f1', filename: 'a.txt', fromAgent: true }], + }); + await tool.func({ query: 'q' }); + expect(bodiesSent()[0].entity_id).toBeUndefined(); + }); +});