| | const { Tools } = require('librechat-data-provider'); |
| |
|
| | |
| | jest.mock('nanoid', () => ({ |
| | nanoid: jest.fn(() => 'mock-id'), |
| | })); |
| |
|
| | jest.mock('@librechat/api', () => ({ |
| | sendEvent: jest.fn(), |
| | })); |
| |
|
| | jest.mock('@librechat/data-schemas', () => ({ |
| | logger: { |
| | error: jest.fn(), |
| | }, |
| | })); |
| |
|
| | jest.mock('@librechat/agents', () => ({ |
| | EnvVar: { CODE_API_KEY: 'CODE_API_KEY' }, |
| | Providers: { GOOGLE: 'google' }, |
| | GraphEvents: {}, |
| | getMessageId: jest.fn(), |
| | ToolEndHandler: jest.fn(), |
| | handleToolCalls: jest.fn(), |
| | ChatModelStreamHandler: jest.fn(), |
| | })); |
| |
|
| | jest.mock('~/server/services/Files/Citations', () => ({ |
| | processFileCitations: jest.fn(), |
| | })); |
| |
|
| | jest.mock('~/server/services/Files/Code/process', () => ({ |
| | processCodeOutput: jest.fn(), |
| | })); |
| |
|
| | jest.mock('~/server/services/Tools/credentials', () => ({ |
| | loadAuthValues: jest.fn(), |
| | })); |
| |
|
| | jest.mock('~/server/services/Files/process', () => ({ |
| | saveBase64Image: jest.fn(), |
| | })); |
| |
|
| | describe('createToolEndCallback', () => { |
| | let req, res, artifactPromises, createToolEndCallback; |
| | let logger; |
| |
|
| | beforeEach(() => { |
| | jest.clearAllMocks(); |
| |
|
| | |
| | logger = require('@librechat/data-schemas').logger; |
| |
|
| | |
| | const callbacks = require('../callbacks'); |
| | createToolEndCallback = callbacks.createToolEndCallback; |
| |
|
| | req = { |
| | user: { id: 'user123' }, |
| | }; |
| | res = { |
| | headersSent: false, |
| | write: jest.fn(), |
| | }; |
| | artifactPromises = []; |
| | }); |
| |
|
| | describe('ui_resources artifact handling', () => { |
| | it('should process ui_resources artifact and return attachment when headers not sent', async () => { |
| | const toolEndCallback = createToolEndCallback({ req, res, artifactPromises }); |
| |
|
| | const output = { |
| | tool_call_id: 'tool123', |
| | artifact: { |
| | [Tools.ui_resources]: { |
| | data: { |
| | 0: { type: 'button', label: 'Click me' }, |
| | 1: { type: 'input', placeholder: 'Enter text' }, |
| | }, |
| | }, |
| | }, |
| | }; |
| |
|
| | const metadata = { |
| | run_id: 'run456', |
| | thread_id: 'thread789', |
| | }; |
| |
|
| | await toolEndCallback({ output }, metadata); |
| |
|
| | |
| | const results = await Promise.all(artifactPromises); |
| |
|
| | |
| | expect(res.write).not.toHaveBeenCalled(); |
| |
|
| | const attachment = results[0]; |
| | expect(attachment).toEqual({ |
| | type: Tools.ui_resources, |
| | messageId: 'run456', |
| | toolCallId: 'tool123', |
| | conversationId: 'thread789', |
| | [Tools.ui_resources]: { |
| | 0: { type: 'button', label: 'Click me' }, |
| | 1: { type: 'input', placeholder: 'Enter text' }, |
| | }, |
| | }); |
| | }); |
| |
|
| | it('should write to response when headers are already sent', async () => { |
| | res.headersSent = true; |
| | const toolEndCallback = createToolEndCallback({ req, res, artifactPromises }); |
| |
|
| | const output = { |
| | tool_call_id: 'tool123', |
| | artifact: { |
| | [Tools.ui_resources]: { |
| | data: { |
| | 0: { type: 'carousel', items: [] }, |
| | }, |
| | }, |
| | }, |
| | }; |
| |
|
| | const metadata = { |
| | run_id: 'run456', |
| | thread_id: 'thread789', |
| | }; |
| |
|
| | await toolEndCallback({ output }, metadata); |
| | const results = await Promise.all(artifactPromises); |
| |
|
| | expect(res.write).toHaveBeenCalled(); |
| | expect(results[0]).toEqual({ |
| | type: Tools.ui_resources, |
| | messageId: 'run456', |
| | toolCallId: 'tool123', |
| | conversationId: 'thread789', |
| | [Tools.ui_resources]: { |
| | 0: { type: 'carousel', items: [] }, |
| | }, |
| | }); |
| | }); |
| |
|
| | it('should handle errors when processing ui_resources', async () => { |
| | const toolEndCallback = createToolEndCallback({ req, res, artifactPromises }); |
| |
|
| | |
| | res.headersSent = true; |
| | res.write.mockImplementation(() => { |
| | throw new Error('Write failed'); |
| | }); |
| |
|
| | const output = { |
| | tool_call_id: 'tool123', |
| | artifact: { |
| | [Tools.ui_resources]: { |
| | data: { |
| | 0: { type: 'test' }, |
| | }, |
| | }, |
| | }, |
| | }; |
| |
|
| | const metadata = { |
| | run_id: 'run456', |
| | thread_id: 'thread789', |
| | }; |
| |
|
| | await toolEndCallback({ output }, metadata); |
| | const results = await Promise.all(artifactPromises); |
| |
|
| | expect(logger.error).toHaveBeenCalledWith( |
| | 'Error processing artifact content:', |
| | expect.any(Error), |
| | ); |
| | expect(results[0]).toBeNull(); |
| | }); |
| |
|
| | it('should handle multiple artifacts including ui_resources', async () => { |
| | const toolEndCallback = createToolEndCallback({ req, res, artifactPromises }); |
| |
|
| | const output = { |
| | tool_call_id: 'tool123', |
| | artifact: { |
| | [Tools.ui_resources]: { |
| | data: { |
| | 0: { type: 'chart', data: [] }, |
| | }, |
| | }, |
| | [Tools.web_search]: { |
| | results: ['result1', 'result2'], |
| | }, |
| | }, |
| | }; |
| |
|
| | const metadata = { |
| | run_id: 'run456', |
| | thread_id: 'thread789', |
| | }; |
| |
|
| | await toolEndCallback({ output }, metadata); |
| | const results = await Promise.all(artifactPromises); |
| |
|
| | |
| | expect(artifactPromises).toHaveLength(2); |
| | expect(results).toHaveLength(2); |
| |
|
| | |
| | const uiResourceAttachment = results.find((r) => r?.type === Tools.ui_resources); |
| | expect(uiResourceAttachment).toBeTruthy(); |
| | expect(uiResourceAttachment[Tools.ui_resources]).toEqual({ |
| | 0: { type: 'chart', data: [] }, |
| | }); |
| |
|
| | |
| | const webSearchAttachment = results.find((r) => r?.type === Tools.web_search); |
| | expect(webSearchAttachment).toBeTruthy(); |
| | expect(webSearchAttachment[Tools.web_search]).toEqual({ |
| | results: ['result1', 'result2'], |
| | }); |
| | }); |
| |
|
| | it('should not process artifacts when output has no artifacts', async () => { |
| | const toolEndCallback = createToolEndCallback({ req, res, artifactPromises }); |
| |
|
| | const output = { |
| | tool_call_id: 'tool123', |
| | content: 'Some regular content', |
| | |
| | }; |
| |
|
| | const metadata = { |
| | run_id: 'run456', |
| | thread_id: 'thread789', |
| | }; |
| |
|
| | await toolEndCallback({ output }, metadata); |
| |
|
| | expect(artifactPromises).toHaveLength(0); |
| | expect(res.write).not.toHaveBeenCalled(); |
| | }); |
| | }); |
| |
|
| | describe('edge cases', () => { |
| | it('should handle empty ui_resources data object', async () => { |
| | const toolEndCallback = createToolEndCallback({ req, res, artifactPromises }); |
| |
|
| | const output = { |
| | tool_call_id: 'tool123', |
| | artifact: { |
| | [Tools.ui_resources]: { |
| | data: {}, |
| | }, |
| | }, |
| | }; |
| |
|
| | const metadata = { |
| | run_id: 'run456', |
| | thread_id: 'thread789', |
| | }; |
| |
|
| | await toolEndCallback({ output }, metadata); |
| | const results = await Promise.all(artifactPromises); |
| |
|
| | expect(results[0]).toEqual({ |
| | type: Tools.ui_resources, |
| | messageId: 'run456', |
| | toolCallId: 'tool123', |
| | conversationId: 'thread789', |
| | [Tools.ui_resources]: {}, |
| | }); |
| | }); |
| |
|
| | it('should handle ui_resources with complex nested data', async () => { |
| | const toolEndCallback = createToolEndCallback({ req, res, artifactPromises }); |
| |
|
| | const complexData = { |
| | 0: { |
| | type: 'form', |
| | fields: [ |
| | { name: 'field1', type: 'text', required: true }, |
| | { name: 'field2', type: 'select', options: ['a', 'b', 'c'] }, |
| | ], |
| | nested: { |
| | deep: { |
| | value: 123, |
| | array: [1, 2, 3], |
| | }, |
| | }, |
| | }, |
| | }; |
| |
|
| | const output = { |
| | tool_call_id: 'tool123', |
| | artifact: { |
| | [Tools.ui_resources]: { |
| | data: complexData, |
| | }, |
| | }, |
| | }; |
| |
|
| | const metadata = { |
| | run_id: 'run456', |
| | thread_id: 'thread789', |
| | }; |
| |
|
| | await toolEndCallback({ output }, metadata); |
| | const results = await Promise.all(artifactPromises); |
| |
|
| | expect(results[0][Tools.ui_resources]).toEqual(complexData); |
| | }); |
| |
|
| | it('should handle when output is undefined', async () => { |
| | const toolEndCallback = createToolEndCallback({ req, res, artifactPromises }); |
| |
|
| | const metadata = { |
| | run_id: 'run456', |
| | thread_id: 'thread789', |
| | }; |
| |
|
| | await toolEndCallback({ output: undefined }, metadata); |
| |
|
| | expect(artifactPromises).toHaveLength(0); |
| | expect(res.write).not.toHaveBeenCalled(); |
| | }); |
| |
|
| | it('should handle when data parameter is undefined', async () => { |
| | const toolEndCallback = createToolEndCallback({ req, res, artifactPromises }); |
| |
|
| | const metadata = { |
| | run_id: 'run456', |
| | thread_id: 'thread789', |
| | }; |
| |
|
| | await toolEndCallback(undefined, metadata); |
| |
|
| | expect(artifactPromises).toHaveLength(0); |
| | expect(res.write).not.toHaveBeenCalled(); |
| | }); |
| | }); |
| | }); |
| |
|