const crypto = require('crypto'); const { Constants } = require('librechat-data-provider'); const { HumanMessage, AIMessage } = require('@langchain/core/messages'); const PluginsClient = require('../PluginsClient'); jest.mock('~/lib/db/connectDb'); jest.mock('~/models/Conversation', () => { return function () { return { save: jest.fn(), deleteConvos: jest.fn(), }; }; }); const defaultAzureOptions = { azureOpenAIApiInstanceName: 'your-instance-name', azureOpenAIApiDeploymentName: 'your-deployment-name', azureOpenAIApiVersion: '2020-07-01-preview', }; describe('PluginsClient', () => { let TestAgent; let options = { tools: [], modelOptions: { model: 'gpt-3.5-turbo', temperature: 0, max_tokens: 2, }, agentOptions: { model: 'gpt-3.5-turbo', }, }; let parentMessageId; let conversationId; const fakeMessages = []; const userMessage = 'Hello, ChatGPT!'; const apiKey = 'fake-api-key'; beforeEach(() => { TestAgent = new PluginsClient(apiKey, options); TestAgent.loadHistory = jest .fn() .mockImplementation((conversationId, parentMessageId = null) => { if (!conversationId) { TestAgent.currentMessages = []; return Promise.resolve([]); } const orderedMessages = TestAgent.constructor.getMessagesForConversation({ messages: fakeMessages, parentMessageId, }); const chatMessages = orderedMessages.map((msg) => msg?.isCreatedByUser || msg?.role?.toLowerCase() === 'user' ? new HumanMessage(msg.text) : new AIMessage(msg.text), ); TestAgent.currentMessages = orderedMessages; return Promise.resolve(chatMessages); }); TestAgent.sendMessage = jest.fn().mockImplementation(async (message, opts = {}) => { if (opts && typeof opts === 'object') { TestAgent.setOptions(opts); } const conversationId = opts.conversationId || crypto.randomUUID(); const parentMessageId = opts.parentMessageId || Constants.NO_PARENT; const userMessageId = opts.overrideParentMessageId || crypto.randomUUID(); this.pastMessages = await TestAgent.loadHistory( conversationId, TestAgent.options?.parentMessageId, ); const userMessage = { text: message, sender: 'ChatGPT', isCreatedByUser: true, messageId: userMessageId, parentMessageId, conversationId, }; const response = { sender: 'ChatGPT', text: 'Hello, User!', isCreatedByUser: false, messageId: crypto.randomUUID(), parentMessageId: userMessage.messageId, conversationId, }; fakeMessages.push(userMessage); fakeMessages.push(response); return response; }); }); test('initializes PluginsClient without crashing', () => { expect(TestAgent).toBeInstanceOf(PluginsClient); }); test('check setOptions function', () => { expect(TestAgent.agentIsGpt3).toBe(true); }); describe('sendMessage', () => { test('sendMessage should return a response message', async () => { const expectedResult = expect.objectContaining({ sender: 'ChatGPT', text: expect.any(String), isCreatedByUser: false, messageId: expect.any(String), parentMessageId: expect.any(String), conversationId: expect.any(String), }); const response = await TestAgent.sendMessage(userMessage); parentMessageId = response.messageId; conversationId = response.conversationId; expect(response).toEqual(expectedResult); }); test('sendMessage should work with provided conversationId and parentMessageId', async () => { const userMessage = 'Second message in the conversation'; const opts = { conversationId, parentMessageId, }; const expectedResult = expect.objectContaining({ sender: 'ChatGPT', text: expect.any(String), isCreatedByUser: false, messageId: expect.any(String), parentMessageId: expect.any(String), conversationId: opts.conversationId, }); const response = await TestAgent.sendMessage(userMessage, opts); parentMessageId = response.messageId; expect(response.conversationId).toEqual(conversationId); expect(response).toEqual(expectedResult); }); test('should return chat history', async () => { const chatMessages = await TestAgent.loadHistory(conversationId, parentMessageId); expect(TestAgent.currentMessages).toHaveLength(4); expect(chatMessages[0].text).toEqual(userMessage); }); }); describe('getFunctionModelName', () => { let client; beforeEach(() => { client = new PluginsClient('dummy_api_key'); }); test('should return the input when it includes a dash followed by four digits', () => { expect(client.getFunctionModelName('-1234')).toBe('-1234'); expect(client.getFunctionModelName('gpt-4-5678-preview')).toBe('gpt-4-5678-preview'); }); test('should return the input for all function-capable models (`0613` models and above)', () => { expect(client.getFunctionModelName('gpt-4-0613')).toBe('gpt-4-0613'); expect(client.getFunctionModelName('gpt-4-32k-0613')).toBe('gpt-4-32k-0613'); expect(client.getFunctionModelName('gpt-3.5-turbo-0613')).toBe('gpt-3.5-turbo-0613'); expect(client.getFunctionModelName('gpt-3.5-turbo-16k-0613')).toBe('gpt-3.5-turbo-16k-0613'); expect(client.getFunctionModelName('gpt-3.5-turbo-1106')).toBe('gpt-3.5-turbo-1106'); expect(client.getFunctionModelName('gpt-4-1106-preview')).toBe('gpt-4-1106-preview'); expect(client.getFunctionModelName('gpt-4-1106')).toBe('gpt-4-1106'); }); test('should return the corresponding model if input is non-function capable (`0314` models)', () => { expect(client.getFunctionModelName('gpt-4-0314')).toBe('gpt-4'); expect(client.getFunctionModelName('gpt-4-32k-0314')).toBe('gpt-4'); expect(client.getFunctionModelName('gpt-3.5-turbo-0314')).toBe('gpt-3.5-turbo'); expect(client.getFunctionModelName('gpt-3.5-turbo-16k-0314')).toBe('gpt-3.5-turbo'); }); test('should return "gpt-3.5-turbo" when the input includes "gpt-3.5-turbo"', () => { expect(client.getFunctionModelName('test gpt-3.5-turbo model')).toBe('gpt-3.5-turbo'); }); test('should return "gpt-4" when the input includes "gpt-4"', () => { expect(client.getFunctionModelName('testing gpt-4')).toBe('gpt-4'); }); test('should return "gpt-3.5-turbo" for input that does not meet any specific condition', () => { expect(client.getFunctionModelName('random string')).toBe('gpt-3.5-turbo'); expect(client.getFunctionModelName('')).toBe('gpt-3.5-turbo'); }); }); describe('Azure OpenAI tests specific to Plugins', () => { // TODO: add more tests for Azure OpenAI integration with Plugins // let client; // beforeEach(() => { // client = new PluginsClient('dummy_api_key'); // }); test('should not call getFunctionModelName when azure options are set', () => { const spy = jest.spyOn(PluginsClient.prototype, 'getFunctionModelName'); const model = 'gpt-4-turbo'; // note, without the azure change in PR #1766, `getFunctionModelName` is called twice const testClient = new PluginsClient('dummy_api_key', { agentOptions: { model, agent: 'functions', }, azure: defaultAzureOptions, }); expect(spy).not.toHaveBeenCalled(); expect(testClient.agentOptions.model).toBe(model); spy.mockRestore(); }); }); describe('sendMessage with filtered tools', () => { let TestAgent; const apiKey = 'fake-api-key'; const mockTools = [{ name: 'tool1' }, { name: 'tool2' }, { name: 'tool3' }, { name: 'tool4' }]; beforeEach(() => { TestAgent = new PluginsClient(apiKey, { tools: mockTools, modelOptions: { model: 'gpt-3.5-turbo', temperature: 0, max_tokens: 2, }, agentOptions: { model: 'gpt-3.5-turbo', }, }); TestAgent.options.req = { app: { locals: {}, }, }; TestAgent.sendMessage = jest.fn().mockImplementation(async () => { const { filteredTools = [], includedTools = [] } = TestAgent.options.req.app.locals; if (includedTools.length > 0) { const tools = TestAgent.options.tools.filter((plugin) => includedTools.includes(plugin.name), ); TestAgent.options.tools = tools; } else { const tools = TestAgent.options.tools.filter( (plugin) => !filteredTools.includes(plugin.name), ); TestAgent.options.tools = tools; } return { text: 'Mocked response', tools: TestAgent.options.tools, }; }); }); test('should filter out tools when filteredTools is provided', async () => { TestAgent.options.req.app.locals.filteredTools = ['tool1', 'tool3']; const response = await TestAgent.sendMessage('Test message'); expect(response.tools).toHaveLength(2); expect(response.tools).toEqual( expect.arrayContaining([ expect.objectContaining({ name: 'tool2' }), expect.objectContaining({ name: 'tool4' }), ]), ); }); test('should only include specified tools when includedTools is provided', async () => { TestAgent.options.req.app.locals.includedTools = ['tool2', 'tool4']; const response = await TestAgent.sendMessage('Test message'); expect(response.tools).toHaveLength(2); expect(response.tools).toEqual( expect.arrayContaining([ expect.objectContaining({ name: 'tool2' }), expect.objectContaining({ name: 'tool4' }), ]), ); }); test('should prioritize includedTools over filteredTools', async () => { TestAgent.options.req.app.locals.filteredTools = ['tool1', 'tool3']; TestAgent.options.req.app.locals.includedTools = ['tool1', 'tool2']; const response = await TestAgent.sendMessage('Test message'); expect(response.tools).toHaveLength(2); expect(response.tools).toEqual( expect.arrayContaining([ expect.objectContaining({ name: 'tool1' }), expect.objectContaining({ name: 'tool2' }), ]), ); }); test('should not modify tools when no filters are provided', async () => { const response = await TestAgent.sendMessage('Test message'); expect(response.tools).toHaveLength(4); expect(response.tools).toEqual(expect.arrayContaining(mockTools)); }); }); });