feat: Add Current Datetime to Assistants (v1/v2) (#4952)

* Feature: Added ability to send current date and time to v1 and v2 assistants

* remove date_feature.patch

* fix: rename append_today_date to append_current_datetime

* feat: Refactor time handling in chatV1 and chatV2, add date and time utility functions

* fix: Add warning log and response for missing run values in abortRun middleware

---------

Co-authored-by: Max Sanna <max@maxsanna.com>
This commit is contained in:
Danny Avila 2024-12-11 15:26:18 -05:00 committed by GitHub
parent b5c9144127
commit 1dbe6ee75d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 378 additions and 67 deletions

View file

@ -1,5 +1,6 @@
const { v4 } = require('uuid');
const {
Time,
Constants,
RunStatus,
CacheKeys,
@ -24,6 +25,7 @@ const validateAuthor = require('~/server/middleware/assistants/validateAuthor');
const { formatMessage, createVisionPrompt } = require('~/app/clients/prompts');
const { createRun, StreamRunManager } = require('~/server/services/Runs');
const { addTitle } = require('~/server/services/Endpoints/assistants');
const { createRunBody } = require('~/server/services/createRunBody');
const { getTransactions } = require('~/models/Transaction');
const checkBalance = require('~/models/checkBalance');
const { getConvo } = require('~/models/Conversation');
@ -32,8 +34,6 @@ const { getModelMaxTokens } = require('~/utils');
const { getOpenAIClient } = require('./helpers');
const { logger } = require('~/config');
const ten_minutes = 1000 * 60 * 10;
/**
* @route POST /
* @desc Chat with an assistant
@ -59,6 +59,7 @@ const chatV1 = async (req, res) => {
messageId: _messageId,
conversationId: convoId,
parentMessageId: _parentId = Constants.NO_PARENT,
clientTimestamp,
} = req.body;
/** @type {OpenAIClient} */
@ -304,24 +305,14 @@ const chatV1 = async (req, res) => {
};
/** @type {CreateRunBody | undefined} */
const body = {
const body = createRunBody({
assistant_id,
model,
};
if (promptPrefix) {
body.additional_instructions = promptPrefix;
}
if (typeof endpointOption.artifactsPrompt === 'string' && endpointOption.artifactsPrompt) {
body.additional_instructions = `${body.additional_instructions ?? ''}\n${
endpointOption.artifactsPrompt
}`.trim();
}
if (instructions) {
body.instructions = instructions;
}
promptPrefix,
instructions,
endpointOption,
clientTimestamp,
});
const getRequestFileIds = async () => {
let thread_file_ids = [];
@ -518,7 +509,7 @@ const chatV1 = async (req, res) => {
});
run_id = run.id;
await cache.set(cacheKey, `${thread_id}:${run_id}`, ten_minutes);
await cache.set(cacheKey, `${thread_id}:${run_id}`, Time.TEN_MINUTES);
sendInitialResponse();
// todo: retry logic
@ -529,7 +520,7 @@ const chatV1 = async (req, res) => {
/** @type {{[AssistantStreamEvents.ThreadRunCreated]: (event: ThreadRunCreated) => Promise<void>}} */
const handlers = {
[AssistantStreamEvents.ThreadRunCreated]: async (event) => {
await cache.set(cacheKey, `${thread_id}:${event.data.id}`, ten_minutes);
await cache.set(cacheKey, `${thread_id}:${event.data.id}`, Time.TEN_MINUTES);
run_id = event.data.id;
sendInitialResponse();
},

View file

@ -23,6 +23,7 @@ const { createErrorHandler } = require('~/server/controllers/assistants/errors')
const validateAuthor = require('~/server/middleware/assistants/validateAuthor');
const { createRun, StreamRunManager } = require('~/server/services/Runs');
const { addTitle } = require('~/server/services/Endpoints/assistants');
const { createRunBody } = require('~/server/services/createRunBody');
const { getTransactions } = require('~/models/Transaction');
const checkBalance = require('~/models/checkBalance');
const { getConvo } = require('~/models/Conversation');
@ -31,8 +32,6 @@ const { getModelMaxTokens } = require('~/utils');
const { getOpenAIClient } = require('./helpers');
const { logger } = require('~/config');
const ten_minutes = 1000 * 60 * 10;
/**
* @route POST /
* @desc Chat with an assistant
@ -58,6 +57,7 @@ const chatV2 = async (req, res) => {
messageId: _messageId,
conversationId: convoId,
parentMessageId: _parentId = Constants.NO_PARENT,
clientTimestamp,
} = req.body;
/** @type {OpenAIClient} */
@ -186,22 +186,14 @@ const chatV2 = async (req, res) => {
};
/** @type {CreateRunBody | undefined} */
const body = {
const body = createRunBody({
assistant_id,
model,
};
if (promptPrefix) {
body.additional_instructions = promptPrefix;
}
if (typeof endpointOption.artifactsPrompt === 'string' && endpointOption.artifactsPrompt) {
body.additional_instructions = `${body.additional_instructions ?? ''}\n${endpointOption.artifactsPrompt}`.trim();
}
if (instructions) {
body.instructions = instructions;
}
promptPrefix,
instructions,
endpointOption,
clientTimestamp,
});
const getRequestFileIds = async () => {
let thread_file_ids = [];
@ -361,7 +353,7 @@ const chatV2 = async (req, res) => {
});
run_id = run.id;
await cache.set(cacheKey, `${thread_id}:${run_id}`, ten_minutes);
await cache.set(cacheKey, `${thread_id}:${run_id}`, Time.TEN_MINUTES);
sendInitialResponse();
// todo: retry logic
@ -372,7 +364,7 @@ const chatV2 = async (req, res) => {
/** @type {{[AssistantStreamEvents.ThreadRunCreated]: (event: ThreadRunCreated) => Promise<void>}} */
const handlers = {
[AssistantStreamEvents.ThreadRunCreated]: async (event) => {
await cache.set(cacheKey, `${thread_id}:${event.data.id}`, ten_minutes);
await cache.set(cacheKey, `${thread_id}:${event.data.id}`, Time.TEN_MINUTES);
run_id = event.data.id;
sendInitialResponse();
},

View file

@ -19,8 +19,15 @@ const createAssistant = async (req, res) => {
try {
const { openai } = await getOpenAIClient({ req, res });
const { tools = [], endpoint, conversation_starters, ...assistantData } = req.body;
const {
tools = [],
endpoint,
conversation_starters,
append_current_datetime,
...assistantData
} = req.body;
delete assistantData.conversation_starters;
delete assistantData.append_current_datetime;
assistantData.tools = tools
.map((tool) => {
@ -49,6 +56,9 @@ const createAssistant = async (req, res) => {
if (conversation_starters) {
createData.conversation_starters = conversation_starters;
}
if (append_current_datetime !== undefined) {
createData.append_current_datetime = append_current_datetime;
}
const document = await updateAssistantDoc({ assistant_id: assistant.id }, createData);
@ -60,6 +70,10 @@ const createAssistant = async (req, res) => {
assistant.conversation_starters = document.conversation_starters;
}
if (append_current_datetime !== undefined) {
assistant.append_current_datetime = append_current_datetime;
}
logger.debug('/assistants/', assistant);
res.status(201).json(assistant);
} catch (error) {
@ -102,7 +116,12 @@ const patchAssistant = async (req, res) => {
await validateAuthor({ req, openai });
const assistant_id = req.params.id;
const { endpoint: _e, conversation_starters, ...updateData } = req.body;
const {
endpoint: _e,
conversation_starters,
append_current_datetime,
...updateData
} = req.body;
updateData.tools = (updateData.tools ?? [])
.map((tool) => {
if (typeof tool !== 'string') {
@ -127,6 +146,11 @@ const patchAssistant = async (req, res) => {
updatedAssistant.conversation_starters = conversationStartersUpdate.conversation_starters;
}
if (append_current_datetime !== undefined) {
await updateAssistantDoc({ assistant_id }, { append_current_datetime });
updatedAssistant.append_current_datetime = append_current_datetime;
}
res.json(updatedAssistant);
} catch (error) {
logger.error('[/assistants/:id] Error updating assistant', error);
@ -219,6 +243,7 @@ const getAssistantDocuments = async (req, res) => {
conversation_starters: 1,
createdAt: 1,
updatedAt: 1,
append_current_datetime: 1,
},
);

View file

@ -16,8 +16,15 @@ const createAssistant = async (req, res) => {
/** @type {{ openai: OpenAIClient }} */
const { openai } = await getOpenAIClient({ req, res });
const { tools = [], endpoint, conversation_starters, ...assistantData } = req.body;
const {
tools = [],
endpoint,
conversation_starters,
append_current_datetime,
...assistantData
} = req.body;
delete assistantData.conversation_starters;
delete assistantData.append_current_datetime;
assistantData.tools = tools
.map((tool) => {
@ -46,6 +53,9 @@ const createAssistant = async (req, res) => {
if (conversation_starters) {
createData.conversation_starters = conversation_starters;
}
if (append_current_datetime !== undefined) {
createData.append_current_datetime = append_current_datetime;
}
const document = await updateAssistantDoc({ assistant_id: assistant.id }, createData);
@ -56,6 +66,9 @@ const createAssistant = async (req, res) => {
if (document.conversation_starters) {
assistant.conversation_starters = document.conversation_starters;
}
if (append_current_datetime !== undefined) {
assistant.append_current_datetime = append_current_datetime;
}
logger.debug('/assistants/', assistant);
res.status(201).json(assistant);
@ -89,6 +102,14 @@ const updateAssistant = async ({ req, openai, assistant_id, updateData }) => {
delete updateData.conversation_starters;
}
if (updateData?.append_current_datetime !== undefined) {
await updateAssistantDoc(
{ assistant_id: assistant_id },
{ append_current_datetime: updateData.append_current_datetime },
);
delete updateData.append_current_datetime;
}
let hasFileSearch = false;
for (const tool of updateData.tools ?? []) {
let actualTool = typeof tool === 'string' ? req.app.locals.availableTools[tool] : tool;

View file

@ -27,6 +27,10 @@ async function abortRun(req, res) {
const cacheKey = `${req.user.id}:${conversationId}`;
const cache = getLogStores(CacheKeys.ABORT_KEYS);
const runValues = await cache.get(cacheKey);
if (!runValues) {
logger.warn('[abortRun] Run not found in cache', { cacheKey });
return res.status(204).send({ message: 'Run not found' });
}
const [thread_id, run_id] = runValues.split(':');
if (!run_id) {

View file

@ -79,7 +79,7 @@ async function buildEndpointOption(req, res, next) {
const builder = isAgents ? (...args) => endpointFn(req, ...args) : endpointFn;
// TODO: use object params
req.body.endpointOption = builder(endpoint, parsedBody, endpointType);
req.body.endpointOption = await builder(endpoint, parsedBody, endpointType);
// TODO: use `getModelsConfig` only when necessary
const modelsConfig = await getModelsConfig(req);

View file

@ -1,7 +1,8 @@
const { removeNullishValues } = require('librechat-data-provider');
const generateArtifactsPrompt = require('~/app/clients/prompts/artifacts');
const { getAssistant } = require('~/models/Assistant');
const buildOptions = (endpoint, parsedBody) => {
const buildOptions = async (endpoint, parsedBody) => {
// eslint-disable-next-line no-unused-vars
const { promptPrefix, assistant_id, iconURL, greeting, spec, artifacts, ...modelOptions } =
parsedBody;
@ -15,6 +16,21 @@ const buildOptions = (endpoint, parsedBody) => {
modelOptions,
});
if (assistant_id) {
const assistantDoc = await getAssistant({ assistant_id });
if (assistantDoc) {
// Create a clean assistant object with only the needed properties
endpointOption.assistant = {
append_current_datetime: assistantDoc.append_current_datetime,
assistant_id: assistantDoc.assistant_id,
conversation_starters: assistantDoc.conversation_starters,
createdAt: assistantDoc.createdAt,
updatedAt: assistantDoc.updatedAt,
};
}
}
if (typeof artifacts === 'string') {
endpointOption.artifactsPrompt = generateArtifactsPrompt({ endpoint, artifacts });
}

View file

@ -1,7 +1,8 @@
const { removeNullishValues } = require('librechat-data-provider');
const generateArtifactsPrompt = require('~/app/clients/prompts/artifacts');
const { getAssistant } = require('~/models/Assistant');
const buildOptions = (endpoint, parsedBody) => {
const buildOptions = async (endpoint, parsedBody) => {
// eslint-disable-next-line no-unused-vars
const { promptPrefix, assistant_id, iconURL, greeting, spec, artifacts, ...modelOptions } =
parsedBody;
@ -15,6 +16,19 @@ const buildOptions = (endpoint, parsedBody) => {
modelOptions,
});
if (assistant_id) {
const assistantDoc = await getAssistant({ assistant_id });
if (assistantDoc) {
endpointOption.assistant = {
append_current_datetime: assistantDoc.append_current_datetime,
assistant_id: assistantDoc.assistant_id,
conversation_starters: assistantDoc.conversation_starters,
createdAt: assistantDoc.createdAt,
updatedAt: assistantDoc.updatedAt,
};
}
}
if (typeof artifacts === 'string') {
endpointOption.artifactsPrompt = generateArtifactsPrompt({ endpoint, artifacts });
}

View file

@ -0,0 +1,78 @@
/**
* Obtains the date string in 'YYYY-MM-DD' format.
*
* @param {string} [clientTimestamp] - Optional ISO timestamp string. If provided, uses this timestamp;
* otherwise, uses the current date.
* @returns {string} - The date string in 'YYYY-MM-DD' format.
*/
function getDateStr(clientTimestamp) {
return clientTimestamp ? clientTimestamp.split('T')[0] : new Date().toISOString().split('T')[0];
}
/**
* Obtains the time string in 'HH:MM:SS' format.
*
* @param {string} [clientTimestamp] - Optional ISO timestamp string. If provided, uses this timestamp;
* otherwise, uses the current time.
* @returns {string} - The time string in 'HH:MM:SS' format.
*/
function getTimeStr(clientTimestamp) {
return clientTimestamp
? clientTimestamp.split('T')[1].split('.')[0]
: new Date().toTimeString().split(' ')[0];
}
/**
* Creates the body object for a run request.
*
* @param {Object} options - The options for creating the run body.
* @param {string} options.assistant_id - The assistant ID.
* @param {string} options.model - The model name.
* @param {string} [options.promptPrefix] - The prompt prefix to include.
* @param {string} [options.instructions] - The instructions to include.
* @param {Object} [options.endpointOption={}] - The endpoint options.
* @param {string} [options.clientTimestamp] - Client timestamp in ISO format.
*
* @returns {Object} - The constructed body object for the run request.
*/
const createRunBody = ({
assistant_id,
model,
promptPrefix,
instructions,
endpointOption = {},
clientTimestamp,
}) => {
const body = {
assistant_id,
model,
};
let systemInstructions = '';
if (endpointOption.assistant?.append_current_datetime) {
const dateStr = getDateStr(clientTimestamp);
const timeStr = getTimeStr(clientTimestamp);
systemInstructions = `Current date and time: ${dateStr} ${timeStr}\n`;
}
if (promptPrefix) {
systemInstructions += promptPrefix;
}
if (typeof endpointOption?.artifactsPrompt === 'string' && endpointOption.artifactsPrompt) {
systemInstructions += `\n${endpointOption.artifactsPrompt}`;
}
if (systemInstructions.trim()) {
body.additional_instructions = systemInstructions.trim();
}
if (instructions) {
body.instructions = instructions;
}
return body;
};
module.exports = { createRunBody, getDateStr, getTimeStr };