| | const { retrieveAndProcessFile } = require('~/server/services/Files/process'); |
| | const { processMessages } = require('./manage'); |
| |
|
| | jest.mock('~/server/services/Files/process', () => ({ |
| | retrieveAndProcessFile: jest.fn(), |
| | })); |
| |
|
| | describe('processMessages', () => { |
| | let openai, client; |
| |
|
| | beforeEach(() => { |
| | openai = {}; |
| | client = { |
| | processedFileIds: new Set(), |
| | }; |
| | jest.clearAllMocks(); |
| | retrieveAndProcessFile.mockReset(); |
| | }); |
| |
|
| | test('handles normal case with single source', async () => { |
| | const messages = [ |
| | { |
| | content: [ |
| | { |
| | type: 'text', |
| | text: { |
| | value: 'This is a test ^1^ and another^1^', |
| | annotations: [ |
| | { |
| | type: 'file_citation', |
| | start_index: 15, |
| | end_index: 18, |
| | file_citation: { file_id: 'file1' }, |
| | }, |
| | { |
| | type: 'file_citation', |
| | start_index: 30, |
| | end_index: 33, |
| | file_citation: { file_id: 'file1' }, |
| | }, |
| | ], |
| | }, |
| | }, |
| | ], |
| | created_at: 1, |
| | }, |
| | ]; |
| |
|
| | retrieveAndProcessFile.mockResolvedValue({ filename: 'test.txt' }); |
| |
|
| | const result = await processMessages({ openai, client, messages }); |
| |
|
| | expect(result.text).toBe('This is a test ^1^ and another^1^\n\n^1.^ test.txt'); |
| | expect(result.edited).toBe(true); |
| | }); |
| |
|
| | test('handles multiple different sources', async () => { |
| | const messages = [ |
| | { |
| | content: [ |
| | { |
| | type: 'text', |
| | text: { |
| | value: 'This is a test ^1^ and another^2^', |
| | annotations: [ |
| | { |
| | type: 'file_citation', |
| | start_index: 15, |
| | end_index: 18, |
| | file_citation: { file_id: 'file1' }, |
| | }, |
| | { |
| | type: 'file_citation', |
| | start_index: 30, |
| | end_index: 33, |
| | file_citation: { file_id: 'file2' }, |
| | }, |
| | ], |
| | }, |
| | }, |
| | ], |
| | created_at: 1, |
| | }, |
| | ]; |
| |
|
| | retrieveAndProcessFile |
| | .mockResolvedValueOnce({ filename: 'test1.txt' }) |
| | .mockResolvedValueOnce({ filename: 'test2.txt' }); |
| |
|
| | const result = await processMessages({ openai, client, messages }); |
| |
|
| | expect(result.text).toBe('This is a test ^1^ and another^2^\n\n^1.^ test1.txt\n^2.^ test2.txt'); |
| | expect(result.edited).toBe(true); |
| | }); |
| |
|
| | test('handles file retrieval failure', async () => { |
| | const messages = [ |
| | { |
| | content: [ |
| | { |
| | type: 'text', |
| | text: { |
| | value: 'This is a test ^1^', |
| | annotations: [ |
| | { |
| | type: 'file_citation', |
| | start_index: 15, |
| | end_index: 18, |
| | file_citation: { file_id: 'file1' }, |
| | }, |
| | ], |
| | }, |
| | }, |
| | ], |
| | created_at: 1, |
| | }, |
| | ]; |
| |
|
| | retrieveAndProcessFile.mockRejectedValue(new Error('File not found')); |
| |
|
| | const result = await processMessages({ openai, client, messages }); |
| |
|
| | expect(result.text).toBe('This is a test ^1^'); |
| | expect(result.edited).toBe(false); |
| | }); |
| |
|
| | test('handles citations without file ids', async () => { |
| | const messages = [ |
| | { |
| | content: [ |
| | { |
| | type: 'text', |
| | text: { |
| | value: 'This is a test ^1^', |
| | annotations: [{ type: 'file_citation', start_index: 15, end_index: 18 }], |
| | }, |
| | }, |
| | ], |
| | created_at: 1, |
| | }, |
| | ]; |
| |
|
| | const result = await processMessages({ openai, client, messages }); |
| |
|
| | expect(result.text).toBe('This is a test ^1^'); |
| | expect(result.edited).toBe(false); |
| | }); |
| |
|
| | test('handles mixed valid and invalid citations', async () => { |
| | const messages = [ |
| | { |
| | content: [ |
| | { |
| | type: 'text', |
| | text: { |
| | value: 'This is a test ^1^ and ^2^ and ^3^', |
| | annotations: [ |
| | { |
| | type: 'file_citation', |
| | start_index: 15, |
| | end_index: 18, |
| | file_citation: { file_id: 'file1' }, |
| | }, |
| | { type: 'file_citation', start_index: 23, end_index: 26 }, |
| | { |
| | type: 'file_citation', |
| | start_index: 31, |
| | end_index: 34, |
| | file_citation: { file_id: 'file3' }, |
| | }, |
| | ], |
| | }, |
| | }, |
| | ], |
| | created_at: 1, |
| | }, |
| | ]; |
| |
|
| | retrieveAndProcessFile |
| | .mockResolvedValueOnce({ filename: 'test1.txt' }) |
| | .mockResolvedValueOnce({ filename: 'test3.txt' }); |
| |
|
| | const result = await processMessages({ openai, client, messages }); |
| |
|
| | expect(result.text).toBe( |
| | 'This is a test ^1^ and ^2^ and ^2^\n\n^1.^ test1.txt\n^2.^ test3.txt', |
| | ); |
| | expect(result.edited).toBe(true); |
| | }); |
| |
|
| | test('handles adjacent identical citations', async () => { |
| | const messages = [ |
| | { |
| | content: [ |
| | { |
| | type: 'text', |
| | text: { |
| | value: 'This is a test ^1^^1^ and ^1^ ^1^', |
| | annotations: [ |
| | { |
| | type: 'file_citation', |
| | start_index: 15, |
| | end_index: 18, |
| | file_citation: { file_id: 'file1' }, |
| | }, |
| | { |
| | type: 'file_citation', |
| | start_index: 18, |
| | end_index: 21, |
| | file_citation: { file_id: 'file1' }, |
| | }, |
| | { |
| | type: 'file_citation', |
| | start_index: 26, |
| | end_index: 29, |
| | file_citation: { file_id: 'file1' }, |
| | }, |
| | { |
| | type: 'file_citation', |
| | start_index: 30, |
| | end_index: 33, |
| | file_citation: { file_id: 'file1' }, |
| | }, |
| | ], |
| | }, |
| | }, |
| | ], |
| | created_at: 1, |
| | }, |
| | ]; |
| |
|
| | retrieveAndProcessFile.mockResolvedValue({ filename: 'test.txt' }); |
| |
|
| | const result = await processMessages({ openai, client, messages }); |
| |
|
| | expect(result.text).toBe('This is a test ^1^ and ^1^\n\n^1.^ test.txt'); |
| | expect(result.edited).toBe(true); |
| | }); |
| | test('handles real data with multiple adjacent citations', async () => { |
| | const messages = [ |
| | { |
| | id: 'msg_XXXXXXXXXXXXXXXXXXXX', |
| | object: 'thread.message', |
| | created_at: 1722980324, |
| | assistant_id: 'asst_XXXXXXXXXXXXXXXXXXXX', |
| | thread_id: 'thread_XXXXXXXXXXXXXXXXXXXX', |
| | run_id: 'run_XXXXXXXXXXXXXXXXXXXX', |
| | status: 'completed', |
| | incomplete_details: null, |
| | incomplete_at: null, |
| | completed_at: 1722980331, |
| | role: 'assistant', |
| | content: [ |
| | { |
| | type: 'text', |
| | text: { |
| | value: |
| | 'The text you have uploaded is from the book "Harry Potter and the Philosopher\'s Stone" by J.K. Rowling. It follows the story of a young boy named Harry Potter who discovers that he is a wizard on his eleventh birthday. Here are some key points of the narrative:\n\n1. **Discovery and Invitation to Hogwarts**: Harry learns that he is a wizard and receives an invitation to attend Hogwarts School of Witchcraft and Wizardry【11:2†source】【11:4†source】.\n\n2. **Shopping for Supplies**: Hagrid takes Harry to Diagon Alley to buy his school supplies, including his wand from Ollivander\'s【11:9†source】【11:14†source】.\n\n3. **Introduction to Hogwarts**: Harry is introduced to Hogwarts, the magical school where he will learn about magic and discover more about his own background【11:12†source】【11:18†source】.\n\n4. **Meeting Friends and Enemies**: At Hogwarts, Harry makes friends like Ron Weasley and Hermione Granger, and enemies like Draco Malfoy【11:16†source】.\n\n5. **Uncovering the Mystery**: Harry, along with Ron and Hermione, uncovers the mystery of the Philosopher\'s Stone and its connection to the dark wizard Voldemort【11:1†source】【11:10†source】【11:7†source】.\n\nThese points highlight Harry\'s initial experiences in the magical world and set the stage for his adventures at Hogwarts.', |
| | annotations: [ |
| | { |
| | type: 'file_citation', |
| | text: '【11:2†source】', |
| | start_index: 420, |
| | end_index: 433, |
| | file_citation: { |
| | file_id: 'file-XXXXXXXXXXXXXXXXXXXX', |
| | }, |
| | }, |
| | { |
| | type: 'file_citation', |
| | text: '【11:4†source】', |
| | start_index: 433, |
| | end_index: 446, |
| | file_citation: { |
| | file_id: 'file-XXXXXXXXXXXXXXXXXXXX', |
| | }, |
| | }, |
| | { |
| | type: 'file_citation', |
| | text: '【11:9†source】', |
| | start_index: 578, |
| | end_index: 591, |
| | file_citation: { |
| | file_id: 'file-XXXXXXXXXXXXXXXXXXXX', |
| | }, |
| | }, |
| | { |
| | type: 'file_citation', |
| | text: '【11:14†source】', |
| | start_index: 591, |
| | end_index: 605, |
| | file_citation: { |
| | file_id: 'file-XXXXXXXXXXXXXXXXXXXX', |
| | }, |
| | }, |
| | { |
| | type: 'file_citation', |
| | text: '【11:12†source】', |
| | start_index: 767, |
| | end_index: 781, |
| | file_citation: { |
| | file_id: 'file-XXXXXXXXXXXXXXXXXXXX', |
| | }, |
| | }, |
| | { |
| | type: 'file_citation', |
| | text: '【11:18†source】', |
| | start_index: 781, |
| | end_index: 795, |
| | file_citation: { |
| | file_id: 'file-XXXXXXXXXXXXXXXXXXXX', |
| | }, |
| | }, |
| | { |
| | type: 'file_citation', |
| | text: '【11:16†source】', |
| | start_index: 935, |
| | end_index: 949, |
| | file_citation: { |
| | file_id: 'file-XXXXXXXXXXXXXXXXXXXX', |
| | }, |
| | }, |
| | { |
| | type: 'file_citation', |
| | text: '【11:1†source】', |
| | start_index: 1114, |
| | end_index: 1127, |
| | file_citation: { |
| | file_id: 'file-XXXXXXXXXXXXXXXXXXXX', |
| | }, |
| | }, |
| | { |
| | type: 'file_citation', |
| | text: '【11:10†source】', |
| | start_index: 1127, |
| | end_index: 1141, |
| | file_citation: { |
| | file_id: 'file-XXXXXXXXXXXXXXXXXXXX', |
| | }, |
| | }, |
| | { |
| | type: 'file_citation', |
| | text: '【11:7†source】', |
| | start_index: 1141, |
| | end_index: 1154, |
| | file_citation: { |
| | file_id: 'file-XXXXXXXXXXXXXXXXXXXX', |
| | }, |
| | }, |
| | ], |
| | }, |
| | }, |
| | ], |
| | attachments: [], |
| | metadata: {}, |
| | files: [ |
| | { |
| | object: 'file', |
| | id: 'file-XXXXXXXXXXXXXXXXXXXX', |
| | purpose: 'assistants', |
| | filename: 'hp1.txt', |
| | bytes: 439742, |
| | created_at: 1722962139, |
| | status: 'processed', |
| | status_details: null, |
| | type: 'text/plain', |
| | file_id: 'file-XXXXXXXXXXXXXXXXXXXX', |
| | filepath: |
| | 'https://api.openai.com/v1/files/XXXXXXXXXXXXXXXXXXXX/file-XXXXXXXXXXXXXXXXXXXX/hp1.txt', |
| | usage: 1, |
| | user: 'XXXXXXXXXXXXXXXXXXXX', |
| | context: 'assistants', |
| | source: 'openai', |
| | model: 'gpt-4o', |
| | }, |
| | ], |
| | }, |
| | ]; |
| |
|
| | retrieveAndProcessFile.mockResolvedValue({ filename: 'hp1.txt' }); |
| |
|
| | const result = await processMessages({ |
| | openai: {}, |
| | client: { processedFileIds: new Set() }, |
| | messages, |
| | }); |
| |
|
| | const expectedText = `The text you have uploaded is from the book "Harry Potter and the Philosopher's Stone" by J.K. Rowling. It follows the story of a young boy named Harry Potter who discovers that he is a wizard on his eleventh birthday. Here are some key points of the narrative: |
| | |
| | 1. **Discovery and Invitation to Hogwarts**: Harry learns that he is a wizard and receives an invitation to attend Hogwarts School of Witchcraft and Wizardry^1^. |
| | |
| | 2. **Shopping for Supplies**: Hagrid takes Harry to Diagon Alley to buy his school supplies, including his wand from Ollivander's^1^. |
| | |
| | 3. **Introduction to Hogwarts**: Harry is introduced to Hogwarts, the magical school where he will learn about magic and discover more about his own background^1^. |
| | |
| | 4. **Meeting Friends and Enemies**: At Hogwarts, Harry makes friends like Ron Weasley and Hermione Granger, and enemies like Draco Malfoy^1^. |
| | |
| | 5. **Uncovering the Mystery**: Harry, along with Ron and Hermione, uncovers the mystery of the Philosopher's Stone and its connection to the dark wizard Voldemort^1^. |
| | |
| | These points highlight Harry's initial experiences in the magical world and set the stage for his adventures at Hogwarts. |
| | |
| | ^1.^ hp1.txt`; |
| |
|
| | expect(result.text).toBe(expectedText); |
| | expect(result.edited).toBe(true); |
| | }); |
| |
|
| | test('handles real data with multiple adjacent citations with multiple sources', async () => { |
| | const messages = [ |
| | { |
| | id: 'msg_XXXXXXXXXXXXXXXXXXXX', |
| | object: 'thread.message', |
| | created_at: 1722980324, |
| | assistant_id: 'asst_XXXXXXXXXXXXXXXXXXXX', |
| | thread_id: 'thread_XXXXXXXXXXXXXXXXXXXX', |
| | run_id: 'run_XXXXXXXXXXXXXXXXXXXX', |
| | status: 'completed', |
| | incomplete_details: null, |
| | incomplete_at: null, |
| | completed_at: 1722980331, |
| | role: 'assistant', |
| | content: [ |
| | { |
| | type: 'text', |
| | text: { |
| | value: |
| | 'The text you have uploaded is from the book "Harry Potter and the Philosopher\'s Stone" by J.K. Rowling. It follows the story of a young boy named Harry Potter who discovers that he is a wizard on his eleventh birthday. Here are some key points of the narrative:\n\n1. **Discovery and Invitation to Hogwarts**: Harry learns that he is a wizard and receives an invitation to attend Hogwarts School of Witchcraft and Wizardry【11:2†source】【11:4†source】.\n\n2. **Shopping for Supplies**: Hagrid takes Harry to Diagon Alley to buy his school supplies, including his wand from Ollivander\'s【11:9†source】【11:14†source】.\n\n3. **Introduction to Hogwarts**: Harry is introduced to Hogwarts, the magical school where he will learn about magic and discover more about his own background【11:12†source】【11:18†source】.\n\n4. **Meeting Friends and Enemies**: At Hogwarts, Harry makes friends like Ron Weasley and Hermione Granger, and enemies like Draco Malfoy【11:16†source】.\n\n5. **Uncovering the Mystery**: Harry, along with Ron and Hermione, uncovers the mystery of the Philosopher\'s Stone and its connection to the dark wizard Voldemort【11:1†source】【11:10†source】【11:7†source】.\n\nThese points highlight Harry\'s initial experiences in the magical world and set the stage for his adventures at Hogwarts.', |
| | annotations: [ |
| | { |
| | type: 'file_citation', |
| | text: '【11:2†source】', |
| | start_index: 420, |
| | end_index: 433, |
| | file_citation: { |
| | file_id: 'file-XXXXXXXXXXXXXXXXXXXX', |
| | }, |
| | }, |
| | { |
| | type: 'file_citation', |
| | text: '【11:4†source】', |
| | start_index: 433, |
| | end_index: 446, |
| | file_citation: { |
| | file_id: 'file-XXXXXXXXXXXXXXXXXXXX', |
| | }, |
| | }, |
| | { |
| | type: 'file_citation', |
| | text: '【11:9†source】', |
| | start_index: 578, |
| | end_index: 591, |
| | file_citation: { |
| | file_id: 'file-XXXXXXXXXXXXXXXXXXXX', |
| | }, |
| | }, |
| | { |
| | type: 'file_citation', |
| | text: '【11:14†source】', |
| | start_index: 591, |
| | end_index: 605, |
| | file_citation: { |
| | file_id: 'file-XXXXXXXXXXXXXXXXXXXX', |
| | }, |
| | }, |
| | { |
| | type: 'file_citation', |
| | text: '【11:12†source】', |
| | start_index: 767, |
| | end_index: 781, |
| | file_citation: { |
| | file_id: 'file-XXXXXXXXXXXXXXXXXXXX', |
| | }, |
| | }, |
| | { |
| | type: 'file_citation', |
| | text: '【11:18†source】', |
| | start_index: 781, |
| | end_index: 795, |
| | file_citation: { |
| | file_id: 'file-XXXXXXXXXXXXXXXXXXXX', |
| | }, |
| | }, |
| | { |
| | type: 'file_citation', |
| | text: '【11:16†source】', |
| | start_index: 935, |
| | end_index: 949, |
| | file_citation: { |
| | file_id: 'file-XXXXXXXXXXXXXXXXXXXX', |
| | }, |
| | }, |
| | { |
| | type: 'file_citation', |
| | text: '【11:1†source】', |
| | start_index: 1114, |
| | end_index: 1127, |
| | file_citation: { |
| | file_id: 'file-XXXXXXXXXXXXXXXXXXXX', |
| | }, |
| | }, |
| | { |
| | type: 'file_citation', |
| | text: '【11:10†source】', |
| | start_index: 1127, |
| | end_index: 1141, |
| | file_citation: { |
| | file_id: 'file-XXXXXXXXXXXXXXXXXXXX', |
| | }, |
| | }, |
| | { |
| | type: 'file_citation', |
| | text: '【11:7†source】', |
| | start_index: 1141, |
| | end_index: 1154, |
| | file_citation: { |
| | file_id: 'file-XXXXXXXXXXXXXXXXXXXX', |
| | }, |
| | }, |
| | ], |
| | }, |
| | }, |
| | ], |
| | attachments: [], |
| | metadata: {}, |
| | files: [ |
| | { |
| | object: 'file', |
| | id: 'file-XXXXXXXXXXXXXXXXXXXX', |
| | purpose: 'assistants', |
| | filename: 'hp1.txt', |
| | bytes: 439742, |
| | created_at: 1722962139, |
| | status: 'processed', |
| | status_details: null, |
| | type: 'text/plain', |
| | file_id: 'file-XXXXXXXXXXXXXXXXXXXX', |
| | filepath: |
| | 'https://api.openai.com/v1/files/XXXXXXXXXXXXXXXXXXXX/file-XXXXXXXXXXXXXXXXXXXX/hp1.txt', |
| | usage: 1, |
| | user: 'XXXXXXXXXXXXXXXXXXXX', |
| | context: 'assistants', |
| | source: 'openai', |
| | model: 'gpt-4o', |
| | }, |
| | ], |
| | }, |
| | ]; |
| |
|
| | retrieveAndProcessFile.mockResolvedValue({ filename: 'hp1.txt' }); |
| |
|
| | const result = await processMessages({ |
| | openai: {}, |
| | client: { processedFileIds: new Set() }, |
| | messages, |
| | }); |
| |
|
| | const expectedText = `The text you have uploaded is from the book "Harry Potter and the Philosopher's Stone" by J.K. Rowling. It follows the story of a young boy named Harry Potter who discovers that he is a wizard on his eleventh birthday. Here are some key points of the narrative: |
| | |
| | 1. **Discovery and Invitation to Hogwarts**: Harry learns that he is a wizard and receives an invitation to attend Hogwarts School of Witchcraft and Wizardry^1^. |
| | |
| | 2. **Shopping for Supplies**: Hagrid takes Harry to Diagon Alley to buy his school supplies, including his wand from Ollivander's^1^. |
| | |
| | 3. **Introduction to Hogwarts**: Harry is introduced to Hogwarts, the magical school where he will learn about magic and discover more about his own background^1^. |
| | |
| | 4. **Meeting Friends and Enemies**: At Hogwarts, Harry makes friends like Ron Weasley and Hermione Granger, and enemies like Draco Malfoy^1^. |
| | |
| | 5. **Uncovering the Mystery**: Harry, along with Ron and Hermione, uncovers the mystery of the Philosopher's Stone and its connection to the dark wizard Voldemort^1^. |
| | |
| | These points highlight Harry's initial experiences in the magical world and set the stage for his adventures at Hogwarts. |
| | |
| | ^1.^ hp1.txt`; |
| |
|
| | expect(result.text).toBe(expectedText); |
| | expect(result.edited).toBe(true); |
| | }); |
| |
|
| | test('handles edge case with pre-existing citation-like text', async () => { |
| | const messages = [ |
| | { |
| | content: [ |
| | { |
| | type: 'text', |
| | text: { |
| | value: |
| | 'This is a test ^1^ with pre-existing citation-like text. Here\'s a real citation【11:2†source】.', |
| | annotations: [ |
| | { |
| | type: 'file_citation', |
| | text: '【11:2†source】', |
| | start_index: 79, |
| | end_index: 92, |
| | file_citation: { |
| | file_id: 'file-XXXXXXXXXXXXXXXXXXXX', |
| | }, |
| | }, |
| | ], |
| | }, |
| | }, |
| | ], |
| | created_at: 1, |
| | }, |
| | ]; |
| |
|
| | retrieveAndProcessFile.mockResolvedValue({ filename: 'test.txt' }); |
| |
|
| | const result = await processMessages({ |
| | openai: {}, |
| | client: { processedFileIds: new Set() }, |
| | messages, |
| | }); |
| |
|
| | const expectedText = |
| | 'This is a test ^1^ with pre-existing citation-like text. Here\'s a real citation^1^.\n\n^1.^ test.txt'; |
| |
|
| | expect(result.text).toBe(expectedText); |
| | expect(result.edited).toBe(true); |
| | }); |
| |
|
| | test('handles FILE_PATH annotation type', async () => { |
| | const messages = [ |
| | { |
| | content: [ |
| | { |
| | type: 'text', |
| | text: { |
| | value: 'Here is a file path: [file_path]', |
| | annotations: [ |
| | { |
| | type: 'file_path', |
| | text: '[file_path]', |
| | start_index: 21, |
| | end_index: 32, |
| | file_path: { |
| | file_id: 'file-XXXXXXXXXXXXXXXXXXXX', |
| | }, |
| | }, |
| | ], |
| | }, |
| | }, |
| | ], |
| | created_at: 1, |
| | }, |
| | ]; |
| |
|
| | retrieveAndProcessFile.mockResolvedValue({ |
| | filename: 'test.txt', |
| | filepath: '/path/to/test.txt', |
| | }); |
| |
|
| | const result = await processMessages({ |
| | openai: {}, |
| | client: { processedFileIds: new Set() }, |
| | messages, |
| | }); |
| |
|
| | const expectedText = 'Here is a file path: /path/to/test.txt'; |
| |
|
| | expect(result.text).toBe(expectedText); |
| | expect(result.edited).toBe(true); |
| | }); |
| |
|
| | test('handles FILE_CITATION annotation type', async () => { |
| | const messages = [ |
| | { |
| | content: [ |
| | { |
| | type: 'text', |
| | text: { |
| | value: 'Here is a citation: [citation]', |
| | annotations: [ |
| | { |
| | type: 'file_citation', |
| | text: '[citation]', |
| | start_index: 20, |
| | end_index: 30, |
| | file_citation: { |
| | file_id: 'file-XXXXXXXXXXXXXXXXXXXX', |
| | }, |
| | }, |
| | ], |
| | }, |
| | }, |
| | ], |
| | created_at: 1, |
| | }, |
| | ]; |
| |
|
| | retrieveAndProcessFile.mockResolvedValue({ filename: 'test.txt' }); |
| |
|
| | const result = await processMessages({ |
| | openai: {}, |
| | client: { processedFileIds: new Set() }, |
| | messages, |
| | }); |
| |
|
| | const expectedText = 'Here is a citation: ^1^\n\n^1.^ test.txt'; |
| |
|
| | expect(result.text).toBe(expectedText); |
| | expect(result.edited).toBe(true); |
| | }); |
| |
|
| | test('handles multiple annotation types in a single message', async () => { |
| | const messages = [ |
| | { |
| | content: [ |
| | { |
| | type: 'text', |
| | text: { |
| | value: |
| | 'File path: [file_path]. Citation: [citation1]. Another citation: [citation2].', |
| | annotations: [ |
| | { |
| | type: 'file_path', |
| | text: '[file_path]', |
| | start_index: 11, |
| | end_index: 22, |
| | file_path: { |
| | file_id: 'file-XXXXXXXXXXXXXXXX1', |
| | }, |
| | }, |
| | { |
| | type: 'file_citation', |
| | text: '[citation1]', |
| | start_index: 34, |
| | end_index: 45, |
| | file_citation: { |
| | file_id: 'file-XXXXXXXXXXXXXXXX2', |
| | }, |
| | }, |
| | { |
| | type: 'file_citation', |
| | text: '[citation2]', |
| | start_index: 65, |
| | end_index: 76, |
| | file_citation: { |
| | file_id: 'file-XXXXXXXXXXXXXXXX3', |
| | }, |
| | }, |
| | ], |
| | }, |
| | }, |
| | ], |
| | created_at: 1, |
| | }, |
| | ]; |
| |
|
| | retrieveAndProcessFile.mockResolvedValueOnce({ |
| | filename: 'file1.txt', |
| | filepath: '/path/to/file1.txt', |
| | }); |
| | retrieveAndProcessFile.mockResolvedValueOnce({ filename: 'file2.txt' }); |
| | retrieveAndProcessFile.mockResolvedValueOnce({ filename: 'file3.txt' }); |
| |
|
| | const result = await processMessages({ |
| | openai: {}, |
| | client: { processedFileIds: new Set() }, |
| | messages, |
| | }); |
| |
|
| | const expectedText = |
| | 'File path: /path/to/file1.txt. Citation: ^1^. Another citation: ^2^.\n\n^1.^ file2.txt\n^2.^ file3.txt'; |
| |
|
| | expect(result.text).toBe(expectedText); |
| | expect(result.edited).toBe(true); |
| | }); |
| |
|
| | test('handles annotation processing failure', async () => { |
| | const messages = [ |
| | { |
| | content: [ |
| | { |
| | type: 'text', |
| | text: { |
| | value: 'This citation will fail: [citation]', |
| | annotations: [ |
| | { |
| | type: 'file_citation', |
| | text: '[citation]', |
| | start_index: 25, |
| | end_index: 35, |
| | file_citation: { |
| | file_id: 'file-XXXXXXXXXXXXXXXXXXXX', |
| | }, |
| | }, |
| | ], |
| | }, |
| | }, |
| | ], |
| | created_at: 1, |
| | }, |
| | ]; |
| |
|
| | retrieveAndProcessFile.mockRejectedValue(new Error('File not found')); |
| |
|
| | const result = await processMessages({ |
| | openai: {}, |
| | client: { processedFileIds: new Set() }, |
| | messages, |
| | }); |
| |
|
| | const expectedText = 'This citation will fail: [citation]'; |
| |
|
| | expect(result.text).toBe(expectedText); |
| | expect(result.edited).toBe(false); |
| | }); |
| |
|
| | test('handles multiple FILE_PATH annotations with sandbox links', async () => { |
| | const messages = [ |
| | { |
| | id: 'msg_XXXXXXXXXXXXXXXXXXXX', |
| | object: 'thread.message', |
| | created_at: 1722983745, |
| | assistant_id: 'asst_XXXXXXXXXXXXXXXXXXXX', |
| | thread_id: 'thread_XXXXXXXXXXXXXXXXXXXX', |
| | run_id: 'run_XXXXXXXXXXXXXXXXXXXX', |
| | status: 'completed', |
| | incomplete_details: null, |
| | incomplete_at: null, |
| | completed_at: 1722983747, |
| | role: 'assistant', |
| | content: [ |
| | { |
| | type: 'text', |
| | text: { |
| | value: |
| | 'I have generated three dummy CSV files for you. You can download them using the links below:\n\n1. [Download Dummy Data 1](sandbox:/mnt/data/dummy_data1.csv)\n2. [Download Dummy Data 2](sandbox:/mnt/data/dummy_data2.csv)\n3. [Download Dummy Data 3](sandbox:/mnt/data/dummy_data3.csv)', |
| | annotations: [ |
| | { |
| | type: 'file_path', |
| | text: 'sandbox:/mnt/data/dummy_data1.csv', |
| | start_index: 121, |
| | end_index: 154, |
| | file_path: { |
| | file_id: 'file-XXXXXXXXXXXXXXXXXXXX', |
| | }, |
| | }, |
| | { |
| | type: 'file_path', |
| | text: 'sandbox:/mnt/data/dummy_data2.csv', |
| | start_index: 183, |
| | end_index: 216, |
| | file_path: { |
| | file_id: 'file-YYYYYYYYYYYYYYYYYYYY', |
| | }, |
| | }, |
| | { |
| | type: 'file_path', |
| | text: 'sandbox:/mnt/data/dummy_data3.csv', |
| | start_index: 245, |
| | end_index: 278, |
| | file_path: { |
| | file_id: 'file-ZZZZZZZZZZZZZZZZZZZZ', |
| | }, |
| | }, |
| | ], |
| | }, |
| | }, |
| | ], |
| | attachments: [ |
| | { |
| | file_id: 'file-XXXXXXXXXXXXXXXXXXXX', |
| | tools: [ |
| | { |
| | type: 'code_interpreter', |
| | }, |
| | ], |
| | }, |
| | { |
| | file_id: 'file-YYYYYYYYYYYYYYYYYYYY', |
| | tools: [ |
| | { |
| | type: 'code_interpreter', |
| | }, |
| | ], |
| | }, |
| | { |
| | file_id: 'file-ZZZZZZZZZZZZZZZZZZZZ', |
| | tools: [ |
| | { |
| | type: 'code_interpreter', |
| | }, |
| | ], |
| | }, |
| | ], |
| | metadata: {}, |
| | files: [ |
| | { |
| | object: 'file', |
| | id: 'file-XXXXXXXXXXXXXXXXXXXX', |
| | purpose: 'assistants_output', |
| | filename: 'dummy_data1.csv', |
| | bytes: 1925, |
| | created_at: 1722983746, |
| | status: 'processed', |
| | status_details: null, |
| | type: 'text/csv', |
| | file_id: 'file-XXXXXXXXXXXXXXXXXXXX', |
| | filepath: |
| | 'https://api.openai.com/v1/files/XXXXXXXXXXXXXXXXXXXX/file-XXXXXXXXXXXXXXXXXXXX/dummy_data1.csv', |
| | usage: 1, |
| | user: 'XXXXXXXXXXXXXXXXXXXX', |
| | context: 'assistants_output', |
| | source: 'openai', |
| | model: 'gpt-4o-mini', |
| | }, |
| | { |
| | object: 'file', |
| | id: 'file-YYYYYYYYYYYYYYYYYYYY', |
| | purpose: 'assistants_output', |
| | filename: 'dummy_data2.csv', |
| | bytes: 4221, |
| | created_at: 1722983746, |
| | status: 'processed', |
| | status_details: null, |
| | type: 'text/csv', |
| | file_id: 'file-YYYYYYYYYYYYYYYYYYYY', |
| | filepath: |
| | 'https://api.openai.com/v1/files/XXXXXXXXXXXXXXXXXXXX/file-YYYYYYYYYYYYYYYYYYYY/dummy_data2.csv', |
| | usage: 1, |
| | user: 'XXXXXXXXXXXXXXXXXXXX', |
| | context: 'assistants_output', |
| | source: 'openai', |
| | model: 'gpt-4o-mini', |
| | }, |
| | { |
| | object: 'file', |
| | id: 'file-ZZZZZZZZZZZZZZZZZZZZ', |
| | purpose: 'assistants_output', |
| | filename: 'dummy_data3.csv', |
| | bytes: 3534, |
| | created_at: 1722983747, |
| | status: 'processed', |
| | status_details: null, |
| | type: 'text/csv', |
| | file_id: 'file-ZZZZZZZZZZZZZZZZZZZZ', |
| | filepath: |
| | 'https://api.openai.com/v1/files/XXXXXXXXXXXXXXXXXXXX/file-ZZZZZZZZZZZZZZZZZZZZ/dummy_data3.csv', |
| | usage: 1, |
| | user: 'XXXXXXXXXXXXXXXXXXXX', |
| | context: 'assistants_output', |
| | source: 'openai', |
| | model: 'gpt-4o-mini', |
| | }, |
| | ], |
| | }, |
| | ]; |
| |
|
| | const mockClient = { |
| | processedFileIds: new Set(), |
| | }; |
| |
|
| | |
| | retrieveAndProcessFile.mockImplementation(({ file_id }) => { |
| | const fileMap = { |
| | 'file-XXXXXXXXXXXXXXXXXXXX': { |
| | filename: 'dummy_data1.csv', |
| | filepath: |
| | 'https://api.openai.com/v1/files/XXXXXXXXXXXXXXXXXXXX/file-XXXXXXXXXXXXXXXXXXXX/dummy_data1.csv', |
| | }, |
| | 'file-YYYYYYYYYYYYYYYYYYYY': { |
| | filename: 'dummy_data2.csv', |
| | filepath: |
| | 'https://api.openai.com/v1/files/XXXXXXXXXXXXXXXXXXXX/file-YYYYYYYYYYYYYYYYYYYY/dummy_data2.csv', |
| | }, |
| | 'file-ZZZZZZZZZZZZZZZZZZZZ': { |
| | filename: 'dummy_data3.csv', |
| | filepath: |
| | 'https://api.openai.com/v1/files/XXXXXXXXXXXXXXXXXXXX/file-ZZZZZZZZZZZZZZZZZZZZ/dummy_data3.csv', |
| | }, |
| | }; |
| |
|
| | return Promise.resolve(fileMap[file_id]); |
| | }); |
| |
|
| | const result = await processMessages({ openai: {}, client: mockClient, messages }); |
| |
|
| | const expectedText = |
| | 'I have generated three dummy CSV files for you. You can download them using the links below:\n\n1. [Download Dummy Data 1](https://api.openai.com/v1/files/XXXXXXXXXXXXXXXXXXXX/file-XXXXXXXXXXXXXXXXXXXX/dummy_data1.csv)\n2. [Download Dummy Data 2](https://api.openai.com/v1/files/XXXXXXXXXXXXXXXXXXXX/file-YYYYYYYYYYYYYYYYYYYY/dummy_data2.csv)\n3. [Download Dummy Data 3](https://api.openai.com/v1/files/XXXXXXXXXXXXXXXXXXXX/file-ZZZZZZZZZZZZZZZZZZZZ/dummy_data3.csv)'; |
| |
|
| | expect(result.text).toBe(expectedText); |
| | expect(result.edited).toBe(true); |
| | }); |
| | }); |
| |
|