mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-28 12:44:28 +00:00
🏷️ fix: Scope File Search entity_id to Agent Knowledge-Base Files Only (#13693)
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.
This commit is contained in:
parent
3926fda234
commit
21d98b85bd
2 changed files with 72 additions and 4 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue