mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 17:00:15 +01:00
🚧 WIP: Merge Dev Build (#4611)
* refactor: Agent CodeFiles, abortUpload WIP * feat: code environment file upload * refactor: useLazyEffect * refactor: - Add `watch` from `useFormContext` to check if code execution is enabled - Disable file upload button if `agent_id` is not selected or code execution is disabled * WIP: primeCodeFiles; refactor: rename sessionId to session_id for uniformity * Refactor: Rename session_id to sessionId for uniformity in AuthService.js * chore: bump @librechat/agents to version 1.7.1 * WIP: prime code files * refactor: Update code env file upload method to use read stream * feat: reupload code env file if no longer active * refactor: isAssistantTool -> isEntityTool + address type issues * feat: execute code tool hook * refactor: Rename isPluginAuthenticated to checkPluginAuth in PluginController.js * refactor: Update PluginController.js to use AuthType constant for comparison * feat: verify tool authentication (execute_code) * feat: enter librechat_code_api_key * refactor: Remove unused imports in BookmarkForm.tsx * feat: authenticate code tool * refactor: Update Action.tsx to conditionally render the key and revoke key buttons * refactor(Code/Action): prevent uncheck-able 'Run Code' capability when key is revoked * refactor(Code/Action): Update Action.tsx to conditionally render the key and revoke key buttons * fix: agent file upload edge cases * chore: bump @librechat/agents * fix: custom endpoint providerValue icon * feat: ollama meta modal token values + context * feat: ollama agents * refactor: Update token models for Ollama models * chore: Comment out CodeForm * refactor: Update token models for Ollama and Meta models
This commit is contained in:
parent
1909efd6ba
commit
95011ce349
58 changed files with 1418 additions and 1002 deletions
|
|
@ -1,5 +1,5 @@
|
|||
const { promises: fs } = require('fs');
|
||||
const { CacheKeys } = require('librechat-data-provider');
|
||||
const { CacheKeys, AuthType } = require('librechat-data-provider');
|
||||
const { addOpenAPISpecs } = require('~/app/clients/tools/util/addOpenAPISpecs');
|
||||
const { getLogStores } = require('~/cache');
|
||||
|
||||
|
|
@ -25,7 +25,7 @@ const filterUniquePlugins = (plugins) => {
|
|||
* @param {TPlugin} plugin The plugin object containing the authentication configuration.
|
||||
* @returns {boolean} True if the plugin is authenticated for all required fields, false otherwise.
|
||||
*/
|
||||
const isPluginAuthenticated = (plugin) => {
|
||||
const checkPluginAuth = (plugin) => {
|
||||
if (!plugin.authConfig || plugin.authConfig.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -36,7 +36,7 @@ const isPluginAuthenticated = (plugin) => {
|
|||
|
||||
for (const fieldOption of authFieldOptions) {
|
||||
const envValue = process.env[fieldOption];
|
||||
if (envValue && envValue.trim() !== '' && envValue !== 'user_provided') {
|
||||
if (envValue && envValue.trim() !== '' && envValue !== AuthType.USER_PROVIDED) {
|
||||
isFieldAuthenticated = true;
|
||||
break;
|
||||
}
|
||||
|
|
@ -64,7 +64,7 @@ const getAvailablePluginsController = async (req, res) => {
|
|||
let authenticatedPlugins = [];
|
||||
for (const plugin of uniquePlugins) {
|
||||
authenticatedPlugins.push(
|
||||
isPluginAuthenticated(plugin) ? { ...plugin, authenticated: true } : plugin,
|
||||
checkPluginAuth(plugin) ? { ...plugin, authenticated: true } : plugin,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -111,7 +111,7 @@ const getAvailableTools = async (req, res) => {
|
|||
const uniquePlugins = filterUniquePlugins(jsonData);
|
||||
|
||||
const authenticatedPlugins = uniquePlugins.map((plugin) => {
|
||||
if (isPluginAuthenticated(plugin)) {
|
||||
if (checkPluginAuth(plugin)) {
|
||||
return { ...plugin, authenticated: true };
|
||||
} else {
|
||||
return plugin;
|
||||
|
|
|
|||
|
|
@ -61,10 +61,10 @@ const deleteUserFiles = async (req) => {
|
|||
|
||||
const updateUserPluginsController = async (req, res) => {
|
||||
const { user } = req;
|
||||
const { pluginKey, action, auth, isAssistantTool } = req.body;
|
||||
const { pluginKey, action, auth, isEntityTool } = req.body;
|
||||
let authService;
|
||||
try {
|
||||
if (!isAssistantTool) {
|
||||
if (!isEntityTool) {
|
||||
const userPluginsService = await updateUserPluginsService(user, pluginKey, action);
|
||||
|
||||
if (userPluginsService instanceof Error) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
const { Tools } = require('librechat-data-provider');
|
||||
const { GraphEvents, ToolEndHandler, ChatModelStreamHandler } = require('@librechat/agents');
|
||||
const {
|
||||
EnvVar,
|
||||
GraphEvents,
|
||||
ToolEndHandler,
|
||||
ChatModelStreamHandler,
|
||||
} = require('@librechat/agents');
|
||||
const { processCodeOutput } = require('~/server/services/Files/Code/process');
|
||||
const { loadAuthValues } = require('~/app/clients/tools/util');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
/** @typedef {import('@librechat/agents').Graph} Graph */
|
||||
|
|
@ -158,13 +164,18 @@ function createToolEndCallback({ req, res, artifactPromises }) {
|
|||
const { id, name } = file;
|
||||
artifactPromises.push(
|
||||
(async () => {
|
||||
const result = await loadAuthValues({
|
||||
userId: req.user.id,
|
||||
authFields: [EnvVar.CODE_API_KEY],
|
||||
});
|
||||
const fileMetadata = await processCodeOutput({
|
||||
req,
|
||||
id,
|
||||
name,
|
||||
apiKey: result[EnvVar.CODE_API_KEY],
|
||||
toolCallId: tool_call_id,
|
||||
messageId: metadata.run_id,
|
||||
sessionId: artifact.session_id,
|
||||
session_id: artifact.session_id,
|
||||
conversationId: metadata.thread_id,
|
||||
});
|
||||
if (!res.headersSent) {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ const {
|
|||
EModelEndpoint,
|
||||
anthropicSchema,
|
||||
bedrockOutputParser,
|
||||
providerEndpointMap,
|
||||
removeNullishValues,
|
||||
} = require('librechat-data-provider');
|
||||
const {
|
||||
|
|
@ -465,7 +464,6 @@ class AgentClient extends BaseClient {
|
|||
|
||||
const config = {
|
||||
configurable: {
|
||||
provider: providerEndpointMap[this.options.agent.provider],
|
||||
thread_id: this.conversationId,
|
||||
},
|
||||
signal: abortController.signal,
|
||||
|
|
|
|||
|
|
@ -35,9 +35,10 @@ async function createRun({
|
|||
streaming = true,
|
||||
streamUsage = true,
|
||||
}) {
|
||||
const provider = providerEndpointMap[agent.provider] ?? agent.provider;
|
||||
const llmConfig = Object.assign(
|
||||
{
|
||||
provider: providerEndpointMap[agent.provider],
|
||||
provider,
|
||||
streaming,
|
||||
streamUsage,
|
||||
},
|
||||
|
|
|
|||
53
api/server/controllers/tools.js
Normal file
53
api/server/controllers/tools.js
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
const { EnvVar } = require('@librechat/agents');
|
||||
const { Tools, AuthType } = require('librechat-data-provider');
|
||||
const { loadAuthValues } = require('~/app/clients/tools/util');
|
||||
|
||||
const fieldsMap = {
|
||||
[Tools.execute_code]: [EnvVar.CODE_API_KEY],
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {ServerRequest} req - The request object, containing information about the HTTP request.
|
||||
* @param {ServerResponse} res - The response object, used to send back the desired HTTP response.
|
||||
* @returns {Promise<void>} A promise that resolves when the function has completed.
|
||||
*/
|
||||
const verifyToolAuth = async (req, res) => {
|
||||
try {
|
||||
const { toolId } = req.params;
|
||||
const authFields = fieldsMap[toolId];
|
||||
if (!authFields) {
|
||||
res.status(404).json({ message: 'Tool not found' });
|
||||
return;
|
||||
}
|
||||
let result;
|
||||
try {
|
||||
result = await loadAuthValues({
|
||||
userId: req.user.id,
|
||||
authFields,
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(200).json({ authenticated: false, message: AuthType.USER_PROVIDED });
|
||||
return;
|
||||
}
|
||||
let isUserProvided = false;
|
||||
for (const field of authFields) {
|
||||
if (!result[field]) {
|
||||
res.status(200).json({ authenticated: false, message: AuthType.USER_PROVIDED });
|
||||
return;
|
||||
}
|
||||
if (!isUserProvided && process.env[field] !== result[field]) {
|
||||
isUserProvided = true;
|
||||
}
|
||||
}
|
||||
res.status(200).json({
|
||||
authenticated: true,
|
||||
message: isUserProvided ? AuthType.USER_PROVIDED : AuthType.SYSTEM_DEFINED,
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
verifyToolAuth,
|
||||
};
|
||||
22
api/server/routes/agents/tools.js
Normal file
22
api/server/routes/agents/tools.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
const express = require('express');
|
||||
const { getAvailableTools } = require('~/server/controllers/PluginController');
|
||||
const { verifyToolAuth } = require('~/server/controllers/tools');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* Get a list of available tools for agents.
|
||||
* @route GET /agents/tools
|
||||
* @returns {TPlugin[]} 200 - application/json
|
||||
*/
|
||||
router.get('/', getAvailableTools);
|
||||
|
||||
/**
|
||||
* Verify authentication for a specific tool
|
||||
* @route GET /agents/tools/:toolId/auth
|
||||
* @param {string} toolId - The ID of the tool to verify
|
||||
* @returns {{ authenticated?: boolean; message?: string }}
|
||||
*/
|
||||
router.get('/:toolId/auth', verifyToolAuth);
|
||||
|
||||
module.exports = router;
|
||||
|
|
@ -2,9 +2,9 @@ const multer = require('multer');
|
|||
const express = require('express');
|
||||
const { PermissionTypes, Permissions } = require('librechat-data-provider');
|
||||
const { requireJwtAuth, generateCheckAccess } = require('~/server/middleware');
|
||||
const { getAvailableTools } = require('~/server/controllers/PluginController');
|
||||
const v1 = require('~/server/controllers/agents/v1');
|
||||
const actions = require('./actions');
|
||||
const tools = require('./tools');
|
||||
|
||||
const upload = multer();
|
||||
const router = express.Router();
|
||||
|
|
@ -35,9 +35,8 @@ router.use('/actions', actions);
|
|||
/**
|
||||
* Get a list of available tools for agents.
|
||||
* @route GET /agents/tools
|
||||
* @returns {TPlugin[]} 200 - application/json
|
||||
*/
|
||||
router.use('/tools', getAvailableTools);
|
||||
router.use('/tools', tools);
|
||||
|
||||
/**
|
||||
* Creates an agent.
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ const {
|
|||
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
|
||||
const { getOpenAIClient } = require('~/server/controllers/assistants/helpers');
|
||||
const { loadAuthValues } = require('~/app/clients/tools/util');
|
||||
const { getAgent } = require('~/models/Agent');
|
||||
const { getFiles } = require('~/models/File');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
|
|
@ -67,12 +68,31 @@ router.delete('/', async (req, res) => {
|
|||
}
|
||||
|
||||
const fileIds = files.map((file) => file.file_id);
|
||||
const userFiles = await getFiles({ file_id: { $in: fileIds }, user: req.user.id });
|
||||
if (userFiles.length !== files.length) {
|
||||
return res.status(403).json({ message: 'You can only delete your own files' });
|
||||
const dbFiles = await getFiles({ file_id: { $in: fileIds } });
|
||||
const unauthorizedFiles = dbFiles.filter((file) => file.user.toString() !== req.user.id);
|
||||
|
||||
if (unauthorizedFiles.length > 0) {
|
||||
return res.status(403).json({
|
||||
message: 'You can only delete your own files',
|
||||
unauthorizedFiles: unauthorizedFiles.map((f) => f.file_id),
|
||||
});
|
||||
}
|
||||
|
||||
await processDeleteRequest({ req, files: userFiles });
|
||||
/* Handle entity unlinking even if no valid files to delete */
|
||||
if (req.body.agent_id && req.body.tool_resource && dbFiles.length === 0) {
|
||||
const agent = await getAgent({
|
||||
id: req.body.agent_id,
|
||||
});
|
||||
|
||||
const toolResourceFiles = agent.tool_resources?.[req.body.tool_resource]?.file_ids ?? [];
|
||||
const agentFiles = files.filter((f) => toolResourceFiles.includes(f.file_id));
|
||||
|
||||
await processDeleteRequest({ req, files: agentFiles });
|
||||
res.status(200).json({ message: 'File associations removed successfully' });
|
||||
return;
|
||||
}
|
||||
|
||||
await processDeleteRequest({ req, files: dbFiles });
|
||||
|
||||
logger.debug(
|
||||
`[/files] Files deleted successfully: ${files
|
||||
|
|
@ -87,13 +107,13 @@ router.delete('/', async (req, res) => {
|
|||
}
|
||||
});
|
||||
|
||||
router.get('/code/download/:sessionId/:fileId', async (req, res) => {
|
||||
router.get('/code/download/:session_id/:fileId', async (req, res) => {
|
||||
try {
|
||||
const { sessionId, fileId } = req.params;
|
||||
const logPrefix = `Session ID: ${sessionId} | File ID: ${fileId} | Code output download requested by user `;
|
||||
const { session_id, fileId } = req.params;
|
||||
const logPrefix = `Session ID: ${session_id} | File ID: ${fileId} | Code output download requested by user `;
|
||||
logger.debug(logPrefix);
|
||||
|
||||
if (!sessionId || !fileId) {
|
||||
if (!session_id || !fileId) {
|
||||
return res.status(400).send('Bad request');
|
||||
}
|
||||
|
||||
|
|
@ -108,7 +128,10 @@ router.get('/code/download/:sessionId/:fileId', async (req, res) => {
|
|||
const result = await loadAuthValues({ userId: req.user.id, authFields: [EnvVar.CODE_API_KEY] });
|
||||
|
||||
/** @type {AxiosResponse<ReadableStream> | undefined} */
|
||||
const response = await getDownloadStream(`${sessionId}/${fileId}`, result[EnvVar.CODE_API_KEY]);
|
||||
const response = await getDownloadStream(
|
||||
`${session_id}/${fileId}`,
|
||||
result[EnvVar.CODE_API_KEY],
|
||||
);
|
||||
res.set(response.headers);
|
||||
response.data.pipe(res);
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
const { z } = require('zod');
|
||||
const { tool } = require('@langchain/core/tools');
|
||||
const { createContentAggregator } = require('@librechat/agents');
|
||||
const { createContentAggregator, Providers } = require('@librechat/agents');
|
||||
const {
|
||||
EModelEndpoint,
|
||||
getResponseSender,
|
||||
|
|
@ -22,8 +22,9 @@ const {
|
|||
createToolEndCallback,
|
||||
} = require('~/server/controllers/agents/callbacks');
|
||||
const initAnthropic = require('~/server/services/Endpoints/anthropic/initialize');
|
||||
const initOpenAI = require('~/server/services/Endpoints/openAI/initialize');
|
||||
const getBedrockOptions = require('~/server/services/Endpoints/bedrock/options');
|
||||
const initOpenAI = require('~/server/services/Endpoints/openAI/initialize');
|
||||
const initCustom = require('~/server/services/Endpoints/custom/initialize');
|
||||
const { loadAgentTools } = require('~/server/services/ToolService');
|
||||
const AgentClient = require('~/server/controllers/agents/client');
|
||||
const { getModelMaxTokens } = require('~/utils');
|
||||
|
|
@ -53,6 +54,7 @@ const providerConfigMap = {
|
|||
[EModelEndpoint.azureOpenAI]: initOpenAI,
|
||||
[EModelEndpoint.anthropic]: initAnthropic,
|
||||
[EModelEndpoint.bedrock]: getBedrockOptions,
|
||||
[Providers.OLLAMA]: initCustom,
|
||||
};
|
||||
|
||||
const initializeClient = async ({ req, res, endpointOption }) => {
|
||||
|
|
@ -92,7 +94,7 @@ const initializeClient = async ({ req, res, endpointOption }) => {
|
|||
});
|
||||
|
||||
let modelOptions = { model: agent.model };
|
||||
const getOptions = providerConfigMap[agent.provider];
|
||||
let getOptions = providerConfigMap[agent.provider];
|
||||
if (!getOptions) {
|
||||
throw new Error(`Provider ${agent.provider} not supported`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,11 +12,14 @@ const { fetchModels } = require('~/server/services/ModelService');
|
|||
const getLogStores = require('~/cache/getLogStores');
|
||||
const { isUserProvided } = require('~/server/utils');
|
||||
const { OpenAIClient } = require('~/app');
|
||||
const { Providers } = require('@librechat/agents');
|
||||
|
||||
const { PROXY } = process.env;
|
||||
|
||||
const initializeClient = async ({ req, res, endpointOption }) => {
|
||||
const { key: expiresAt, endpoint } = req.body;
|
||||
const initializeClient = async ({ req, res, endpointOption, optionsOnly, overrideEndpoint }) => {
|
||||
const { key: expiresAt } = req.body;
|
||||
const endpoint = overrideEndpoint ?? req.body.endpoint;
|
||||
|
||||
const customConfig = await getCustomConfig();
|
||||
if (!customConfig) {
|
||||
throw new Error(`Config not found for the ${endpoint} custom endpoint.`);
|
||||
|
|
@ -133,6 +136,17 @@ const initializeClient = async ({ req, res, endpointOption }) => {
|
|||
...endpointOption,
|
||||
};
|
||||
|
||||
if (optionsOnly) {
|
||||
const modelOptions = endpointOption.model_parameters;
|
||||
if (endpoint === Providers.OLLAMA && clientOptions.reverseProxyUrl) {
|
||||
modelOptions.baseUrl = clientOptions.reverseProxyUrl.split('/v1')[0];
|
||||
delete clientOptions.reverseProxyUrl;
|
||||
}
|
||||
return {
|
||||
llmConfig: modelOptions,
|
||||
};
|
||||
}
|
||||
|
||||
const client = new OpenAIClient(apiKey, clientOptions);
|
||||
return {
|
||||
client,
|
||||
|
|
|
|||
|
|
@ -1,19 +1,20 @@
|
|||
// downloadStream.js
|
||||
|
||||
// Code Files
|
||||
const axios = require('axios');
|
||||
const FormData = require('form-data');
|
||||
const { getCodeBaseURL } = require('@librechat/agents');
|
||||
|
||||
const baseURL = getCodeBaseURL();
|
||||
const MAX_FILE_SIZE = 25 * 1024 * 1024;
|
||||
|
||||
/**
|
||||
* Retrieves a download stream for a specified file.
|
||||
* @param {string} fileIdentifier - The identifier for the file (e.g., "sessionId/fileId").
|
||||
* @param {string} fileIdentifier - The identifier for the file (e.g., "session_id/fileId").
|
||||
* @param {string} apiKey - The API key for authentication.
|
||||
* @returns {Promise<AxiosResponse>} A promise that resolves to a readable stream of the file content.
|
||||
* @throws {Error} If there's an error during the download process.
|
||||
*/
|
||||
async function getCodeOutputDownloadStream(fileIdentifier, apiKey) {
|
||||
try {
|
||||
const baseURL = getCodeBaseURL();
|
||||
const response = await axios({
|
||||
method: 'get',
|
||||
url: `${baseURL}/download/${fileIdentifier}`,
|
||||
|
|
@ -31,4 +32,45 @@ async function getCodeOutputDownloadStream(fileIdentifier, apiKey) {
|
|||
}
|
||||
}
|
||||
|
||||
module.exports = { getCodeOutputDownloadStream };
|
||||
/**
|
||||
* Uploads a file to the Code Environment server.
|
||||
* @param {Object} params - The params object.
|
||||
* @param {ServerRequest} params.req - The request object from Express. It should have a `user` property with an `id`
|
||||
* representing the user, and an `app.locals.paths` object with an `uploads` path.
|
||||
* @param {import('fs').ReadStream | import('stream').Readable} params.stream - The read stream for the file.
|
||||
* @param {string} params.filename - The name of the file.
|
||||
* @param {string} params.apiKey - The API key for authentication.
|
||||
* @returns {Promise<string>}
|
||||
* @throws {Error} If there's an error during the upload process.
|
||||
*/
|
||||
async function uploadCodeEnvFile({ req, stream, filename, apiKey }) {
|
||||
try {
|
||||
const form = new FormData();
|
||||
form.append('file', stream, filename);
|
||||
|
||||
const baseURL = getCodeBaseURL();
|
||||
const response = await axios.post(`${baseURL}/upload`, form, {
|
||||
headers: {
|
||||
...form.getHeaders(),
|
||||
'Content-Type': 'multipart/form-data',
|
||||
'User-Agent': 'LibreChat/1.0',
|
||||
'User-Id': req.user.id,
|
||||
'X-API-Key': apiKey,
|
||||
},
|
||||
maxContentLength: MAX_FILE_SIZE,
|
||||
maxBodyLength: MAX_FILE_SIZE,
|
||||
});
|
||||
|
||||
/** @type {{ message: string; session_id: string; files: Array<{ fileId: string; filename: string }> }} */
|
||||
const result = response.data;
|
||||
if (result.message !== 'success') {
|
||||
throw new Error(`Error uploading file: ${result.message}`);
|
||||
}
|
||||
|
||||
return `${result.session_id}/${result.files[0].fileId}`;
|
||||
} catch (error) {
|
||||
throw new Error(`Error uploading file: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { getCodeOutputDownloadStream, uploadCodeEnvFile };
|
||||
|
|
|
|||
|
|
@ -1,11 +1,16 @@
|
|||
const path = require('path');
|
||||
const { v4 } = require('uuid');
|
||||
const axios = require('axios');
|
||||
const { getCodeBaseURL, EnvVar } = require('@librechat/agents');
|
||||
const { FileContext, imageExtRegex } = require('librechat-data-provider');
|
||||
const { getCodeBaseURL } = require('@librechat/agents');
|
||||
const {
|
||||
EToolResources,
|
||||
FileContext,
|
||||
imageExtRegex,
|
||||
FileSources,
|
||||
} = require('librechat-data-provider');
|
||||
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
|
||||
const { convertImage } = require('~/server/services/Files/images/convert');
|
||||
const { loadAuthValues } = require('~/app/clients/tools/util');
|
||||
const { createFile } = require('~/models/File');
|
||||
const { createFile, getFiles, updateFile } = require('~/models/File');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
/**
|
||||
|
|
@ -13,8 +18,9 @@ const { logger } = require('~/config');
|
|||
* @param {ServerRequest} params.req - The Express request object.
|
||||
* @param {string} params.id - The file ID.
|
||||
* @param {string} params.name - The filename.
|
||||
* @param {string} params.apiKey - The code execution API key.
|
||||
* @param {string} params.toolCallId - The tool call ID that generated the file.
|
||||
* @param {string} params.sessionId - The code execution session ID.
|
||||
* @param {string} params.session_id - The code execution session ID.
|
||||
* @param {string} params.conversationId - The current conversation ID.
|
||||
* @param {string} params.messageId - The current message ID.
|
||||
* @returns {Promise<MongoFile & { messageId: string, toolCallId: string } | { filename: string; filepath: string; expiresAt: number; conversationId: string; toolCallId: string; messageId: string } | undefined>} The file metadata or undefined if an error occurs.
|
||||
|
|
@ -23,10 +29,11 @@ const processCodeOutput = async ({
|
|||
req,
|
||||
id,
|
||||
name,
|
||||
apiKey,
|
||||
toolCallId,
|
||||
conversationId,
|
||||
messageId,
|
||||
sessionId,
|
||||
session_id,
|
||||
}) => {
|
||||
const currentDate = new Date();
|
||||
const baseURL = getCodeBaseURL();
|
||||
|
|
@ -34,7 +41,7 @@ const processCodeOutput = async ({
|
|||
if (!fileExt || !imageExtRegex.test(name)) {
|
||||
return {
|
||||
filename: name,
|
||||
filepath: `/api/files/code/download/${sessionId}/${id}`,
|
||||
filepath: `/api/files/code/download/${session_id}/${id}`,
|
||||
/** Note: expires 24 hours after creation */
|
||||
expiresAt: currentDate.getTime() + 86400000,
|
||||
conversationId,
|
||||
|
|
@ -45,14 +52,13 @@ const processCodeOutput = async ({
|
|||
|
||||
try {
|
||||
const formattedDate = currentDate.toISOString();
|
||||
const result = await loadAuthValues({ userId: req.user.id, authFields: [EnvVar.CODE_API_KEY] });
|
||||
const response = await axios({
|
||||
method: 'get',
|
||||
url: `${baseURL}/download/${sessionId}/${id}`,
|
||||
url: `${baseURL}/download/${session_id}/${id}`,
|
||||
responseType: 'arraybuffer',
|
||||
headers: {
|
||||
'User-Agent': 'LibreChat/1.0',
|
||||
'X-API-Key': result[EnvVar.CODE_API_KEY],
|
||||
'X-API-Key': apiKey,
|
||||
},
|
||||
timeout: 15000,
|
||||
});
|
||||
|
|
@ -82,6 +88,120 @@ const processCodeOutput = async ({
|
|||
}
|
||||
};
|
||||
|
||||
function checkIfActive(dateString) {
|
||||
const givenDate = new Date(dateString);
|
||||
const currentDate = new Date();
|
||||
const timeDifference = currentDate - givenDate;
|
||||
const hoursPassed = timeDifference / (1000 * 60 * 60);
|
||||
return hoursPassed < 23;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the `lastModified` time string for a specified file from Code Execution Server.
|
||||
*
|
||||
* @param {Object} params - The parameters object.
|
||||
* @param {string} params.fileIdentifier - The identifier for the file (e.g., "session_id/fileId").
|
||||
* @param {string} params.apiKey - The API key for authentication.
|
||||
*
|
||||
* @returns {Promise<string|null>}
|
||||
* A promise that resolves to the `lastModified` time string of the file if successful, or null if there is an
|
||||
* error in initialization or fetching the info.
|
||||
*/
|
||||
async function getSessionInfo(fileIdentifier, apiKey) {
|
||||
try {
|
||||
const baseURL = getCodeBaseURL();
|
||||
const session_id = fileIdentifier.split('/')[0];
|
||||
const response = await axios({
|
||||
method: 'get',
|
||||
url: `${baseURL}/files/${session_id}`,
|
||||
params: {
|
||||
detail: 'summary',
|
||||
},
|
||||
headers: {
|
||||
'User-Agent': 'LibreChat/1.0',
|
||||
'X-API-Key': apiKey,
|
||||
},
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
return response.data.find((file) => file.name.startsWith(fileIdentifier))?.lastModified;
|
||||
} catch (error) {
|
||||
logger.error(`Error fetching session info: ${error.message}`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Object} options
|
||||
* @param {ServerRequest} options.req
|
||||
* @param {Agent['tool_resources']} options.tool_resources
|
||||
* @param {string} apiKey
|
||||
* @returns {Promise<Array<{ id: string; session_id: string; name: string }>>}
|
||||
*/
|
||||
const primeFiles = async (options, apiKey) => {
|
||||
const { tool_resources } = options;
|
||||
const file_ids = tool_resources?.[EToolResources.execute_code]?.file_ids ?? [];
|
||||
const dbFiles = await getFiles({ file_id: { $in: file_ids } });
|
||||
|
||||
const files = [];
|
||||
const sessions = new Map();
|
||||
for (const file of dbFiles) {
|
||||
if (file.metadata.fileIdentifier) {
|
||||
const [session_id, id] = file.metadata.fileIdentifier.split('/');
|
||||
const pushFile = () => {
|
||||
files.push({
|
||||
id,
|
||||
session_id,
|
||||
name: file.filename,
|
||||
});
|
||||
};
|
||||
if (sessions.has(session_id)) {
|
||||
pushFile();
|
||||
continue;
|
||||
}
|
||||
const reuploadFile = async () => {
|
||||
try {
|
||||
const { getDownloadStream } = getStrategyFunctions(file.source);
|
||||
const { handleFileUpload: uploadCodeEnvFile } = getStrategyFunctions(
|
||||
FileSources.execute_code,
|
||||
);
|
||||
const stream = await getDownloadStream(file.filepath);
|
||||
const fileIdentifier = await uploadCodeEnvFile({
|
||||
req: options.req,
|
||||
stream,
|
||||
filename: file.filename,
|
||||
apiKey,
|
||||
});
|
||||
await updateFile({ file_id: file.file_id, metadata: { fileIdentifier } });
|
||||
sessions.set(session_id, true);
|
||||
pushFile();
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Error re-uploading file ${id} in session ${session_id}: ${error.message}`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
};
|
||||
const uploadTime = await getSessionInfo(file.metadata.fileIdentifier, apiKey);
|
||||
if (!uploadTime) {
|
||||
logger.warn(`Failed to get upload time for file ${id} in session ${session_id}`);
|
||||
await reuploadFile();
|
||||
continue;
|
||||
}
|
||||
if (!checkIfActive(uploadTime)) {
|
||||
await reuploadFile();
|
||||
continue;
|
||||
}
|
||||
sessions.set(session_id, true);
|
||||
pushFile();
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
primeFiles,
|
||||
processCodeOutput,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ const fs = require('fs');
|
|||
const path = require('path');
|
||||
const axios = require('axios');
|
||||
const fetch = require('node-fetch');
|
||||
const { ref, uploadBytes, getDownloadURL, getStream, deleteObject } = require('firebase/storage');
|
||||
const { ref, uploadBytes, getDownloadURL, deleteObject } = require('firebase/storage');
|
||||
const { getBufferMetadata } = require('~/server/utils');
|
||||
const { getFirebaseStorage } = require('./initialize');
|
||||
const { logger } = require('~/config');
|
||||
|
|
@ -155,7 +155,7 @@ function extractFirebaseFilePath(urlString) {
|
|||
* Deletes a file from Firebase storage. This function determines the filepath from the
|
||||
* Firebase storage URL via regex for deletion. Validated by the user's ID.
|
||||
*
|
||||
* @param {Express.Request} req - The request object from Express.
|
||||
* @param {ServerRequest} req - The request object from Express.
|
||||
* It should contain a `user` object with an `id` property.
|
||||
* @param {MongoFile} file - The file object to be deleted.
|
||||
*
|
||||
|
|
@ -195,7 +195,7 @@ const deleteFirebaseFile = async (req, file) => {
|
|||
* Uploads a file to Firebase Storage.
|
||||
*
|
||||
* @param {Object} params - The params object.
|
||||
* @param {Express.Request} params.req - The request object from Express. It should have a `user` property with an `id`
|
||||
* @param {ServerRequest} params.req - The request object from Express. It should have a `user` property with an `id`
|
||||
* representing the user.
|
||||
* @param {Express.Multer.File} params.file - The file object, which is part of the request. The file object should
|
||||
* have a `path` property that points to the location of the uploaded file.
|
||||
|
|
@ -225,16 +225,22 @@ async function uploadFileToFirebase({ req, file, file_id }) {
|
|||
* Retrieves a readable stream for a file from Firebase storage.
|
||||
*
|
||||
* @param {string} filepath - The filepath.
|
||||
* @returns {ReadableStream} A readable stream of the file.
|
||||
* @returns {Promise<ReadableStream>} A readable stream of the file.
|
||||
*/
|
||||
function getFirebaseFileStream(filepath) {
|
||||
async function getFirebaseFileStream(filepath) {
|
||||
try {
|
||||
const storage = getFirebaseStorage();
|
||||
if (!storage) {
|
||||
throw new Error('Firebase is not initialized');
|
||||
}
|
||||
const fileRef = ref(storage, filepath);
|
||||
return getStream(fileRef);
|
||||
|
||||
const response = await axios({
|
||||
method: 'get',
|
||||
url: filepath,
|
||||
responseType: 'stream',
|
||||
});
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
logger.error('Error getting Firebase file stream:', error);
|
||||
throw error;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const axios = require('axios');
|
||||
const { EModelEndpoint } = require('librechat-data-provider');
|
||||
const { getBufferMetadata } = require('~/server/utils');
|
||||
const paths = require('~/config/paths');
|
||||
const { logger } = require('~/config');
|
||||
|
|
@ -222,6 +223,10 @@ const deleteLocalFile = async (req, file) => {
|
|||
|
||||
const parts = file.filepath.split(path.sep);
|
||||
const subfolder = parts[1];
|
||||
if (!subfolder && parts[0] === EModelEndpoint.agents) {
|
||||
logger.warn(`Agent File ${file.file_id} is missing filepath, may have been deleted already`);
|
||||
return;
|
||||
}
|
||||
const filepath = path.join(publicPath, file.filepath);
|
||||
|
||||
if (!isValidPath(req, publicPath, subfolder, filepath)) {
|
||||
|
|
@ -235,7 +240,7 @@ const deleteLocalFile = async (req, file) => {
|
|||
* Uploads a file to the specified upload directory.
|
||||
*
|
||||
* @param {Object} params - The params object.
|
||||
* @param {Object} params.req - The request object from Express. It should have a `user` property with an `id`
|
||||
* @param {ServerRequest} params.req - The request object from Express. It should have a `user` property with an `id`
|
||||
* representing the user, and an `app.locals.paths` object with an `uploads` path.
|
||||
* @param {Express.Multer.File} params.file - The file object, which is part of the request. The file object should
|
||||
* have a `path` property that points to the location of the uploaded file.
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ const { logger } = require('~/config');
|
|||
* Uploads a file that can be used across various OpenAI services.
|
||||
*
|
||||
* @param {Object} params - The params object.
|
||||
* @param {Express.Request} params.req - The request object from Express. It should have a `user` property with an `id`
|
||||
* @param {ServerRequest} params.req - The request object from Express. It should have a `user` property with an `id`
|
||||
* representing the user, and an `app.locals.paths` object with an `imageOutput` path.
|
||||
* @param {Express.Multer.File} params.file - The file uploaded to the server via multer.
|
||||
* @param {OpenAIClient} params.openai - The initialized OpenAI client.
|
||||
|
|
@ -42,7 +42,7 @@ async function uploadOpenAIFile({ req, file, openai }) {
|
|||
/**
|
||||
* Deletes a file previously uploaded to OpenAI.
|
||||
*
|
||||
* @param {Express.Request} req - The request object from Express.
|
||||
* @param {ServerRequest} req - The request object from Express.
|
||||
* @param {MongoFile} file - The database representation of the uploaded file.
|
||||
* @param {OpenAI} openai - The initialized OpenAI client.
|
||||
* @returns {Promise<void>}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ const { logger } = require('~/config');
|
|||
* Deletes a file from the vector database. This function takes a file object, constructs the full path, and
|
||||
* verifies the path's validity before deleting the file. If the path is invalid, an error is thrown.
|
||||
*
|
||||
* @param {Express.Request} req - The request object from Express. It should have an `app.locals.paths` object with
|
||||
* @param {ServerRequest} req - The request object from Express. It should have an `app.locals.paths` object with
|
||||
* a `publicPath` property.
|
||||
* @param {MongoFile} file - The file object to be deleted. It should have a `filepath` property that is
|
||||
* a string representing the path of the file relative to the publicPath.
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const mime = require('mime');
|
||||
const { v4 } = require('uuid');
|
||||
|
|
@ -12,14 +13,17 @@ const {
|
|||
mergeFileConfig,
|
||||
hostImageIdSuffix,
|
||||
checkOpenAIStorage,
|
||||
removeNullishValues,
|
||||
hostImageNamePrefix,
|
||||
isAssistantsEndpoint,
|
||||
} = require('librechat-data-provider');
|
||||
const { EnvVar } = require('@librechat/agents');
|
||||
const { addResourceFileId, deleteResourceFileId } = require('~/server/controllers/assistants/v2');
|
||||
const { convertImage, resizeAndConvert } = require('~/server/services/Files/images');
|
||||
const { addAgentResourceFile, removeAgentResourceFile } = require('~/models/Agent');
|
||||
const { getOpenAIClient } = require('~/server/controllers/assistants/helpers');
|
||||
const { createFile, updateFileUsage, deleteFiles } = require('~/models/File');
|
||||
const { loadAuthValues } = require('~/app/clients/tools/util');
|
||||
const { LB_QueueAsyncCall } = require('~/server/utils/queue');
|
||||
const { getStrategyFunctions } = require('./strategies');
|
||||
const { determineFileType } = require('~/server/utils');
|
||||
|
|
@ -89,6 +93,7 @@ function enqueueDeleteOperation({ req, file, deleteFile, promises, resolvedFileI
|
|||
* @param {MongoFile[]} params.files - The file objects to delete.
|
||||
* @param {Express.Request} params.req - The express request object.
|
||||
* @param {DeleteFilesBody} params.req.body - The request body.
|
||||
* @param {string} [params.req.body.agent_id] - The agent ID if file uploaded is associated to an agent.
|
||||
* @param {string} [params.req.body.assistant_id] - The assistant ID if file uploaded is associated to an assistant.
|
||||
* @param {string} [params.req.body.tool_resource] - The tool resource if assistant file uploaded is associated to a tool resource.
|
||||
*
|
||||
|
|
@ -438,10 +443,25 @@ const processAgentFileUpload = async ({ req, res, file, metadata }) => {
|
|||
throw new Error('No agent ID provided for agent file upload');
|
||||
}
|
||||
|
||||
let fileInfoMetadata;
|
||||
if (tool_resource === EToolResources.execute_code) {
|
||||
const { handleFileUpload: uploadCodeEnvFile } = getStrategyFunctions(FileSources.execute_code);
|
||||
const result = await loadAuthValues({ userId: req.user.id, authFields: [EnvVar.CODE_API_KEY] });
|
||||
const stream = fs.createReadStream(file.path);
|
||||
const fileIdentifier = await uploadCodeEnvFile({
|
||||
req,
|
||||
stream,
|
||||
filename: file.originalname,
|
||||
apiKey: result[EnvVar.CODE_API_KEY],
|
||||
});
|
||||
fileInfoMetadata = { fileIdentifier };
|
||||
}
|
||||
|
||||
const source =
|
||||
tool_resource === EToolResources.file_search
|
||||
? FileSources.vectordb
|
||||
: req.app.locals.fileStrategy;
|
||||
|
||||
const { handleFileUpload } = getStrategyFunctions(source);
|
||||
const { file_id, temp_file_id } = metadata;
|
||||
|
||||
|
|
@ -463,9 +483,9 @@ const processAgentFileUpload = async ({ req, res, file, metadata }) => {
|
|||
if (!messageAttachment && tool_resource) {
|
||||
await addAgentResourceFile({
|
||||
req,
|
||||
agent_id,
|
||||
file_id,
|
||||
tool_resource: tool_resource,
|
||||
agent_id,
|
||||
tool_resource,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -479,24 +499,24 @@ const processAgentFileUpload = async ({ req, res, file, metadata }) => {
|
|||
filepath = result.filepath;
|
||||
}
|
||||
|
||||
const result = await createFile(
|
||||
{
|
||||
user: req.user.id,
|
||||
file_id,
|
||||
temp_file_id,
|
||||
bytes,
|
||||
filepath,
|
||||
filename: filename ?? file.originalname,
|
||||
context: messageAttachment ? FileContext.message_attachment : FileContext.agents,
|
||||
model: messageAttachment ? undefined : req.body.model,
|
||||
type: file.mimetype,
|
||||
embedded,
|
||||
source,
|
||||
height,
|
||||
width,
|
||||
},
|
||||
true,
|
||||
);
|
||||
const fileInfo = removeNullishValues({
|
||||
user: req.user.id,
|
||||
file_id,
|
||||
temp_file_id,
|
||||
bytes,
|
||||
filepath,
|
||||
filename: filename ?? file.originalname,
|
||||
context: messageAttachment ? FileContext.message_attachment : FileContext.agents,
|
||||
model: messageAttachment ? undefined : req.body.model,
|
||||
metadata: fileInfoMetadata,
|
||||
type: file.mimetype,
|
||||
embedded,
|
||||
source,
|
||||
height,
|
||||
width,
|
||||
});
|
||||
|
||||
const result = await createFile(fileInfo, true);
|
||||
res.status(200).json({ message: 'Agent file uploaded and processed successfully', ...result });
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -5,11 +5,13 @@ const {
|
|||
saveURLToFirebase,
|
||||
deleteFirebaseFile,
|
||||
saveBufferToFirebase,
|
||||
uploadFileToFirebase,
|
||||
uploadImageToFirebase,
|
||||
processFirebaseAvatar,
|
||||
getFirebaseFileStream,
|
||||
} = require('./Firebase');
|
||||
const {
|
||||
uploadLocalFile,
|
||||
getLocalFileURL,
|
||||
saveFileFromURL,
|
||||
saveLocalBuffer,
|
||||
|
|
@ -20,17 +22,15 @@ const {
|
|||
getLocalFileStream,
|
||||
} = require('./Local');
|
||||
const { uploadOpenAIFile, deleteOpenAIFile, getOpenAIFileStream } = require('./OpenAI');
|
||||
const { getCodeOutputDownloadStream, uploadCodeEnvFile } = require('./Code');
|
||||
const { uploadVectors, deleteVectors } = require('./VectorDB');
|
||||
const { getCodeOutputDownloadStream } = require('./Code');
|
||||
|
||||
/**
|
||||
* Firebase Storage Strategy Functions
|
||||
*
|
||||
* */
|
||||
const firebaseStrategy = () => ({
|
||||
// saveFile:
|
||||
/** @type {typeof uploadVectors | null} */
|
||||
handleFileUpload: null,
|
||||
handleFileUpload: uploadFileToFirebase,
|
||||
saveURL: saveURLToFirebase,
|
||||
getFileURL: getFirebaseURL,
|
||||
deleteFile: deleteFirebaseFile,
|
||||
|
|
@ -46,8 +46,7 @@ const firebaseStrategy = () => ({
|
|||
*
|
||||
* */
|
||||
const localStrategy = () => ({
|
||||
/** @type {typeof uploadVectors | null} */
|
||||
handleFileUpload: null,
|
||||
handleFileUpload: uploadLocalFile,
|
||||
saveURL: saveFileFromURL,
|
||||
getFileURL: getLocalFileURL,
|
||||
saveBuffer: saveLocalBuffer,
|
||||
|
|
@ -124,8 +123,7 @@ const codeOutputStrategy = () => ({
|
|||
prepareImagePayload: null,
|
||||
/** @type {typeof deleteLocalFile | null} */
|
||||
deleteFile: null,
|
||||
/** @type {typeof uploadVectors | null} */
|
||||
handleFileUpload: null,
|
||||
handleFileUpload: uploadCodeEnvFile,
|
||||
getDownloadStream: getCodeOutputDownloadStream,
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue