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(); + }); +});