Skip to content

Commit

Permalink
🤖 feat: OpenAI Assistants v2 (initial support) (danny-avila#2781)
Browse files Browse the repository at this point in the history
* 🤖 Assistants V2 Support: Part 1

- Separated Azure Assistants to its own endpoint
- File Search / Vector Store integration is incomplete, but can toggle and use storage from playground
- Code Interpreter resource files can be added but not deleted
- GPT-4o is supported
- Many improvements to the Assistants Endpoint overall

data-provider v2 changes

copy existing route as v1

chore: rename new endpoint to reduce comparison operations and add new azure filesource

api: add azureAssistants part 1

force use of version for assistants/assistantsAzure

chore: switch name back to azureAssistants

refactor type version: string | number

Ensure assistants endpoints have version set

fix: isArchived type issue in ConversationListParams

refactor: update assistants mutations/queries with endpoint/version definitions, update Assistants Map structure

chore:  FilePreview component ExtendedFile type assertion

feat: isAssistantsEndpoint helper

chore: remove unused useGenerations

chore(buildTree): type issue

chore(Advanced): type issue (unused component, maybe in future)

first pass for multi-assistant endpoint rewrite

fix(listAssistants): pass params correctly

feat: list separate assistants by endpoint

fix(useTextarea): access assistantMap correctly

fix: assistant endpoint switching, resetting ID

fix: broken during rewrite, selecting assistant mention

fix: set/invalidate assistants endpoint query data correctly

feat: Fix issue with assistant ID not being reset correctly

getOpenAIClient helper function

feat: add toast for assistant deletion

fix: assistants delete right after create issue for azure

fix: assistant patching

refactor: actions to use getOpenAIClient

refactor: consolidate logic into helpers file

fix: issue where conversation data was not initially available

v1 chat support

refactor(spendTokens): only early return if completionTokens isNaN

fix(OpenAIClient): ensure spendTokens has all necessary params

refactor: route/controller logic

fix(assistants/initializeClient): use defaultHeaders field

fix: sanitize default operation id

chore: bump openai package

first pass v2 action service

feat: retroactive domain parsing for actions added via v1

feat: delete db records of actions/assistants on openai assistant deletion

chore: remove vision tools from v2 assistants

feat: v2 upload and delete assistant vision images

WIP first pass, thread attachments

fix: show assistant vision files (save local/firebase copy)

v2 image continue

fix: annotations

fix: refine annotations

show analyze as error if is no longer submitting before progress reaches 1 and show file_search as retrieval tool

fix: abort run, undefined endpoint issue

refactor: consolidate capabilities logic and anticipate versioning

frontend version 2 changes

fix: query selection and filter

add endpoint to unknown filepath

add file ids to resource, deleting in progress

enable/disable file search

remove version log

* 🤖 Assistants V2 Support: Part 2

🎹 fix: Autocompletion Chrome Bug on Action API Key Input

chore: remove `useOriginNavigate`

chore: set correct OpenAI Storage Source

fix: azure file deletions, instantiate clients by source for deletion

update code interpret files info

feat: deleteResourceFileId

chore: increase poll interval as azure easily rate limits

fix: openai file deletions, TODO: evaluate rejected deletion settled promises to determine which to delete from db records

file source icons

update table file filters

chore: file search info and versioning

fix: retrieval update with necessary tool_resources if specified

fix(useMentions): add optional chaining in case listMap value is undefined

fix: force assistant avatar roundedness

fix: azure assistants, check correct flag

chore: bump data-provider

* fix: merge conflict

* ci: fix backend tests due to new updates

* chore: update .env.example

* meilisearch improvements

* localization updates

* chore: update comparisons

* feat: add additional metadata: endpoint, author ID

* chore: azureAssistants ENDPOINTS exclusion warning
  • Loading branch information
danny-avila authored May 19, 2024
1 parent af8bcb0 commit 1a45212
Show file tree
Hide file tree
Showing 158 changed files with 4,178 additions and 1,198 deletions.
10 changes: 10 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,16 @@ ASSISTANTS_API_KEY=user_provided
# ASSISTANTS_BASE_URL=
# ASSISTANTS_MODELS=gpt-4o,gpt-3.5-turbo-0125,gpt-3.5-turbo-16k-0613,gpt-3.5-turbo-16k,gpt-3.5-turbo,gpt-4,gpt-4-0314,gpt-4-32k-0314,gpt-4-0613,gpt-3.5-turbo-0613,gpt-3.5-turbo-1106,gpt-4-0125-preview,gpt-4-turbo-preview,gpt-4-1106-preview

#==========================#
# Azure Assistants API #
#==========================#

# Note: You should map your credentials with custom variables according to your Azure OpenAI Configuration
# The models for Azure Assistants are also determined by your Azure OpenAI configuration.

# More info, including how to enable use of Assistants with Azure here:
# https://www.librechat.ai/docs/configuration/librechat_yaml/ai_endpoints/azure#using-assistants-with-azure

#============#
# OpenRouter #
#============#
Expand Down
9 changes: 8 additions & 1 deletion api/app/clients/OpenAIClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -756,6 +756,8 @@ class OpenAIClient extends BaseClient {
* In case of failure, it will return the default title, "New Chat".
*/
async titleConvo({ text, conversationId, responseText = '' }) {
this.conversationId = conversationId;

if (this.options.attachments) {
delete this.options.attachments;
}
Expand Down Expand Up @@ -838,13 +840,17 @@ ${convo}

try {
let useChatCompletion = true;

if (this.options.reverseProxyUrl === CohereConstants.API_URL) {
useChatCompletion = false;
}

title = (
await this.sendPayload(instructionsPayload, { modelOptions, useChatCompletion })
).replaceAll('"', '');

const completionTokens = this.getTokenCount(title);

this.recordTokenUsage({ promptTokens, completionTokens, context: 'title' });
} catch (e) {
logger.error(
Expand All @@ -868,6 +874,7 @@ ${convo}
context: 'title',
tokenBuffer: 150,
});

title = await runTitleChain({ llm, text, convo, signal: this.abortController.signal });
} catch (e) {
if (e?.message?.toLowerCase()?.includes('abort')) {
Expand Down Expand Up @@ -1005,9 +1012,9 @@ ${convo}
await spendTokens(
{
context,
user: this.user,
model: this.modelOptions.model,
conversationId: this.conversationId,
user: this.user ?? this.options.req.user?.id,
endpointTokenConfig: this.options.endpointTokenConfig,
},
{ promptTokens, completionTokens },
Expand Down
1 change: 1 addition & 0 deletions api/app/clients/specs/OpenAIClient.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ describe('OpenAIClient', () => {

const defaultOptions = {
// debug: true,
req: {},
openaiApiKey: 'new-api-key',
modelOptions: {
model,
Expand Down
18 changes: 17 additions & 1 deletion api/models/Action.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,24 @@ const deleteAction = async (searchParams, session = null) => {
return await Action.findOneAndDelete(searchParams, options).lean();
};

/**
* Deletes actions by params, within a transaction session if provided.
*
* @param {Object} searchParams - The search parameters to find the actions to delete.
* @param {string} searchParams.action_id - The ID of the action(s) to delete.
* @param {string} searchParams.user - The user ID of the action's author.
* @param {mongoose.ClientSession} [session] - The transaction session to use (optional).
* @returns {Promise<Number>} A promise that resolves to the number of deleted action documents.
*/
const deleteActions = async (searchParams, session = null) => {
const options = session ? { session } : {};
const result = await Action.deleteMany(searchParams, options);
return result.deletedCount;
};

module.exports = {
updateAction,
getActions,
updateAction,
deleteAction,
deleteActions,
};
13 changes: 13 additions & 0 deletions api/models/Assistant.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,21 @@ const getAssistants = async (searchParams) => {
return await Assistant.find(searchParams).lean();
};

/**
* Deletes an assistant based on the provided ID.
*
* @param {Object} searchParams - The search parameters to find the assistant to delete.
* @param {string} searchParams.assistant_id - The ID of the assistant to delete.
* @param {string} searchParams.user - The user ID of the assistant's author.
* @returns {Promise<void>} Resolves when the assistant has been successfully deleted.
*/
const deleteAssistant = async (searchParams) => {
return await Assistant.findOneAndDelete(searchParams);
};

module.exports = {
updateAssistant,
deleteAssistant,
getAssistants,
getAssistant,
};
2 changes: 1 addition & 1 deletion api/models/plugins/mongoMeili.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ const createMeiliMongooseModel = function ({ index, attributesToIndex }) {
function (results, value, key) {
return { ...results, [key]: 1 };
},
{ _id: 1 },
{ _id: 1, __v: 1 },
),
).lean();

Expand Down
2 changes: 1 addition & 1 deletion api/models/spendTokens.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const spendTokens = async (txData, tokenUsage) => {
});
}

if (!completionTokens) {
if (!completionTokens && isNaN(completionTokens)) {
logger.debug('[spendTokens] !completionTokens', { prompt, completion });
return;
}
Expand Down
2 changes: 1 addition & 1 deletion api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
"nodejs-gpt": "^1.37.4",
"nodemailer": "^6.9.4",
"ollama": "^0.5.0",
"openai": "4.36.0",
"openai": "^4.47.1",
"openai-chat-tokens": "^0.2.8",
"openid-client": "^5.4.2",
"passport": "^0.6.0",
Expand Down
20 changes: 19 additions & 1 deletion api/server/controllers/EndpointController.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,28 @@ async function endpointController(req, res) {
/** @type {TEndpointsConfig} */
const mergedConfig = { ...defaultEndpointsConfig, ...customConfigEndpoints };
if (mergedConfig[EModelEndpoint.assistants] && req.app.locals?.[EModelEndpoint.assistants]) {
const { disableBuilder, retrievalModels, capabilities, ..._rest } =
const { disableBuilder, retrievalModels, capabilities, version, ..._rest } =
req.app.locals[EModelEndpoint.assistants];

mergedConfig[EModelEndpoint.assistants] = {
...mergedConfig[EModelEndpoint.assistants],
version,
retrievalModels,
disableBuilder,
capabilities,
};
}

if (
mergedConfig[EModelEndpoint.azureAssistants] &&
req.app.locals?.[EModelEndpoint.azureAssistants]
) {
const { disableBuilder, retrievalModels, capabilities, version, ..._rest } =
req.app.locals[EModelEndpoint.azureAssistants];

mergedConfig[EModelEndpoint.azureAssistants] = {
...mergedConfig[EModelEndpoint.azureAssistants],
version,
retrievalModels,
disableBuilder,
capabilities,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
const { v4 } = require('uuid');
const express = require('express');
const {
Constants,
RunStatus,
CacheKeys,
FileSources,
ContentTypes,
EModelEndpoint,
ViolationTypes,
ImageVisionTool,
checkOpenAIStorage,
AssistantStreamEvents,
} = require('librechat-data-provider');
const {
Expand All @@ -21,44 +20,36 @@ const {
} = require('~/server/services/Threads');
const { sendResponse, sendMessage, sleep, isEnabled, countTokens } = require('~/server/utils');
const { runAssistant, createOnTextProgress } = require('~/server/services/AssistantService');
const { addTitle, initializeClient } = require('~/server/services/Endpoints/assistants');
const { formatMessage, createVisionPrompt } = require('~/app/clients/prompts');
const { createRun, StreamRunManager } = require('~/server/services/Runs');
const { addTitle } = require('~/server/services/Endpoints/assistants');
const { getTransactions } = require('~/models/Transaction');
const checkBalance = require('~/models/checkBalance');
const { getConvo } = require('~/models/Conversation');
const getLogStores = require('~/cache/getLogStores');
const { getModelMaxTokens } = require('~/utils');
const { getOpenAIClient } = require('./helpers');
const { logger } = require('~/config');

const router = express.Router();
const {
setHeaders,
handleAbort,
validateModel,
handleAbortError,
// validateEndpoint,
buildEndpointOption,
} = require('~/server/middleware');

router.post('/abort', handleAbort());
const { handleAbortError } = require('~/server/middleware');

const ten_minutes = 1000 * 60 * 10;

/**
* @route POST /
* @desc Chat with an assistant
* @access Public
* @param {express.Request} req - The request object, containing the request data.
* @param {express.Response} res - The response object, used to send back a response.
* @param {Express.Request} req - The request object, containing the request data.
* @param {Express.Response} res - The response object, used to send back a response.
* @returns {void}
*/
router.post('/', validateModel, buildEndpointOption, setHeaders, async (req, res) => {
const chatV1 = async (req, res) => {
logger.debug('[/assistants/chat/] req.body', req.body);

const {
text,
model,
endpoint,
files = [],
promptPrefix,
assistant_id,
Expand All @@ -70,7 +61,7 @@ router.post('/', validateModel, buildEndpointOption, setHeaders, async (req, res
} = req.body;

/** @type {Partial<TAssistantEndpoint>} */
const assistantsConfig = req.app.locals?.[EModelEndpoint.assistants];
const assistantsConfig = req.app.locals?.[endpoint];

if (assistantsConfig) {
const { supportedIds, excludedIds } = assistantsConfig;
Expand Down Expand Up @@ -138,7 +129,7 @@ router.post('/', validateModel, buildEndpointOption, setHeaders, async (req, res
user: req.user.id,
shouldSaveMessage: false,
messageId: responseMessageId,
endpoint: EModelEndpoint.assistants,
endpoint,
};

if (error.message === 'Run cancelled') {
Expand All @@ -149,7 +140,7 @@ router.post('/', validateModel, buildEndpointOption, setHeaders, async (req, res
logger.debug('[/assistants/chat/] Request aborted on close');
} else if (/Files.*are invalid/.test(error.message)) {
const errorMessage = `Files are invalid, or may not have uploaded yet.${
req.app.locals?.[EModelEndpoint.azureOpenAI].assistants
endpoint === EModelEndpoint.azureAssistants
? ' If using Azure OpenAI, files are only available in the region of the assistant\'s model at the time of upload.'
: ''
}`;
Expand Down Expand Up @@ -205,6 +196,7 @@ router.post('/', validateModel, buildEndpointOption, setHeaders, async (req, res
const runMessages = await checkMessageGaps({
openai,
run_id,
endpoint,
thread_id,
conversationId,
latestMessageId: responseMessageId,
Expand Down Expand Up @@ -311,8 +303,7 @@ router.post('/', validateModel, buildEndpointOption, setHeaders, async (req, res
});
};

/** @type {{ openai: OpenAIClient }} */
const { openai: _openai, client } = await initializeClient({
const { openai: _openai, client } = await getOpenAIClient({
req,
res,
endpointOption: req.body.endpointOption,
Expand Down Expand Up @@ -370,10 +361,7 @@ router.post('/', validateModel, buildEndpointOption, setHeaders, async (req, res

/** @type {MongoFile[]} */
const attachments = await req.body.endpointOption.attachments;
if (
attachments &&
attachments.every((attachment) => attachment.source === FileSources.openai)
) {
if (attachments && attachments.every((attachment) => checkOpenAIStorage(attachment.source))) {
return;
}

Expand Down Expand Up @@ -431,7 +419,7 @@ router.post('/', validateModel, buildEndpointOption, setHeaders, async (req, res

if (processedFiles) {
for (const file of processedFiles) {
if (file.source !== FileSources.openai) {
if (!checkOpenAIStorage(file.source)) {
attachedFileIds.delete(file.file_id);
const index = file_ids.indexOf(file.file_id);
if (index > -1) {
Expand Down Expand Up @@ -467,6 +455,7 @@ router.post('/', validateModel, buildEndpointOption, setHeaders, async (req, res
assistant_id,
thread_id,
model: assistant_id,
endpoint,
};

previousMessages.push(requestMessage);
Expand All @@ -476,7 +465,7 @@ router.post('/', validateModel, buildEndpointOption, setHeaders, async (req, res

conversation = {
conversationId,
endpoint: EModelEndpoint.assistants,
endpoint,
promptPrefix: promptPrefix,
instructions: instructions,
assistant_id,
Expand Down Expand Up @@ -513,7 +502,7 @@ router.post('/', validateModel, buildEndpointOption, setHeaders, async (req, res
let response;

const processRun = async (retry = false) => {
if (req.app.locals[EModelEndpoint.azureOpenAI]?.assistants) {
if (endpoint === EModelEndpoint.azureAssistants) {
body.model = openai._options.model;
openai.attachedFileIds = attachedFileIds;
openai.visionPromise = visionPromise;
Expand Down Expand Up @@ -603,6 +592,7 @@ router.post('/', validateModel, buildEndpointOption, setHeaders, async (req, res
assistant_id,
thread_id,
model: assistant_id,
endpoint,
};

sendMessage(res, {
Expand Down Expand Up @@ -655,6 +645,6 @@ router.post('/', validateModel, buildEndpointOption, setHeaders, async (req, res
} catch (error) {
await handleError(error);
}
});
};

module.exports = router;
module.exports = chatV1;
Loading

0 comments on commit 1a45212

Please sign in to comment.