mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 17:00:15 +01:00
* refactor: pass model in message edit payload, use encoder in standalone util function * feat: add summaryBuffer helper * refactor(api/messages): use new countTokens helper and add auth middleware at top * wip: ConversationSummaryBufferMemory * refactor: move pre-generation helpers to prompts dir * chore: remove console log * chore: remove test as payload will no longer carry tokenCount * chore: update getMessagesWithinTokenLimit JSDoc * refactor: optimize getMessagesForConversation and also break on summary, feat(ci): getMessagesForConversation tests * refactor(getMessagesForConvo): count '00000000-0000-0000-0000-000000000000' as root message * chore: add newer model to token map * fix: condition was point to prop of array instead of message prop * refactor(BaseClient): use object for refineMessages param, rename 'summary' to 'summaryMessage', add previous_summary refactor(getMessagesWithinTokenLimit): replace text and tokenCount if should summarize, summary, and summaryTokenCount are present fix/refactor(handleContextStrategy): use the right comparison length for context diff, and replace payload first message when a summary is present * chore: log previous_summary if debugging * refactor(formatMessage): assume if role is defined that it's a valid value * refactor(getMessagesWithinTokenLimit): remove summary logic refactor(handleContextStrategy): add usePrevSummary logic in case only summary was pruned refactor(loadHistory): initial message query will return all ordered messages but keep track of the latest summary refactor(getMessagesForConversation): use object for single param, edit jsdoc, edit all files using the method refactor(ChatGPTClient): order messages before buildPrompt is called, TODO: add convoSumBuffMemory logic * fix: undefined handling and summarizing only when shouldRefineContext is true * chore(BaseClient): fix test results omitting system role for summaries and test edge case * chore: export summaryBuffer from index file * refactor(OpenAIClient/BaseClient): move refineMessages to subclass, implement LLM initialization for summaryBuffer * feat: add OPENAI_SUMMARIZE to enable summarizing, refactor: rename client prop 'shouldRefineContext' to 'shouldSummarize', change contextStrategy value to 'summarize' from 'refine' * refactor: rename refineMessages method to summarizeMessages for clarity * chore: clarify summary future intent in .env.example * refactor(initializeLLM): handle case for either 'model' or 'modelName' being passed * feat(gptPlugins): enable summarization for plugins * refactor(gptPlugins): utilize new initializeLLM method and formatting methods for messages, use payload array for currentMessages and assign pastMessages sooner * refactor(agents): use ConversationSummaryBufferMemory for both agent types * refactor(formatMessage): optimize original method for langchain, add helper function for langchain messages, add JSDocs and tests * refactor(summaryBuffer): add helper to createSummaryBufferMemory, and use new formatting helpers * fix: forgot to spread formatMessages also took opportunity to pluralize filename * refactor: pass memory to tools, namely openapi specs. not used and may never be used by new method but added for testing * ci(formatMessages): add more exhaustive checks for langchain messages * feat: add debug env var for OpenAI * chore: delete unnecessary comments * chore: add extra note about summary feature * fix: remove tokenCount from payload instructions * fix: test fail * fix: only pass instructions to payload when defined or not empty object * refactor: fromPromptMessages is deprecated, use renamed method fromMessages * refactor: use 'includes' instead of 'startsWith' for extended OpenRouter compatibility * fix(PluginsClient.buildPromptBody): handle undefined message strings * chore: log langchain titling error * feat: getModelMaxTokens helper * feat: tokenSplit helper * feat: summary prompts updated * fix: optimize _CUT_OFF_SUMMARIZER prompt * refactor(summaryBuffer): use custom summary prompt, allow prompt to be passed, pass humanPrefix and aiPrefix to memory, along with any future variables, rename messagesToRefine to context * fix(summaryBuffer): handle edge case where messagesToRefine exceeds summary context, refactor(BaseClient): allow custom maxContextTokens to be passed to getMessagesWithinTokenLimit, add defined check before unshifting summaryMessage, update shouldSummarize based on this refactor(OpenAIClient): use getModelMaxTokens, use cut-off message method for summary if no messages were left after pruning * fix(handleContextStrategy): handle case where incoming prompt is bigger than model context * chore: rename refinedContent to splitText * chore: remove unnecessary debug log
264 lines
8.9 KiB
JavaScript
264 lines
8.9 KiB
JavaScript
const OpenAIClient = require('../OpenAIClient');
|
|
|
|
jest.mock('meilisearch');
|
|
|
|
describe('OpenAIClient', () => {
|
|
let client, client2;
|
|
const model = 'gpt-4';
|
|
const parentMessageId = '1';
|
|
const messages = [
|
|
{ role: 'user', sender: 'User', text: 'Hello', messageId: parentMessageId },
|
|
{ role: 'assistant', sender: 'Assistant', text: 'Hi', messageId: '2' },
|
|
];
|
|
|
|
beforeEach(() => {
|
|
const options = {
|
|
// debug: true,
|
|
openaiApiKey: 'new-api-key',
|
|
modelOptions: {
|
|
model,
|
|
temperature: 0.7,
|
|
},
|
|
};
|
|
client = new OpenAIClient('test-api-key', options);
|
|
client2 = new OpenAIClient('test-api-key', options);
|
|
client.summarizeMessages = jest.fn().mockResolvedValue({
|
|
role: 'assistant',
|
|
content: 'Refined answer',
|
|
tokenCount: 30,
|
|
});
|
|
client.buildPrompt = jest
|
|
.fn()
|
|
.mockResolvedValue({ prompt: messages.map((m) => m.text).join('\n') });
|
|
client.constructor.freeAndResetAllEncoders();
|
|
});
|
|
|
|
describe('setOptions', () => {
|
|
it('should set the options correctly', () => {
|
|
expect(client.apiKey).toBe('new-api-key');
|
|
expect(client.modelOptions.model).toBe(model);
|
|
expect(client.modelOptions.temperature).toBe(0.7);
|
|
});
|
|
});
|
|
|
|
describe('selectTokenizer', () => {
|
|
it('should get the correct tokenizer based on the instance state', () => {
|
|
const tokenizer = client.selectTokenizer();
|
|
expect(tokenizer).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe('freeAllTokenizers', () => {
|
|
it('should free all tokenizers', () => {
|
|
// Create a tokenizer
|
|
const tokenizer = client.selectTokenizer();
|
|
|
|
// Mock 'free' method on the tokenizer
|
|
tokenizer.free = jest.fn();
|
|
|
|
client.constructor.freeAndResetAllEncoders();
|
|
|
|
// Check if 'free' method has been called on the tokenizer
|
|
expect(tokenizer.free).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('getTokenCount', () => {
|
|
it('should return the correct token count', () => {
|
|
const count = client.getTokenCount('Hello, world!');
|
|
expect(count).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should reset the encoder and count when count reaches 25', () => {
|
|
const freeAndResetEncoderSpy = jest.spyOn(client.constructor, 'freeAndResetAllEncoders');
|
|
|
|
// Call getTokenCount 25 times
|
|
for (let i = 0; i < 25; i++) {
|
|
client.getTokenCount('test text');
|
|
}
|
|
|
|
expect(freeAndResetEncoderSpy).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should not reset the encoder and count when count is less than 25', () => {
|
|
const freeAndResetEncoderSpy = jest.spyOn(client.constructor, 'freeAndResetAllEncoders');
|
|
freeAndResetEncoderSpy.mockClear();
|
|
|
|
// Call getTokenCount 24 times
|
|
for (let i = 0; i < 24; i++) {
|
|
client.getTokenCount('test text');
|
|
}
|
|
|
|
expect(freeAndResetEncoderSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle errors and reset the encoder', () => {
|
|
const freeAndResetEncoderSpy = jest.spyOn(client.constructor, 'freeAndResetAllEncoders');
|
|
|
|
// Mock encode function to throw an error
|
|
client.selectTokenizer().encode = jest.fn().mockImplementation(() => {
|
|
throw new Error('Test error');
|
|
});
|
|
|
|
client.getTokenCount('test text');
|
|
|
|
expect(freeAndResetEncoderSpy).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should not throw null pointer error when freeing the same encoder twice', () => {
|
|
client.constructor.freeAndResetAllEncoders();
|
|
client2.constructor.freeAndResetAllEncoders();
|
|
|
|
const count = client2.getTokenCount('test text');
|
|
expect(count).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
describe('getSaveOptions', () => {
|
|
it('should return the correct save options', () => {
|
|
const options = client.getSaveOptions();
|
|
expect(options).toHaveProperty('chatGptLabel');
|
|
expect(options).toHaveProperty('promptPrefix');
|
|
});
|
|
});
|
|
|
|
describe('getBuildMessagesOptions', () => {
|
|
it('should return the correct build messages options', () => {
|
|
const options = client.getBuildMessagesOptions({ promptPrefix: 'Hello' });
|
|
expect(options).toHaveProperty('isChatCompletion');
|
|
expect(options).toHaveProperty('promptPrefix');
|
|
expect(options.promptPrefix).toBe('Hello');
|
|
});
|
|
});
|
|
|
|
describe('buildMessages', () => {
|
|
it('should build messages correctly for chat completion', async () => {
|
|
const result = await client.buildMessages(messages, parentMessageId, {
|
|
isChatCompletion: true,
|
|
});
|
|
expect(result).toHaveProperty('prompt');
|
|
});
|
|
|
|
it('should build messages correctly for non-chat completion', async () => {
|
|
const result = await client.buildMessages(messages, parentMessageId, {
|
|
isChatCompletion: false,
|
|
});
|
|
expect(result).toHaveProperty('prompt');
|
|
});
|
|
|
|
it('should build messages correctly with a promptPrefix', async () => {
|
|
const result = await client.buildMessages(messages, parentMessageId, {
|
|
isChatCompletion: true,
|
|
promptPrefix: 'Test Prefix',
|
|
});
|
|
expect(result).toHaveProperty('prompt');
|
|
const instructions = result.prompt.find((item) => item.name === 'instructions');
|
|
expect(instructions).toBeDefined();
|
|
expect(instructions.content).toContain('Test Prefix');
|
|
});
|
|
|
|
it('should handle context strategy correctly', async () => {
|
|
client.contextStrategy = 'summarize';
|
|
const result = await client.buildMessages(messages, parentMessageId, {
|
|
isChatCompletion: true,
|
|
});
|
|
expect(result).toHaveProperty('prompt');
|
|
expect(result).toHaveProperty('tokenCountMap');
|
|
});
|
|
|
|
it('should assign name property for user messages when options.name is set', async () => {
|
|
client.options.name = 'Test User';
|
|
const result = await client.buildMessages(messages, parentMessageId, {
|
|
isChatCompletion: true,
|
|
});
|
|
const hasUserWithName = result.prompt.some(
|
|
(item) => item.role === 'user' && item.name === 'Test User',
|
|
);
|
|
expect(hasUserWithName).toBe(true);
|
|
});
|
|
|
|
it('should handle promptPrefix from options when promptPrefix argument is not provided', async () => {
|
|
client.options.promptPrefix = 'Test Prefix from options';
|
|
const result = await client.buildMessages(messages, parentMessageId, {
|
|
isChatCompletion: true,
|
|
});
|
|
const instructions = result.prompt.find((item) => item.name === 'instructions');
|
|
expect(instructions.content).toContain('Test Prefix from options');
|
|
});
|
|
|
|
it('should handle case when neither promptPrefix argument nor options.promptPrefix is set', async () => {
|
|
const result = await client.buildMessages(messages, parentMessageId, {
|
|
isChatCompletion: true,
|
|
});
|
|
const instructions = result.prompt.find((item) => item.name === 'instructions');
|
|
expect(instructions).toBeUndefined();
|
|
});
|
|
|
|
it('should handle case when getMessagesForConversation returns null or an empty array', async () => {
|
|
const messages = [];
|
|
const result = await client.buildMessages(messages, parentMessageId, {
|
|
isChatCompletion: true,
|
|
});
|
|
expect(result.prompt).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('getTokenCountForMessage', () => {
|
|
const example_messages = [
|
|
{
|
|
role: 'system',
|
|
content:
|
|
'You are a helpful, pattern-following assistant that translates corporate jargon into plain English.',
|
|
},
|
|
{
|
|
role: 'system',
|
|
name: 'example_user',
|
|
content: 'New synergies will help drive top-line growth.',
|
|
},
|
|
{
|
|
role: 'system',
|
|
name: 'example_assistant',
|
|
content: 'Things working well together will increase revenue.',
|
|
},
|
|
{
|
|
role: 'system',
|
|
name: 'example_user',
|
|
content:
|
|
'Let\'s circle back when we have more bandwidth to touch base on opportunities for increased leverage.',
|
|
},
|
|
{
|
|
role: 'system',
|
|
name: 'example_assistant',
|
|
content: 'Let\'s talk later when we\'re less busy about how to do better.',
|
|
},
|
|
{
|
|
role: 'user',
|
|
content:
|
|
'This late pivot means we don\'t have time to boil the ocean for the client deliverable.',
|
|
},
|
|
];
|
|
|
|
const testCases = [
|
|
{ model: 'gpt-3.5-turbo-0301', expected: 127 },
|
|
{ model: 'gpt-3.5-turbo-0613', expected: 129 },
|
|
{ model: 'gpt-3.5-turbo', expected: 129 },
|
|
{ model: 'gpt-4-0314', expected: 129 },
|
|
{ model: 'gpt-4-0613', expected: 129 },
|
|
{ model: 'gpt-4', expected: 129 },
|
|
{ model: 'unknown', expected: 129 },
|
|
];
|
|
|
|
testCases.forEach((testCase) => {
|
|
it(`should return ${testCase.expected} tokens for model ${testCase.model}`, () => {
|
|
client.modelOptions.model = testCase.model;
|
|
client.selectTokenizer();
|
|
// 3 tokens for assistant label
|
|
let totalTokens = 3;
|
|
for (let message of example_messages) {
|
|
totalTokens += client.getTokenCountForMessage(message);
|
|
}
|
|
expect(totalTokens).toBe(testCase.expected);
|
|
});
|
|
});
|
|
});
|
|
});
|