| const { Tools } = require('librechat-data-provider'); |
| const { |
| processFileCitations, |
| applyCitationLimits, |
| enhanceSourcesWithMetadata, |
| } = require('~/server/services/Files/Citations'); |
|
|
| |
| jest.mock('~/models', () => ({ |
| Files: { |
| find: jest.fn().mockResolvedValue([]), |
| }, |
| })); |
|
|
| jest.mock('~/models/Role', () => ({ |
| getRoleByName: jest.fn(), |
| })); |
|
|
| jest.mock('@librechat/api', () => ({ |
| checkAccess: jest.fn().mockResolvedValue(true), |
| })); |
|
|
| jest.mock('~/cache/getLogStores', () => () => ({ |
| get: jest.fn().mockResolvedValue({ |
| agents: { |
| maxCitations: 30, |
| maxCitationsPerFile: 5, |
| minRelevanceScore: 0.45, |
| }, |
| fileStrategy: 'local', |
| }), |
| set: jest.fn(), |
| delete: jest.fn(), |
| })); |
|
|
| jest.mock('~/config', () => ({ |
| logger: { |
| debug: jest.fn(), |
| error: jest.fn(), |
| warn: jest.fn(), |
| }, |
| })); |
|
|
| describe('processFileCitations', () => { |
| const mockReq = { |
| user: { |
| id: 'user123', |
| }, |
| }; |
|
|
| const mockAppConfig = { |
| endpoints: { |
| agents: { |
| maxCitations: 30, |
| maxCitationsPerFile: 5, |
| minRelevanceScore: 0.45, |
| }, |
| }, |
| fileStrategy: 'local', |
| }; |
|
|
| const mockMetadata = { |
| run_id: 'run123', |
| thread_id: 'conv123', |
| }; |
|
|
| describe('file search artifact processing', () => { |
| it('should process file search artifacts correctly', async () => { |
| const toolArtifact = { |
| [Tools.file_search]: { |
| sources: [ |
| { |
| fileId: 'file_123', |
| fileName: 'example.pdf', |
| pages: [5], |
| relevance: 0.85, |
| type: 'file', |
| pageRelevance: { 5: 0.85 }, |
| content: 'This is the content', |
| }, |
| { |
| fileId: 'file_456', |
| fileName: 'document.txt', |
| pages: [], |
| relevance: 0.72, |
| type: 'file', |
| pageRelevance: {}, |
| content: 'Another document', |
| }, |
| ], |
| }, |
| }; |
|
|
| const result = await processFileCitations({ |
| toolArtifact, |
| toolCallId: 'call_123', |
| metadata: mockMetadata, |
| user: mockReq.user, |
| appConfig: mockAppConfig, |
| }); |
|
|
| expect(result).toBeTruthy(); |
| expect(result.type).toBe('file_search'); |
| expect(result.file_search.sources).toHaveLength(2); |
| expect(result.file_search.sources[0].fileId).toBe('file_123'); |
| expect(result.file_search.sources[0].relevance).toBe(0.85); |
| }); |
|
|
| it('should return null for non-file_search tools', async () => { |
| const result = await processFileCitations({ |
| toolArtifact: { other_tool: {} }, |
| toolCallId: 'call_123', |
| metadata: mockMetadata, |
| user: mockReq.user, |
| appConfig: mockAppConfig, |
| }); |
|
|
| expect(result).toBeNull(); |
| }); |
|
|
| it('should filter results below relevance threshold', async () => { |
| const toolArtifact = { |
| [Tools.file_search]: { |
| sources: [ |
| { |
| fileId: 'file_789', |
| fileName: 'low_relevance.pdf', |
| pages: [], |
| relevance: 0.2, |
| type: 'file', |
| pageRelevance: {}, |
| content: 'Low relevance content', |
| }, |
| ], |
| }, |
| }; |
|
|
| const result = await processFileCitations({ |
| toolArtifact, |
| toolCallId: 'call_123', |
| metadata: mockMetadata, |
| user: mockReq.user, |
| appConfig: mockAppConfig, |
| }); |
|
|
| expect(result).toBeNull(); |
| }); |
|
|
| it('should return null when artifact is missing file_search data', async () => { |
| const result = await processFileCitations({ |
| toolArtifact: {}, |
| toolCallId: 'call_123', |
| metadata: mockMetadata, |
| user: mockReq.user, |
| appConfig: mockAppConfig, |
| }); |
|
|
| expect(result).toBeNull(); |
| }); |
| }); |
|
|
| describe('applyCitationLimits', () => { |
| it('should limit citations per file and total', () => { |
| const sources = [ |
| { fileId: 'file1', relevance: 0.9 }, |
| { fileId: 'file1', relevance: 0.8 }, |
| { fileId: 'file1', relevance: 0.7 }, |
| { fileId: 'file2', relevance: 0.85 }, |
| { fileId: 'file2', relevance: 0.75 }, |
| ]; |
|
|
| const result = applyCitationLimits(sources, 3, 2); |
|
|
| expect(result).toHaveLength(3); |
| expect(result[0].relevance).toBe(0.9); |
| expect(result[1].relevance).toBe(0.85); |
| expect(result[2].relevance).toBe(0.8); |
| }); |
| }); |
|
|
| describe('enhanceSourcesWithMetadata', () => { |
| const { Files } = require('~/models'); |
| const mockCustomConfig = { |
| fileStrategy: 'local', |
| }; |
|
|
| beforeEach(() => { |
| jest.clearAllMocks(); |
| }); |
|
|
| it('should enhance sources with file metadata from database', async () => { |
| const sources = [ |
| { |
| fileId: 'file_123', |
| fileName: 'example.pdf', |
| relevance: 0.85, |
| type: 'file', |
| }, |
| { |
| fileId: 'file_456', |
| fileName: 'document.txt', |
| relevance: 0.72, |
| type: 'file', |
| }, |
| ]; |
|
|
| Files.find.mockResolvedValue([ |
| { |
| file_id: 'file_123', |
| filename: 'example_from_db.pdf', |
| source: 's3', |
| }, |
| { |
| file_id: 'file_456', |
| filename: 'document_from_db.txt', |
| source: 'local', |
| }, |
| ]); |
|
|
| const result = await enhanceSourcesWithMetadata(sources, mockCustomConfig); |
|
|
| expect(Files.find).toHaveBeenCalledWith({ file_id: { $in: ['file_123', 'file_456'] } }); |
| expect(result).toHaveLength(2); |
|
|
| expect(result[0]).toEqual({ |
| fileId: 'file_123', |
| fileName: 'example_from_db.pdf', |
| relevance: 0.85, |
| type: 'file', |
| metadata: { |
| storageType: 's3', |
| }, |
| }); |
|
|
| expect(result[1]).toEqual({ |
| fileId: 'file_456', |
| fileName: 'document_from_db.txt', |
| relevance: 0.72, |
| type: 'file', |
| metadata: { |
| storageType: 'local', |
| }, |
| }); |
| }); |
|
|
| it('should preserve existing metadata and source data', async () => { |
| const sources = [ |
| { |
| fileId: 'file_123', |
| fileName: 'example.pdf', |
| relevance: 0.85, |
| type: 'file', |
| pages: [1, 2, 3], |
| content: 'Some content', |
| metadata: { |
| existingField: 'value', |
| }, |
| }, |
| ]; |
|
|
| Files.find.mockResolvedValue([ |
| { |
| file_id: 'file_123', |
| filename: 'example_from_db.pdf', |
| source: 'gcs', |
| }, |
| ]); |
|
|
| const result = await enhanceSourcesWithMetadata(sources, mockCustomConfig); |
|
|
| expect(result[0]).toEqual({ |
| fileId: 'file_123', |
| fileName: 'example_from_db.pdf', |
| relevance: 0.85, |
| type: 'file', |
| pages: [1, 2, 3], |
| content: 'Some content', |
| metadata: { |
| existingField: 'value', |
| storageType: 'gcs', |
| }, |
| }); |
| }); |
|
|
| it('should handle missing file metadata gracefully', async () => { |
| const sources = [ |
| { |
| fileId: 'file_789', |
| fileName: 'missing.pdf', |
| relevance: 0.9, |
| type: 'file', |
| }, |
| ]; |
|
|
| Files.find.mockResolvedValue([]); |
|
|
| const result = await enhanceSourcesWithMetadata(sources, mockCustomConfig); |
|
|
| expect(result[0]).toEqual({ |
| fileId: 'file_789', |
| fileName: 'missing.pdf', |
| relevance: 0.9, |
| type: 'file', |
| metadata: { |
| storageType: 'local', |
| }, |
| }); |
| }); |
|
|
| it('should handle database errors gracefully', async () => { |
| const sources = [ |
| { |
| fileId: 'file_123', |
| fileName: 'example.pdf', |
| relevance: 0.85, |
| type: 'file', |
| }, |
| ]; |
|
|
| Files.find.mockRejectedValue(new Error('Database error')); |
|
|
| const result = await enhanceSourcesWithMetadata(sources, mockCustomConfig); |
|
|
| expect(result[0]).toEqual({ |
| fileId: 'file_123', |
| fileName: 'example.pdf', |
| relevance: 0.85, |
| type: 'file', |
| metadata: { |
| storageType: 'local', |
| }, |
| }); |
| }); |
|
|
| it('should deduplicate file IDs when querying database', async () => { |
| const sources = [ |
| { fileId: 'file_123', fileName: 'doc1.pdf', relevance: 0.9, type: 'file' }, |
| { fileId: 'file_123', fileName: 'doc1.pdf', relevance: 0.8, type: 'file' }, |
| { fileId: 'file_456', fileName: 'doc2.pdf', relevance: 0.7, type: 'file' }, |
| ]; |
|
|
| Files.find.mockResolvedValue([ |
| { file_id: 'file_123', filename: 'document1.pdf', source: 's3' }, |
| { file_id: 'file_456', filename: 'document2.pdf', source: 'local' }, |
| ]); |
|
|
| await enhanceSourcesWithMetadata(sources, mockCustomConfig); |
|
|
| expect(Files.find).toHaveBeenCalledWith({ file_id: { $in: ['file_123', 'file_456'] } }); |
| }); |
| }); |
| }); |
|
|