mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-22 08:12:00 +02: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
|
@ -24,6 +24,7 @@ const {
|
||||||
StructuredWolfram,
|
StructuredWolfram,
|
||||||
TavilySearchResults,
|
TavilySearchResults,
|
||||||
} = require('../');
|
} = require('../');
|
||||||
|
const { primeFiles } = require('~/server/services/Files/Code/process');
|
||||||
const createFileSearchTool = require('./createFileSearchTool');
|
const createFileSearchTool = require('./createFileSearchTool');
|
||||||
const { loadToolSuite } = require('./loadToolSuite');
|
const { loadToolSuite } = require('./loadToolSuite');
|
||||||
const { loadSpecs } = require('./loadSpecs');
|
const { loadSpecs } = require('./loadSpecs');
|
||||||
|
@ -255,12 +256,14 @@ const loadTools = async ({
|
||||||
for (const tool of tools) {
|
for (const tool of tools) {
|
||||||
if (tool === Tools.execute_code) {
|
if (tool === Tools.execute_code) {
|
||||||
const authValues = await loadAuthValues({
|
const authValues = await loadAuthValues({
|
||||||
userId: user.id,
|
userId: user,
|
||||||
authFields: [EnvVar.CODE_API_KEY],
|
authFields: [EnvVar.CODE_API_KEY],
|
||||||
});
|
});
|
||||||
|
const files = await primeFiles(options, authValues[EnvVar.CODE_API_KEY]);
|
||||||
requestedTools[tool] = () =>
|
requestedTools[tool] = () =>
|
||||||
createCodeExecutionTool({
|
createCodeExecutionTool({
|
||||||
user_id: user.id,
|
user_id: user,
|
||||||
|
files,
|
||||||
...authValues,
|
...authValues,
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
|
|
|
@ -21,6 +21,8 @@ const mongoose = require('mongoose');
|
||||||
* @property {string} [source] - The source of the file (e.g., from FileSources)
|
* @property {string} [source] - The source of the file (e.g., from FileSources)
|
||||||
* @property {number} [width] - Optional width of the file
|
* @property {number} [width] - Optional width of the file
|
||||||
* @property {number} [height] - Optional height of the file
|
* @property {number} [height] - Optional height of the file
|
||||||
|
* @property {Object} [metadata] - Metadata related to the file
|
||||||
|
* @property {string} [metadata.fileIdentifier] - Unique identifier for the file in metadata
|
||||||
* @property {Date} [expiresAt] - Optional expiration date of the file
|
* @property {Date} [expiresAt] - Optional expiration date of the file
|
||||||
* @property {Date} [createdAt] - Date when the file was created
|
* @property {Date} [createdAt] - Date when the file was created
|
||||||
* @property {Date} [updatedAt] - Date when the file was updated
|
* @property {Date} [updatedAt] - Date when the file was updated
|
||||||
|
@ -91,6 +93,9 @@ const fileSchema = mongoose.Schema(
|
||||||
},
|
},
|
||||||
width: Number,
|
width: Number,
|
||||||
height: Number,
|
height: Number,
|
||||||
|
metadata: {
|
||||||
|
fileIdentifier: String,
|
||||||
|
},
|
||||||
expiresAt: {
|
expiresAt: {
|
||||||
type: Date,
|
type: Date,
|
||||||
expires: 3600, // 1 hour in seconds
|
expires: 3600, // 1 hour in seconds
|
||||||
|
|
|
@ -10,6 +10,13 @@ const bedrockValues = {
|
||||||
'llama3-1-8b': { prompt: 0.3, completion: 0.6 },
|
'llama3-1-8b': { prompt: 0.3, completion: 0.6 },
|
||||||
'llama3-1-70b': { prompt: 2.65, completion: 3.5 },
|
'llama3-1-70b': { prompt: 2.65, completion: 3.5 },
|
||||||
'llama3-1-405b': { prompt: 5.32, completion: 16.0 },
|
'llama3-1-405b': { prompt: 5.32, completion: 16.0 },
|
||||||
|
'llama2:13b': { prompt: 0.75, completion: 1.0 },
|
||||||
|
'llama2:70b': { prompt: 1.95, completion: 2.56 },
|
||||||
|
'llama3:8b': { prompt: 0.3, completion: 0.6 },
|
||||||
|
'llama3:70b': { prompt: 2.65, completion: 3.5 },
|
||||||
|
'llama3.1:8b': { prompt: 0.3, completion: 0.6 },
|
||||||
|
'llama3.1:70b': { prompt: 2.65, completion: 3.5 },
|
||||||
|
'llama3.1:405b': { prompt: 5.32, completion: 16.0 },
|
||||||
'mistral-7b': { prompt: 0.15, completion: 0.2 },
|
'mistral-7b': { prompt: 0.15, completion: 0.2 },
|
||||||
'mistral-small': { prompt: 0.15, completion: 0.2 },
|
'mistral-small': { prompt: 0.15, completion: 0.2 },
|
||||||
'mixtral-8x7b': { prompt: 0.45, completion: 0.7 },
|
'mixtral-8x7b': { prompt: 0.45, completion: 0.7 },
|
||||||
|
|
|
@ -43,7 +43,7 @@
|
||||||
"@langchain/core": "^0.2.18",
|
"@langchain/core": "^0.2.18",
|
||||||
"@langchain/google-genai": "^0.0.11",
|
"@langchain/google-genai": "^0.0.11",
|
||||||
"@langchain/google-vertexai": "^0.0.17",
|
"@langchain/google-vertexai": "^0.0.17",
|
||||||
"@librechat/agents": "^1.6.9",
|
"@librechat/agents": "^1.7.7",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"cheerio": "^1.0.0-rc.12",
|
"cheerio": "^1.0.0-rc.12",
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
const { promises: fs } = require('fs');
|
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 { addOpenAPISpecs } = require('~/app/clients/tools/util/addOpenAPISpecs');
|
||||||
const { getLogStores } = require('~/cache');
|
const { getLogStores } = require('~/cache');
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ const filterUniquePlugins = (plugins) => {
|
||||||
* @param {TPlugin} plugin The plugin object containing the authentication configuration.
|
* @param {TPlugin} plugin The plugin object containing the authentication configuration.
|
||||||
* @returns {boolean} True if the plugin is authenticated for all required fields, false otherwise.
|
* @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) {
|
if (!plugin.authConfig || plugin.authConfig.length === 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,7 @@ const isPluginAuthenticated = (plugin) => {
|
||||||
|
|
||||||
for (const fieldOption of authFieldOptions) {
|
for (const fieldOption of authFieldOptions) {
|
||||||
const envValue = process.env[fieldOption];
|
const envValue = process.env[fieldOption];
|
||||||
if (envValue && envValue.trim() !== '' && envValue !== 'user_provided') {
|
if (envValue && envValue.trim() !== '' && envValue !== AuthType.USER_PROVIDED) {
|
||||||
isFieldAuthenticated = true;
|
isFieldAuthenticated = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -64,7 +64,7 @@ const getAvailablePluginsController = async (req, res) => {
|
||||||
let authenticatedPlugins = [];
|
let authenticatedPlugins = [];
|
||||||
for (const plugin of uniquePlugins) {
|
for (const plugin of uniquePlugins) {
|
||||||
authenticatedPlugins.push(
|
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 uniquePlugins = filterUniquePlugins(jsonData);
|
||||||
|
|
||||||
const authenticatedPlugins = uniquePlugins.map((plugin) => {
|
const authenticatedPlugins = uniquePlugins.map((plugin) => {
|
||||||
if (isPluginAuthenticated(plugin)) {
|
if (checkPluginAuth(plugin)) {
|
||||||
return { ...plugin, authenticated: true };
|
return { ...plugin, authenticated: true };
|
||||||
} else {
|
} else {
|
||||||
return plugin;
|
return plugin;
|
||||||
|
|
|
@ -61,10 +61,10 @@ const deleteUserFiles = async (req) => {
|
||||||
|
|
||||||
const updateUserPluginsController = async (req, res) => {
|
const updateUserPluginsController = async (req, res) => {
|
||||||
const { user } = req;
|
const { user } = req;
|
||||||
const { pluginKey, action, auth, isAssistantTool } = req.body;
|
const { pluginKey, action, auth, isEntityTool } = req.body;
|
||||||
let authService;
|
let authService;
|
||||||
try {
|
try {
|
||||||
if (!isAssistantTool) {
|
if (!isEntityTool) {
|
||||||
const userPluginsService = await updateUserPluginsService(user, pluginKey, action);
|
const userPluginsService = await updateUserPluginsService(user, pluginKey, action);
|
||||||
|
|
||||||
if (userPluginsService instanceof Error) {
|
if (userPluginsService instanceof Error) {
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
const { Tools } = require('librechat-data-provider');
|
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 { processCodeOutput } = require('~/server/services/Files/Code/process');
|
||||||
|
const { loadAuthValues } = require('~/app/clients/tools/util');
|
||||||
const { logger } = require('~/config');
|
const { logger } = require('~/config');
|
||||||
|
|
||||||
/** @typedef {import('@librechat/agents').Graph} Graph */
|
/** @typedef {import('@librechat/agents').Graph} Graph */
|
||||||
|
@ -158,13 +164,18 @@ function createToolEndCallback({ req, res, artifactPromises }) {
|
||||||
const { id, name } = file;
|
const { id, name } = file;
|
||||||
artifactPromises.push(
|
artifactPromises.push(
|
||||||
(async () => {
|
(async () => {
|
||||||
|
const result = await loadAuthValues({
|
||||||
|
userId: req.user.id,
|
||||||
|
authFields: [EnvVar.CODE_API_KEY],
|
||||||
|
});
|
||||||
const fileMetadata = await processCodeOutput({
|
const fileMetadata = await processCodeOutput({
|
||||||
req,
|
req,
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
|
apiKey: result[EnvVar.CODE_API_KEY],
|
||||||
toolCallId: tool_call_id,
|
toolCallId: tool_call_id,
|
||||||
messageId: metadata.run_id,
|
messageId: metadata.run_id,
|
||||||
sessionId: artifact.session_id,
|
session_id: artifact.session_id,
|
||||||
conversationId: metadata.thread_id,
|
conversationId: metadata.thread_id,
|
||||||
});
|
});
|
||||||
if (!res.headersSent) {
|
if (!res.headersSent) {
|
||||||
|
|
|
@ -15,7 +15,6 @@ const {
|
||||||
EModelEndpoint,
|
EModelEndpoint,
|
||||||
anthropicSchema,
|
anthropicSchema,
|
||||||
bedrockOutputParser,
|
bedrockOutputParser,
|
||||||
providerEndpointMap,
|
|
||||||
removeNullishValues,
|
removeNullishValues,
|
||||||
} = require('librechat-data-provider');
|
} = require('librechat-data-provider');
|
||||||
const {
|
const {
|
||||||
|
@ -465,7 +464,6 @@ class AgentClient extends BaseClient {
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
configurable: {
|
configurable: {
|
||||||
provider: providerEndpointMap[this.options.agent.provider],
|
|
||||||
thread_id: this.conversationId,
|
thread_id: this.conversationId,
|
||||||
},
|
},
|
||||||
signal: abortController.signal,
|
signal: abortController.signal,
|
||||||
|
|
|
@ -35,9 +35,10 @@ async function createRun({
|
||||||
streaming = true,
|
streaming = true,
|
||||||
streamUsage = true,
|
streamUsage = true,
|
||||||
}) {
|
}) {
|
||||||
|
const provider = providerEndpointMap[agent.provider] ?? agent.provider;
|
||||||
const llmConfig = Object.assign(
|
const llmConfig = Object.assign(
|
||||||
{
|
{
|
||||||
provider: providerEndpointMap[agent.provider],
|
provider,
|
||||||
streaming,
|
streaming,
|
||||||
streamUsage,
|
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 express = require('express');
|
||||||
const { PermissionTypes, Permissions } = require('librechat-data-provider');
|
const { PermissionTypes, Permissions } = require('librechat-data-provider');
|
||||||
const { requireJwtAuth, generateCheckAccess } = require('~/server/middleware');
|
const { requireJwtAuth, generateCheckAccess } = require('~/server/middleware');
|
||||||
const { getAvailableTools } = require('~/server/controllers/PluginController');
|
|
||||||
const v1 = require('~/server/controllers/agents/v1');
|
const v1 = require('~/server/controllers/agents/v1');
|
||||||
const actions = require('./actions');
|
const actions = require('./actions');
|
||||||
|
const tools = require('./tools');
|
||||||
|
|
||||||
const upload = multer();
|
const upload = multer();
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
@ -35,9 +35,8 @@ router.use('/actions', actions);
|
||||||
/**
|
/**
|
||||||
* Get a list of available tools for agents.
|
* Get a list of available tools for agents.
|
||||||
* @route GET /agents/tools
|
* @route GET /agents/tools
|
||||||
* @returns {TPlugin[]} 200 - application/json
|
|
||||||
*/
|
*/
|
||||||
router.use('/tools', getAvailableTools);
|
router.use('/tools', tools);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an agent.
|
* Creates an agent.
|
||||||
|
|
|
@ -17,6 +17,7 @@ const {
|
||||||
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
|
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
|
||||||
const { getOpenAIClient } = require('~/server/controllers/assistants/helpers');
|
const { getOpenAIClient } = require('~/server/controllers/assistants/helpers');
|
||||||
const { loadAuthValues } = require('~/app/clients/tools/util');
|
const { loadAuthValues } = require('~/app/clients/tools/util');
|
||||||
|
const { getAgent } = require('~/models/Agent');
|
||||||
const { getFiles } = require('~/models/File');
|
const { getFiles } = require('~/models/File');
|
||||||
const { logger } = require('~/config');
|
const { logger } = require('~/config');
|
||||||
|
|
||||||
|
@ -67,12 +68,31 @@ router.delete('/', async (req, res) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileIds = files.map((file) => file.file_id);
|
const fileIds = files.map((file) => file.file_id);
|
||||||
const userFiles = await getFiles({ file_id: { $in: fileIds }, user: req.user.id });
|
const dbFiles = await getFiles({ file_id: { $in: fileIds } });
|
||||||
if (userFiles.length !== files.length) {
|
const unauthorizedFiles = dbFiles.filter((file) => file.user.toString() !== req.user.id);
|
||||||
return res.status(403).json({ message: 'You can only delete your own files' });
|
|
||||||
|
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(
|
logger.debug(
|
||||||
`[/files] Files deleted successfully: ${files
|
`[/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 {
|
try {
|
||||||
const { sessionId, fileId } = req.params;
|
const { session_id, fileId } = req.params;
|
||||||
const logPrefix = `Session ID: ${sessionId} | File ID: ${fileId} | Code output download requested by user `;
|
const logPrefix = `Session ID: ${session_id} | File ID: ${fileId} | Code output download requested by user `;
|
||||||
logger.debug(logPrefix);
|
logger.debug(logPrefix);
|
||||||
|
|
||||||
if (!sessionId || !fileId) {
|
if (!session_id || !fileId) {
|
||||||
return res.status(400).send('Bad request');
|
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] });
|
const result = await loadAuthValues({ userId: req.user.id, authFields: [EnvVar.CODE_API_KEY] });
|
||||||
|
|
||||||
/** @type {AxiosResponse<ReadableStream> | undefined} */
|
/** @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);
|
res.set(response.headers);
|
||||||
response.data.pipe(res);
|
response.data.pipe(res);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
|
|
||||||
const { z } = require('zod');
|
const { z } = require('zod');
|
||||||
const { tool } = require('@langchain/core/tools');
|
const { tool } = require('@langchain/core/tools');
|
||||||
const { createContentAggregator } = require('@librechat/agents');
|
const { createContentAggregator, Providers } = require('@librechat/agents');
|
||||||
const {
|
const {
|
||||||
EModelEndpoint,
|
EModelEndpoint,
|
||||||
getResponseSender,
|
getResponseSender,
|
||||||
|
@ -22,8 +22,9 @@ const {
|
||||||
createToolEndCallback,
|
createToolEndCallback,
|
||||||
} = require('~/server/controllers/agents/callbacks');
|
} = require('~/server/controllers/agents/callbacks');
|
||||||
const initAnthropic = require('~/server/services/Endpoints/anthropic/initialize');
|
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 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 { loadAgentTools } = require('~/server/services/ToolService');
|
||||||
const AgentClient = require('~/server/controllers/agents/client');
|
const AgentClient = require('~/server/controllers/agents/client');
|
||||||
const { getModelMaxTokens } = require('~/utils');
|
const { getModelMaxTokens } = require('~/utils');
|
||||||
|
@ -53,6 +54,7 @@ const providerConfigMap = {
|
||||||
[EModelEndpoint.azureOpenAI]: initOpenAI,
|
[EModelEndpoint.azureOpenAI]: initOpenAI,
|
||||||
[EModelEndpoint.anthropic]: initAnthropic,
|
[EModelEndpoint.anthropic]: initAnthropic,
|
||||||
[EModelEndpoint.bedrock]: getBedrockOptions,
|
[EModelEndpoint.bedrock]: getBedrockOptions,
|
||||||
|
[Providers.OLLAMA]: initCustom,
|
||||||
};
|
};
|
||||||
|
|
||||||
const initializeClient = async ({ req, res, endpointOption }) => {
|
const initializeClient = async ({ req, res, endpointOption }) => {
|
||||||
|
@ -92,7 +94,7 @@ const initializeClient = async ({ req, res, endpointOption }) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
let modelOptions = { model: agent.model };
|
let modelOptions = { model: agent.model };
|
||||||
const getOptions = providerConfigMap[agent.provider];
|
let getOptions = providerConfigMap[agent.provider];
|
||||||
if (!getOptions) {
|
if (!getOptions) {
|
||||||
throw new Error(`Provider ${agent.provider} not supported`);
|
throw new Error(`Provider ${agent.provider} not supported`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,11 +12,14 @@ const { fetchModels } = require('~/server/services/ModelService');
|
||||||
const getLogStores = require('~/cache/getLogStores');
|
const getLogStores = require('~/cache/getLogStores');
|
||||||
const { isUserProvided } = require('~/server/utils');
|
const { isUserProvided } = require('~/server/utils');
|
||||||
const { OpenAIClient } = require('~/app');
|
const { OpenAIClient } = require('~/app');
|
||||||
|
const { Providers } = require('@librechat/agents');
|
||||||
|
|
||||||
const { PROXY } = process.env;
|
const { PROXY } = process.env;
|
||||||
|
|
||||||
const initializeClient = async ({ req, res, endpointOption }) => {
|
const initializeClient = async ({ req, res, endpointOption, optionsOnly, overrideEndpoint }) => {
|
||||||
const { key: expiresAt, endpoint } = req.body;
|
const { key: expiresAt } = req.body;
|
||||||
|
const endpoint = overrideEndpoint ?? req.body.endpoint;
|
||||||
|
|
||||||
const customConfig = await getCustomConfig();
|
const customConfig = await getCustomConfig();
|
||||||
if (!customConfig) {
|
if (!customConfig) {
|
||||||
throw new Error(`Config not found for the ${endpoint} custom endpoint.`);
|
throw new Error(`Config not found for the ${endpoint} custom endpoint.`);
|
||||||
|
@ -133,6 +136,17 @@ const initializeClient = async ({ req, res, endpointOption }) => {
|
||||||
...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);
|
const client = new OpenAIClient(apiKey, clientOptions);
|
||||||
return {
|
return {
|
||||||
client,
|
client,
|
||||||
|
|
|
@ -1,19 +1,20 @@
|
||||||
// downloadStream.js
|
// Code Files
|
||||||
|
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
|
const FormData = require('form-data');
|
||||||
const { getCodeBaseURL } = require('@librechat/agents');
|
const { getCodeBaseURL } = require('@librechat/agents');
|
||||||
|
|
||||||
const baseURL = getCodeBaseURL();
|
const MAX_FILE_SIZE = 25 * 1024 * 1024;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves a download stream for a specified file.
|
* 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.
|
* @param {string} apiKey - The API key for authentication.
|
||||||
* @returns {Promise<AxiosResponse>} A promise that resolves to a readable stream of the file content.
|
* @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.
|
* @throws {Error} If there's an error during the download process.
|
||||||
*/
|
*/
|
||||||
async function getCodeOutputDownloadStream(fileIdentifier, apiKey) {
|
async function getCodeOutputDownloadStream(fileIdentifier, apiKey) {
|
||||||
try {
|
try {
|
||||||
|
const baseURL = getCodeBaseURL();
|
||||||
const response = await axios({
|
const response = await axios({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
url: `${baseURL}/download/${fileIdentifier}`,
|
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 path = require('path');
|
||||||
const { v4 } = require('uuid');
|
const { v4 } = require('uuid');
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
const { getCodeBaseURL, EnvVar } = require('@librechat/agents');
|
const { getCodeBaseURL } = require('@librechat/agents');
|
||||||
const { FileContext, imageExtRegex } = require('librechat-data-provider');
|
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 { convertImage } = require('~/server/services/Files/images/convert');
|
||||||
const { loadAuthValues } = require('~/app/clients/tools/util');
|
const { createFile, getFiles, updateFile } = require('~/models/File');
|
||||||
const { createFile } = require('~/models/File');
|
|
||||||
const { logger } = require('~/config');
|
const { logger } = require('~/config');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -13,8 +18,9 @@ const { logger } = require('~/config');
|
||||||
* @param {ServerRequest} params.req - The Express request object.
|
* @param {ServerRequest} params.req - The Express request object.
|
||||||
* @param {string} params.id - The file ID.
|
* @param {string} params.id - The file ID.
|
||||||
* @param {string} params.name - The filename.
|
* @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.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.conversationId - The current conversation ID.
|
||||||
* @param {string} params.messageId - The current message 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.
|
* @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,
|
req,
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
|
apiKey,
|
||||||
toolCallId,
|
toolCallId,
|
||||||
conversationId,
|
conversationId,
|
||||||
messageId,
|
messageId,
|
||||||
sessionId,
|
session_id,
|
||||||
}) => {
|
}) => {
|
||||||
const currentDate = new Date();
|
const currentDate = new Date();
|
||||||
const baseURL = getCodeBaseURL();
|
const baseURL = getCodeBaseURL();
|
||||||
|
@ -34,7 +41,7 @@ const processCodeOutput = async ({
|
||||||
if (!fileExt || !imageExtRegex.test(name)) {
|
if (!fileExt || !imageExtRegex.test(name)) {
|
||||||
return {
|
return {
|
||||||
filename: name,
|
filename: name,
|
||||||
filepath: `/api/files/code/download/${sessionId}/${id}`,
|
filepath: `/api/files/code/download/${session_id}/${id}`,
|
||||||
/** Note: expires 24 hours after creation */
|
/** Note: expires 24 hours after creation */
|
||||||
expiresAt: currentDate.getTime() + 86400000,
|
expiresAt: currentDate.getTime() + 86400000,
|
||||||
conversationId,
|
conversationId,
|
||||||
|
@ -45,14 +52,13 @@ const processCodeOutput = async ({
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const formattedDate = currentDate.toISOString();
|
const formattedDate = currentDate.toISOString();
|
||||||
const result = await loadAuthValues({ userId: req.user.id, authFields: [EnvVar.CODE_API_KEY] });
|
|
||||||
const response = await axios({
|
const response = await axios({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
url: `${baseURL}/download/${sessionId}/${id}`,
|
url: `${baseURL}/download/${session_id}/${id}`,
|
||||||
responseType: 'arraybuffer',
|
responseType: 'arraybuffer',
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent': 'LibreChat/1.0',
|
'User-Agent': 'LibreChat/1.0',
|
||||||
'X-API-Key': result[EnvVar.CODE_API_KEY],
|
'X-API-Key': apiKey,
|
||||||
},
|
},
|
||||||
timeout: 15000,
|
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 = {
|
module.exports = {
|
||||||
|
primeFiles,
|
||||||
processCodeOutput,
|
processCodeOutput,
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,7 +2,7 @@ const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
const fetch = require('node-fetch');
|
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 { getBufferMetadata } = require('~/server/utils');
|
||||||
const { getFirebaseStorage } = require('./initialize');
|
const { getFirebaseStorage } = require('./initialize');
|
||||||
const { logger } = require('~/config');
|
const { logger } = require('~/config');
|
||||||
|
@ -155,7 +155,7 @@ function extractFirebaseFilePath(urlString) {
|
||||||
* Deletes a file from Firebase storage. This function determines the filepath from the
|
* 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.
|
* 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.
|
* It should contain a `user` object with an `id` property.
|
||||||
* @param {MongoFile} file - The file object to be deleted.
|
* @param {MongoFile} file - The file object to be deleted.
|
||||||
*
|
*
|
||||||
|
@ -195,7 +195,7 @@ const deleteFirebaseFile = async (req, file) => {
|
||||||
* Uploads a file to Firebase Storage.
|
* Uploads a file to Firebase Storage.
|
||||||
*
|
*
|
||||||
* @param {Object} params - The params object.
|
* @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.
|
* representing the user.
|
||||||
* @param {Express.Multer.File} params.file - The file object, which is part of the request. The file object should
|
* @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.
|
* 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.
|
* Retrieves a readable stream for a file from Firebase storage.
|
||||||
*
|
*
|
||||||
* @param {string} filepath - The filepath.
|
* @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 {
|
try {
|
||||||
const storage = getFirebaseStorage();
|
const storage = getFirebaseStorage();
|
||||||
if (!storage) {
|
if (!storage) {
|
||||||
throw new Error('Firebase is not initialized');
|
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) {
|
} catch (error) {
|
||||||
logger.error('Error getting Firebase file stream:', error);
|
logger.error('Error getting Firebase file stream:', error);
|
||||||
throw error;
|
throw error;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
|
const { EModelEndpoint } = require('librechat-data-provider');
|
||||||
const { getBufferMetadata } = require('~/server/utils');
|
const { getBufferMetadata } = require('~/server/utils');
|
||||||
const paths = require('~/config/paths');
|
const paths = require('~/config/paths');
|
||||||
const { logger } = require('~/config');
|
const { logger } = require('~/config');
|
||||||
|
@ -222,6 +223,10 @@ const deleteLocalFile = async (req, file) => {
|
||||||
|
|
||||||
const parts = file.filepath.split(path.sep);
|
const parts = file.filepath.split(path.sep);
|
||||||
const subfolder = parts[1];
|
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);
|
const filepath = path.join(publicPath, file.filepath);
|
||||||
|
|
||||||
if (!isValidPath(req, publicPath, subfolder, filepath)) {
|
if (!isValidPath(req, publicPath, subfolder, filepath)) {
|
||||||
|
@ -235,7 +240,7 @@ const deleteLocalFile = async (req, file) => {
|
||||||
* Uploads a file to the specified upload directory.
|
* Uploads a file to the specified upload directory.
|
||||||
*
|
*
|
||||||
* @param {Object} params - The params object.
|
* @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.
|
* 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
|
* @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.
|
* 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.
|
* Uploads a file that can be used across various OpenAI services.
|
||||||
*
|
*
|
||||||
* @param {Object} params - The params object.
|
* @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.
|
* 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 {Express.Multer.File} params.file - The file uploaded to the server via multer.
|
||||||
* @param {OpenAIClient} params.openai - The initialized OpenAI client.
|
* @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.
|
* 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 {MongoFile} file - The database representation of the uploaded file.
|
||||||
* @param {OpenAI} openai - The initialized OpenAI client.
|
* @param {OpenAI} openai - The initialized OpenAI client.
|
||||||
* @returns {Promise<void>}
|
* @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
|
* 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.
|
* 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.
|
* a `publicPath` property.
|
||||||
* @param {MongoFile} file - The file object to be deleted. It should have a `filepath` property that is
|
* @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.
|
* 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 path = require('path');
|
||||||
const mime = require('mime');
|
const mime = require('mime');
|
||||||
const { v4 } = require('uuid');
|
const { v4 } = require('uuid');
|
||||||
|
@ -12,14 +13,17 @@ const {
|
||||||
mergeFileConfig,
|
mergeFileConfig,
|
||||||
hostImageIdSuffix,
|
hostImageIdSuffix,
|
||||||
checkOpenAIStorage,
|
checkOpenAIStorage,
|
||||||
|
removeNullishValues,
|
||||||
hostImageNamePrefix,
|
hostImageNamePrefix,
|
||||||
isAssistantsEndpoint,
|
isAssistantsEndpoint,
|
||||||
} = require('librechat-data-provider');
|
} = require('librechat-data-provider');
|
||||||
|
const { EnvVar } = require('@librechat/agents');
|
||||||
const { addResourceFileId, deleteResourceFileId } = require('~/server/controllers/assistants/v2');
|
const { addResourceFileId, deleteResourceFileId } = require('~/server/controllers/assistants/v2');
|
||||||
const { convertImage, resizeAndConvert } = require('~/server/services/Files/images');
|
const { convertImage, resizeAndConvert } = require('~/server/services/Files/images');
|
||||||
const { addAgentResourceFile, removeAgentResourceFile } = require('~/models/Agent');
|
const { addAgentResourceFile, removeAgentResourceFile } = require('~/models/Agent');
|
||||||
const { getOpenAIClient } = require('~/server/controllers/assistants/helpers');
|
const { getOpenAIClient } = require('~/server/controllers/assistants/helpers');
|
||||||
const { createFile, updateFileUsage, deleteFiles } = require('~/models/File');
|
const { createFile, updateFileUsage, deleteFiles } = require('~/models/File');
|
||||||
|
const { loadAuthValues } = require('~/app/clients/tools/util');
|
||||||
const { LB_QueueAsyncCall } = require('~/server/utils/queue');
|
const { LB_QueueAsyncCall } = require('~/server/utils/queue');
|
||||||
const { getStrategyFunctions } = require('./strategies');
|
const { getStrategyFunctions } = require('./strategies');
|
||||||
const { determineFileType } = require('~/server/utils');
|
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 {MongoFile[]} params.files - The file objects to delete.
|
||||||
* @param {Express.Request} params.req - The express request object.
|
* @param {Express.Request} params.req - The express request object.
|
||||||
* @param {DeleteFilesBody} params.req.body - The request body.
|
* @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.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.
|
* @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');
|
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 =
|
const source =
|
||||||
tool_resource === EToolResources.file_search
|
tool_resource === EToolResources.file_search
|
||||||
? FileSources.vectordb
|
? FileSources.vectordb
|
||||||
: req.app.locals.fileStrategy;
|
: req.app.locals.fileStrategy;
|
||||||
|
|
||||||
const { handleFileUpload } = getStrategyFunctions(source);
|
const { handleFileUpload } = getStrategyFunctions(source);
|
||||||
const { file_id, temp_file_id } = metadata;
|
const { file_id, temp_file_id } = metadata;
|
||||||
|
|
||||||
|
@ -463,9 +483,9 @@ const processAgentFileUpload = async ({ req, res, file, metadata }) => {
|
||||||
if (!messageAttachment && tool_resource) {
|
if (!messageAttachment && tool_resource) {
|
||||||
await addAgentResourceFile({
|
await addAgentResourceFile({
|
||||||
req,
|
req,
|
||||||
agent_id,
|
|
||||||
file_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;
|
filepath = result.filepath;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await createFile(
|
const fileInfo = removeNullishValues({
|
||||||
{
|
user: req.user.id,
|
||||||
user: req.user.id,
|
file_id,
|
||||||
file_id,
|
temp_file_id,
|
||||||
temp_file_id,
|
bytes,
|
||||||
bytes,
|
filepath,
|
||||||
filepath,
|
filename: filename ?? file.originalname,
|
||||||
filename: filename ?? file.originalname,
|
context: messageAttachment ? FileContext.message_attachment : FileContext.agents,
|
||||||
context: messageAttachment ? FileContext.message_attachment : FileContext.agents,
|
model: messageAttachment ? undefined : req.body.model,
|
||||||
model: messageAttachment ? undefined : req.body.model,
|
metadata: fileInfoMetadata,
|
||||||
type: file.mimetype,
|
type: file.mimetype,
|
||||||
embedded,
|
embedded,
|
||||||
source,
|
source,
|
||||||
height,
|
height,
|
||||||
width,
|
width,
|
||||||
},
|
});
|
||||||
true,
|
|
||||||
);
|
const result = await createFile(fileInfo, true);
|
||||||
res.status(200).json({ message: 'Agent file uploaded and processed successfully', ...result });
|
res.status(200).json({ message: 'Agent file uploaded and processed successfully', ...result });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -5,11 +5,13 @@ const {
|
||||||
saveURLToFirebase,
|
saveURLToFirebase,
|
||||||
deleteFirebaseFile,
|
deleteFirebaseFile,
|
||||||
saveBufferToFirebase,
|
saveBufferToFirebase,
|
||||||
|
uploadFileToFirebase,
|
||||||
uploadImageToFirebase,
|
uploadImageToFirebase,
|
||||||
processFirebaseAvatar,
|
processFirebaseAvatar,
|
||||||
getFirebaseFileStream,
|
getFirebaseFileStream,
|
||||||
} = require('./Firebase');
|
} = require('./Firebase');
|
||||||
const {
|
const {
|
||||||
|
uploadLocalFile,
|
||||||
getLocalFileURL,
|
getLocalFileURL,
|
||||||
saveFileFromURL,
|
saveFileFromURL,
|
||||||
saveLocalBuffer,
|
saveLocalBuffer,
|
||||||
|
@ -20,17 +22,15 @@ const {
|
||||||
getLocalFileStream,
|
getLocalFileStream,
|
||||||
} = require('./Local');
|
} = require('./Local');
|
||||||
const { uploadOpenAIFile, deleteOpenAIFile, getOpenAIFileStream } = require('./OpenAI');
|
const { uploadOpenAIFile, deleteOpenAIFile, getOpenAIFileStream } = require('./OpenAI');
|
||||||
|
const { getCodeOutputDownloadStream, uploadCodeEnvFile } = require('./Code');
|
||||||
const { uploadVectors, deleteVectors } = require('./VectorDB');
|
const { uploadVectors, deleteVectors } = require('./VectorDB');
|
||||||
const { getCodeOutputDownloadStream } = require('./Code');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Firebase Storage Strategy Functions
|
* Firebase Storage Strategy Functions
|
||||||
*
|
*
|
||||||
* */
|
* */
|
||||||
const firebaseStrategy = () => ({
|
const firebaseStrategy = () => ({
|
||||||
// saveFile:
|
handleFileUpload: uploadFileToFirebase,
|
||||||
/** @type {typeof uploadVectors | null} */
|
|
||||||
handleFileUpload: null,
|
|
||||||
saveURL: saveURLToFirebase,
|
saveURL: saveURLToFirebase,
|
||||||
getFileURL: getFirebaseURL,
|
getFileURL: getFirebaseURL,
|
||||||
deleteFile: deleteFirebaseFile,
|
deleteFile: deleteFirebaseFile,
|
||||||
|
@ -46,8 +46,7 @@ const firebaseStrategy = () => ({
|
||||||
*
|
*
|
||||||
* */
|
* */
|
||||||
const localStrategy = () => ({
|
const localStrategy = () => ({
|
||||||
/** @type {typeof uploadVectors | null} */
|
handleFileUpload: uploadLocalFile,
|
||||||
handleFileUpload: null,
|
|
||||||
saveURL: saveFileFromURL,
|
saveURL: saveFileFromURL,
|
||||||
getFileURL: getLocalFileURL,
|
getFileURL: getLocalFileURL,
|
||||||
saveBuffer: saveLocalBuffer,
|
saveBuffer: saveLocalBuffer,
|
||||||
|
@ -124,8 +123,7 @@ const codeOutputStrategy = () => ({
|
||||||
prepareImagePayload: null,
|
prepareImagePayload: null,
|
||||||
/** @type {typeof deleteLocalFile | null} */
|
/** @type {typeof deleteLocalFile | null} */
|
||||||
deleteFile: null,
|
deleteFile: null,
|
||||||
/** @type {typeof uploadVectors | null} */
|
handleFileUpload: uploadCodeEnvFile,
|
||||||
handleFileUpload: null,
|
|
||||||
getDownloadStream: getCodeOutputDownloadStream,
|
getDownloadStream: getCodeOutputDownloadStream,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -77,13 +77,27 @@ const anthropicModels = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const metaModels = {
|
const metaModels = {
|
||||||
'llama2-13b': 4000,
|
llama3: 8000,
|
||||||
'llama2-70b': 4000,
|
llama2: 4000,
|
||||||
'llama3-8b': 8000,
|
'llama3.1': 127500,
|
||||||
'llama3-70b': 8000,
|
'llama3-1': 127500,
|
||||||
'llama3-1-8b': 127500,
|
'llama3.1:405b': 127500,
|
||||||
'llama3-1-70b': 127500,
|
'llama3.1:70b': 127500,
|
||||||
|
'llama3.1:8b': 127500,
|
||||||
'llama3-1-405b': 127500,
|
'llama3-1-405b': 127500,
|
||||||
|
'llama3-1-70b': 127500,
|
||||||
|
'llama3-1-8b': 127500,
|
||||||
|
'llama3-70b': 8000,
|
||||||
|
'llama3-8b': 8000,
|
||||||
|
'llama2-70b': 4000,
|
||||||
|
'llama2-13b': 4000,
|
||||||
|
'llama3:70b': 8000,
|
||||||
|
'llama3:8b': 8000,
|
||||||
|
'llama2:70b': 4000,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ollamaModels = {
|
||||||
|
'qwen2.5': 32000,
|
||||||
};
|
};
|
||||||
|
|
||||||
const ai21Models = {
|
const ai21Models = {
|
||||||
|
@ -102,6 +116,7 @@ const bedrockModels = {
|
||||||
...anthropicModels,
|
...anthropicModels,
|
||||||
...mistralModels,
|
...mistralModels,
|
||||||
...cohereModels,
|
...cohereModels,
|
||||||
|
...ollamaModels,
|
||||||
...metaModels,
|
...metaModels,
|
||||||
...ai21Models,
|
...ai21Models,
|
||||||
...amazonModels,
|
...amazonModels,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
const { EModelEndpoint } = require('librechat-data-provider');
|
const { EModelEndpoint } = require('librechat-data-provider');
|
||||||
const { getModelMaxTokens, matchModelName, maxTokensMap } = require('./tokens');
|
const { getModelMaxTokens, processModelData, matchModelName, maxTokensMap } = require('./tokens');
|
||||||
|
|
||||||
describe('getModelMaxTokens', () => {
|
describe('getModelMaxTokens', () => {
|
||||||
test('should return correct tokens for exact match', () => {
|
test('should return correct tokens for exact match', () => {
|
||||||
|
@ -317,3 +317,108 @@ describe('matchModelName', () => {
|
||||||
expect(matchModelName('chat-', EModelEndpoint.google)).toBe('chat-');
|
expect(matchModelName('chat-', EModelEndpoint.google)).toBe('chat-');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Meta Models Tests', () => {
|
||||||
|
describe('getModelMaxTokens', () => {
|
||||||
|
test('should return correct tokens for LLaMa 2 models', () => {
|
||||||
|
expect(getModelMaxTokens('llama2')).toBe(4000);
|
||||||
|
expect(getModelMaxTokens('llama2.70b')).toBe(4000);
|
||||||
|
expect(getModelMaxTokens('llama2-13b')).toBe(4000);
|
||||||
|
expect(getModelMaxTokens('llama2-70b')).toBe(4000);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return correct tokens for LLaMa 3 models', () => {
|
||||||
|
expect(getModelMaxTokens('llama3')).toBe(8000);
|
||||||
|
expect(getModelMaxTokens('llama3.8b')).toBe(8000);
|
||||||
|
expect(getModelMaxTokens('llama3.70b')).toBe(8000);
|
||||||
|
expect(getModelMaxTokens('llama3-8b')).toBe(8000);
|
||||||
|
expect(getModelMaxTokens('llama3-70b')).toBe(8000);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return correct tokens for LLaMa 3.1 models', () => {
|
||||||
|
expect(getModelMaxTokens('llama3.1:8b')).toBe(127500);
|
||||||
|
expect(getModelMaxTokens('llama3.1:70b')).toBe(127500);
|
||||||
|
expect(getModelMaxTokens('llama3.1:405b')).toBe(127500);
|
||||||
|
expect(getModelMaxTokens('llama3-1-8b')).toBe(127500);
|
||||||
|
expect(getModelMaxTokens('llama3-1-70b')).toBe(127500);
|
||||||
|
expect(getModelMaxTokens('llama3-1-405b')).toBe(127500);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle partial matches for Meta models', () => {
|
||||||
|
// Test with full model names
|
||||||
|
expect(getModelMaxTokens('meta/llama3.1:405b')).toBe(127500);
|
||||||
|
expect(getModelMaxTokens('meta/llama3.1:70b')).toBe(127500);
|
||||||
|
expect(getModelMaxTokens('meta/llama3.1:8b')).toBe(127500);
|
||||||
|
expect(getModelMaxTokens('meta/llama3-1-8b')).toBe(127500);
|
||||||
|
|
||||||
|
// Test base versions
|
||||||
|
expect(getModelMaxTokens('meta/llama3.1')).toBe(127500);
|
||||||
|
expect(getModelMaxTokens('meta/llama3-1')).toBe(127500);
|
||||||
|
expect(getModelMaxTokens('meta/llama3')).toBe(8000);
|
||||||
|
expect(getModelMaxTokens('meta/llama2')).toBe(4000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('matchModelName', () => {
|
||||||
|
test('should match exact LLaMa model names', () => {
|
||||||
|
expect(matchModelName('llama2')).toBe('llama2');
|
||||||
|
expect(matchModelName('llama3')).toBe('llama3');
|
||||||
|
expect(matchModelName('llama3.1:8b')).toBe('llama3.1:8b');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should match LLaMa model variations', () => {
|
||||||
|
// Test full model names
|
||||||
|
expect(matchModelName('meta/llama3.1:405b')).toBe('llama3.1:405b');
|
||||||
|
expect(matchModelName('meta/llama3.1:70b')).toBe('llama3.1:70b');
|
||||||
|
expect(matchModelName('meta/llama3.1:8b')).toBe('llama3.1:8b');
|
||||||
|
expect(matchModelName('meta/llama3-1-8b')).toBe('llama3-1-8b');
|
||||||
|
|
||||||
|
// Test base versions
|
||||||
|
expect(matchModelName('meta/llama3.1')).toBe('llama3.1');
|
||||||
|
expect(matchModelName('meta/llama3-1')).toBe('llama3-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle custom endpoint for Meta models', () => {
|
||||||
|
expect(matchModelName('llama2', EModelEndpoint.bedrock)).toBe('llama2');
|
||||||
|
expect(matchModelName('llama3', EModelEndpoint.bedrock)).toBe('llama3');
|
||||||
|
expect(matchModelName('llama3.1:8b', EModelEndpoint.bedrock)).toBe('llama3.1:8b');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('processModelData with Meta models', () => {
|
||||||
|
test('should process Meta model data correctly', () => {
|
||||||
|
const input = {
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
id: 'llama2',
|
||||||
|
pricing: {
|
||||||
|
prompt: '0.00001',
|
||||||
|
completion: '0.00003',
|
||||||
|
},
|
||||||
|
context_length: 4000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'llama3',
|
||||||
|
pricing: {
|
||||||
|
prompt: '0.00002',
|
||||||
|
completion: '0.00004',
|
||||||
|
},
|
||||||
|
context_length: 8000,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = processModelData(input);
|
||||||
|
expect(result.llama2).toEqual({
|
||||||
|
prompt: 10,
|
||||||
|
completion: 30,
|
||||||
|
context: 4000,
|
||||||
|
});
|
||||||
|
expect(result.llama3).toEqual({
|
||||||
|
prompt: 20,
|
||||||
|
completion: 40,
|
||||||
|
context: 8000,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -7,12 +7,12 @@ import type {
|
||||||
TConversationTag,
|
TConversationTag,
|
||||||
TConversationTagRequest,
|
TConversationTagRequest,
|
||||||
} from 'librechat-data-provider';
|
} from 'librechat-data-provider';
|
||||||
import { cn, removeFocusOutlines, defaultTextProps, logger } from '~/utils';
|
|
||||||
import { Checkbox, Label, TextareaAutosize, Input } from '~/components';
|
import { Checkbox, Label, TextareaAutosize, Input } from '~/components';
|
||||||
import { useBookmarkContext } from '~/Providers/BookmarkContext';
|
import { useBookmarkContext } from '~/Providers/BookmarkContext';
|
||||||
import { useConversationTagMutation } from '~/data-provider';
|
import { useConversationTagMutation } from '~/data-provider';
|
||||||
import { useToastContext } from '~/Providers';
|
import { useToastContext } from '~/Providers';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
|
import { cn, logger } from '~/utils';
|
||||||
|
|
||||||
type TBookmarkFormProps = {
|
type TBookmarkFormProps = {
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
|
|
|
@ -47,26 +47,28 @@ const getKnownClass = ({
|
||||||
|
|
||||||
export default function UnknownIcon({
|
export default function UnknownIcon({
|
||||||
className = '',
|
className = '',
|
||||||
endpoint,
|
endpoint: _endpoint,
|
||||||
iconURL,
|
iconURL = '',
|
||||||
context,
|
context,
|
||||||
}: {
|
}: {
|
||||||
iconURL?: string;
|
iconURL?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
endpoint: EModelEndpoint | string | null;
|
endpoint?: EModelEndpoint | string | null;
|
||||||
context?: 'landing' | 'menu-item' | 'nav' | 'message';
|
context?: 'landing' | 'menu-item' | 'nav' | 'message';
|
||||||
}) {
|
}) {
|
||||||
|
const endpoint = _endpoint ?? '';
|
||||||
if (!endpoint) {
|
if (!endpoint) {
|
||||||
return <CustomMinimalIcon className={className} />;
|
return <CustomMinimalIcon className={className} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('UnknownIcon', endpoint);
|
||||||
const currentEndpoint = endpoint.toLowerCase();
|
const currentEndpoint = endpoint.toLowerCase();
|
||||||
|
|
||||||
if (iconURL) {
|
if (iconURL) {
|
||||||
return <img className={className} src={iconURL} alt={`${endpoint} Icon`} />;
|
return <img className={className} src={iconURL} alt={`${endpoint} Icon`} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const assetPath = knownEndpointAssets[currentEndpoint];
|
const assetPath: string = knownEndpointAssets[currentEndpoint] ?? '';
|
||||||
|
|
||||||
if (!assetPath) {
|
if (!assetPath) {
|
||||||
return <CustomMinimalIcon className={className} />;
|
return <CustomMinimalIcon className={className} />;
|
||||||
|
|
|
@ -1,22 +1,26 @@
|
||||||
import { TPlugin, TPluginAuthConfig, TPluginAction } from 'librechat-data-provider';
|
|
||||||
import { Save } from 'lucide-react';
|
import { Save } from 'lucide-react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { TPlugin, TPluginAuthConfig, TPluginAction } from 'librechat-data-provider';
|
||||||
import { HoverCard, HoverCardTrigger } from '~/components/ui';
|
import { HoverCard, HoverCardTrigger } from '~/components/ui';
|
||||||
import PluginTooltip from './PluginTooltip';
|
import PluginTooltip from './PluginTooltip';
|
||||||
|
import { useLocalize } from '~/hooks';
|
||||||
|
|
||||||
type TPluginAuthFormProps = {
|
type TPluginAuthFormProps = {
|
||||||
plugin: TPlugin | undefined;
|
plugin: TPlugin | undefined;
|
||||||
onSubmit: (installActionData: TPluginAction) => void;
|
onSubmit: (installActionData: TPluginAction) => void;
|
||||||
isAssistantTool?: boolean;
|
isEntityTool?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function PluginAuthForm({ plugin, onSubmit, isAssistantTool }: TPluginAuthFormProps) {
|
function PluginAuthForm({ plugin, onSubmit, isEntityTool }: TPluginAuthFormProps) {
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { errors, isDirty, isValid, isSubmitting },
|
formState: { errors, isDirty, isValid, isSubmitting },
|
||||||
} = useForm();
|
} = useForm();
|
||||||
|
|
||||||
|
const localize = useLocalize();
|
||||||
|
const authConfig = plugin?.authConfig ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col items-center gap-2">
|
<div className="flex w-full flex-col items-center gap-2">
|
||||||
<div className="grid w-full gap-6 sm:grid-cols-2">
|
<div className="grid w-full gap-6 sm:grid-cols-2">
|
||||||
|
@ -28,11 +32,11 @@ function PluginAuthForm({ plugin, onSubmit, isAssistantTool }: TPluginAuthFormPr
|
||||||
pluginKey: plugin?.pluginKey ?? '',
|
pluginKey: plugin?.pluginKey ?? '',
|
||||||
action: 'install',
|
action: 'install',
|
||||||
auth,
|
auth,
|
||||||
isAssistantTool,
|
isEntityTool,
|
||||||
}),
|
}),
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{plugin?.authConfig?.map((config: TPluginAuthConfig, i: number) => {
|
{authConfig.map((config: TPluginAuthConfig, i: number) => {
|
||||||
const authField = config.authField.split('||')[0];
|
const authField = config.authField.split('||')[0];
|
||||||
return (
|
return (
|
||||||
<div key={`${authField}-${i}`} className="flex w-full flex-col gap-1">
|
<div key={`${authField}-${i}`} className="flex w-full flex-col gap-1">
|
||||||
|
@ -66,8 +70,7 @@ function PluginAuthForm({ plugin, onSubmit, isAssistantTool }: TPluginAuthFormPr
|
||||||
</HoverCard>
|
</HoverCard>
|
||||||
{errors[authField] && (
|
{errors[authField] && (
|
||||||
<span role="alert" className="mt-1 text-sm text-red-400">
|
<span role="alert" className="mt-1 text-sm text-red-400">
|
||||||
{/* @ts-ignore - Type 'string | FieldError | Merge<FieldError, FieldErrorsImpl<any>> | undefined' is not assignable to type 'ReactNode' */}
|
{errors[authField].message as string}
|
||||||
{errors[authField].message}
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -79,7 +82,7 @@ function PluginAuthForm({ plugin, onSubmit, isAssistantTool }: TPluginAuthFormPr
|
||||||
className="btn btn-primary relative"
|
className="btn btn-primary relative"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-center gap-2">
|
<div className="flex items-center justify-center gap-2">
|
||||||
Save
|
{localize('com_ui_save')}
|
||||||
<Save className="flex h-4 w-4 items-center stroke-2" />
|
<Save className="flex h-4 w-4 items-center stroke-2" />
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -11,7 +11,6 @@ import { icons } from '~/components/Chat/Menus/Endpoints/Icons';
|
||||||
import Action from '~/components/SidePanel/Builder/Action';
|
import Action from '~/components/SidePanel/Builder/Action';
|
||||||
import { ToolSelectDialog } from '~/components/Tools';
|
import { ToolSelectDialog } from '~/components/Tools';
|
||||||
import { useLocalize, useAuthContext } from '~/hooks';
|
import { useLocalize, useAuthContext } from '~/hooks';
|
||||||
import CapabilitiesForm from './CapabilitiesForm';
|
|
||||||
import { processAgentOption } from '~/utils';
|
import { processAgentOption } from '~/utils';
|
||||||
import { Spinner } from '~/components/svg';
|
import { Spinner } from '~/components/svg';
|
||||||
import DeleteButton from './DeleteButton';
|
import DeleteButton from './DeleteButton';
|
||||||
|
@ -19,6 +18,7 @@ import AgentAvatar from './AgentAvatar';
|
||||||
import FileSearch from './FileSearch';
|
import FileSearch from './FileSearch';
|
||||||
import ShareAgent from './ShareAgent';
|
import ShareAgent from './ShareAgent';
|
||||||
import AgentTool from './AgentTool';
|
import AgentTool from './AgentTool';
|
||||||
|
// import CodeForm from './Code/Form';
|
||||||
import { Panel } from '~/common';
|
import { Panel } from '~/common';
|
||||||
|
|
||||||
const labelClass = 'mb-2 text-token-text-primary block font-medium';
|
const labelClass = 'mb-2 text-token-text-primary block font-medium';
|
||||||
|
@ -92,6 +92,26 @@ export default function AgentConfig({
|
||||||
return _agent.knowledge_files ?? [];
|
return _agent.knowledge_files ?? [];
|
||||||
}, [agent, agent_id, fileMap]);
|
}, [agent, agent_id, fileMap]);
|
||||||
|
|
||||||
|
const code_files = useMemo(() => {
|
||||||
|
if (typeof agent === 'string') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (agent?.id !== agent_id) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (agent.code_files) {
|
||||||
|
return agent.code_files;
|
||||||
|
}
|
||||||
|
|
||||||
|
const _agent = processAgentOption({
|
||||||
|
agent,
|
||||||
|
fileMap,
|
||||||
|
});
|
||||||
|
return _agent.code_files ?? [];
|
||||||
|
}, [agent, agent_id, fileMap]);
|
||||||
|
|
||||||
/* Mutations */
|
/* Mutations */
|
||||||
const update = useUpdateAgentMutation({
|
const update = useUpdateAgentMutation({
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
|
@ -293,7 +313,7 @@ export default function AgentConfig({
|
||||||
<div className="shadow-stroke relative flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-white text-black dark:bg-white">
|
<div className="shadow-stroke relative flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-white text-black dark:bg-white">
|
||||||
<Icon
|
<Icon
|
||||||
className="h-2/3 w-2/3"
|
className="h-2/3 w-2/3"
|
||||||
endpoint={provider as string}
|
endpoint={providerValue as string}
|
||||||
endpointType={endpointType}
|
endpointType={endpointType}
|
||||||
iconURL={endpointIconURL}
|
iconURL={endpointIconURL}
|
||||||
/>
|
/>
|
||||||
|
@ -303,11 +323,8 @@ export default function AgentConfig({
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<CapabilitiesForm
|
{/* Code Execution */}
|
||||||
codeEnabled={codeEnabled}
|
{/* {codeEnabled && <CodeForm agent_id={agent_id} files={code_files} />} */}
|
||||||
agentsConfig={agentsConfig}
|
|
||||||
retrievalEnabled={false}
|
|
||||||
/>
|
|
||||||
{/* File Search */}
|
{/* File Search */}
|
||||||
{fileSearchEnabled && <FileSearch agent_id={agent_id} files={knowledge_files} />}
|
{fileSearchEnabled && <FileSearch agent_id={agent_id} files={knowledge_files} />}
|
||||||
{/* Agent Tools & Actions */}
|
{/* Agent Tools & Actions */}
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { cn } from '~/utils';
|
||||||
export default function AgentTool({
|
export default function AgentTool({
|
||||||
tool,
|
tool,
|
||||||
allTools,
|
allTools,
|
||||||
agent_id,
|
agent_id = '',
|
||||||
}: {
|
}: {
|
||||||
tool: string;
|
tool: string;
|
||||||
allTools: TPlugin[];
|
allTools: TPlugin[];
|
||||||
|
@ -28,7 +28,7 @@ export default function AgentTool({
|
||||||
const removeTool = (tool: string) => {
|
const removeTool = (tool: string) => {
|
||||||
if (tool) {
|
if (tool) {
|
||||||
updateUserPlugins.mutate(
|
updateUserPlugins.mutate(
|
||||||
{ pluginKey: tool, action: 'uninstall', auth: null, isAgentTool: true },
|
{ pluginKey: tool, action: 'uninstall', auth: null, isEntityTool: true },
|
||||||
{
|
{
|
||||||
onError: (error: unknown) => {
|
onError: (error: unknown) => {
|
||||||
showToast({ message: `Error while deleting the tool: ${error}`, status: 'error' });
|
showToast({ message: `Error while deleting the tool: ${error}`, status: 'error' });
|
||||||
|
|
|
@ -1,56 +0,0 @@
|
||||||
import { useMemo } from 'react';
|
|
||||||
// import { Capabilities } from 'librechat-data-provider';
|
|
||||||
// import { useFormContext, useWatch } from 'react-hook-form';
|
|
||||||
import type { TConfig } from 'librechat-data-provider';
|
|
||||||
// import type { AgentForm } from '~/common';
|
|
||||||
// import ImageVision from './ImageVision';
|
|
||||||
import { useLocalize } from '~/hooks';
|
|
||||||
import Retrieval from './Retrieval';
|
|
||||||
// import CodeFiles from './CodeFiles';
|
|
||||||
import Code from './Code';
|
|
||||||
|
|
||||||
export default function CapabilitiesForm({
|
|
||||||
codeEnabled,
|
|
||||||
retrievalEnabled,
|
|
||||||
agentsConfig,
|
|
||||||
}: {
|
|
||||||
codeEnabled?: boolean;
|
|
||||||
retrievalEnabled?: boolean;
|
|
||||||
agentsConfig?: TConfig | null;
|
|
||||||
}) {
|
|
||||||
const localize = useLocalize();
|
|
||||||
|
|
||||||
// const methods = useFormContext<AgentForm>();
|
|
||||||
// const { control } = methods;
|
|
||||||
// const agent = useWatch({ control, name: 'agent' });
|
|
||||||
// const agent_id = useWatch({ control, name: 'id' });
|
|
||||||
// const files = useMemo(() => {
|
|
||||||
// if (typeof agent === 'string') {
|
|
||||||
// return [];
|
|
||||||
// }
|
|
||||||
// return agent?.code_files;
|
|
||||||
// }, [agent]);
|
|
||||||
|
|
||||||
const retrievalModels = useMemo(
|
|
||||||
() => new Set(agentsConfig?.retrievalModels ?? []),
|
|
||||||
[agentsConfig],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mb-4">
|
|
||||||
<div className="mb-1.5 flex items-center">
|
|
||||||
<span>
|
|
||||||
<label className="text-token-text-primary block font-medium">
|
|
||||||
{localize('com_assistants_capabilities')}
|
|
||||||
</label>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col items-start gap-2">
|
|
||||||
{codeEnabled === true && <Code />}
|
|
||||||
{retrievalEnabled === true && <Retrieval retrievalModels={retrievalModels} />}
|
|
||||||
{/* {imageVisionEnabled && version == 1 && <ImageVision />} */}
|
|
||||||
{/* {codeEnabled && <CodeFiles agent_id={agent_id} files={files} />} */}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,70 +0,0 @@
|
||||||
import { AgentCapabilities } from 'librechat-data-provider';
|
|
||||||
import { useFormContext, Controller } from 'react-hook-form';
|
|
||||||
import type { AgentForm } from '~/common';
|
|
||||||
import {
|
|
||||||
Checkbox,
|
|
||||||
HoverCard,
|
|
||||||
HoverCardContent,
|
|
||||||
HoverCardPortal,
|
|
||||||
HoverCardTrigger,
|
|
||||||
} from '~/components/ui';
|
|
||||||
import { CircleHelpIcon } from '~/components/svg';
|
|
||||||
import { useLocalize } from '~/hooks';
|
|
||||||
import { ESide } from '~/common';
|
|
||||||
|
|
||||||
export default function Code() {
|
|
||||||
const localize = useLocalize();
|
|
||||||
const methods = useFormContext<AgentForm>();
|
|
||||||
const { control, setValue, getValues } = methods;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<HoverCard openDelay={50}>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Controller
|
|
||||||
name={AgentCapabilities.execute_code}
|
|
||||||
control={control}
|
|
||||||
render={({ field }) => (
|
|
||||||
<Checkbox
|
|
||||||
{...field}
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
|
|
||||||
value={field.value.toString()}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="flex items-center space-x-2"
|
|
||||||
onClick={() =>
|
|
||||||
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
|
||||||
setValue(AgentCapabilities.execute_code, !getValues(AgentCapabilities.execute_code), {
|
|
||||||
shouldDirty: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<label
|
|
||||||
className="form-check-label text-token-text-primary w-full cursor-pointer"
|
|
||||||
htmlFor={AgentCapabilities.execute_code}
|
|
||||||
>
|
|
||||||
{localize('com_agents_execute_code')}
|
|
||||||
</label>
|
|
||||||
<HoverCardTrigger>
|
|
||||||
<CircleHelpIcon className="h-5 w-5 text-gray-500" />
|
|
||||||
</HoverCardTrigger>
|
|
||||||
</button>
|
|
||||||
<HoverCardPortal>
|
|
||||||
<HoverCardContent side={ESide.Top} className="w-80">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<p className="text-sm text-text-secondary">
|
|
||||||
{/* // TODO: add a Code Interpreter description */}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</HoverCardContent>
|
|
||||||
</HoverCardPortal>
|
|
||||||
</div>
|
|
||||||
</HoverCard>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
151
client/src/components/SidePanel/Agents/Code/Action.tsx
Normal file
151
client/src/components/SidePanel/Agents/Code/Action.tsx
Normal file
|
@ -0,0 +1,151 @@
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { KeyRoundIcon } from 'lucide-react';
|
||||||
|
import { AuthType, AgentCapabilities } from 'librechat-data-provider';
|
||||||
|
import { useFormContext, Controller, useForm, useWatch } from 'react-hook-form';
|
||||||
|
import type { AgentForm } from '~/common';
|
||||||
|
import {
|
||||||
|
Input,
|
||||||
|
OGDialog,
|
||||||
|
Checkbox,
|
||||||
|
HoverCard,
|
||||||
|
HoverCardContent,
|
||||||
|
HoverCardPortal,
|
||||||
|
HoverCardTrigger,
|
||||||
|
Button,
|
||||||
|
} from '~/components/ui';
|
||||||
|
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
|
||||||
|
import { useLocalize, useAuthCodeTool } from '~/hooks';
|
||||||
|
import { CircleHelpIcon } from '~/components/svg';
|
||||||
|
import { ESide } from '~/common';
|
||||||
|
|
||||||
|
type ApiKeyFormData = {
|
||||||
|
apiKey: string;
|
||||||
|
authType?: string | AuthType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Action({ authType = '', isToolAuthenticated = false }) {
|
||||||
|
const localize = useLocalize();
|
||||||
|
const methods = useFormContext<AgentForm>();
|
||||||
|
const { control, setValue, getValues } = methods;
|
||||||
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
|
const runCodeIsEnabled = useWatch({ control, name: AgentCapabilities.execute_code });
|
||||||
|
|
||||||
|
const { installTool, removeTool } = useAuthCodeTool({ isEntityTool: true });
|
||||||
|
|
||||||
|
const { reset, register, handleSubmit } = useForm<ApiKeyFormData>();
|
||||||
|
const isUserProvided = authType === AuthType.USER_PROVIDED;
|
||||||
|
|
||||||
|
const handleCheckboxChange = (checked: boolean) => {
|
||||||
|
if (isToolAuthenticated) {
|
||||||
|
setValue(AgentCapabilities.execute_code, checked, { shouldDirty: true });
|
||||||
|
} else if (runCodeIsEnabled) {
|
||||||
|
setValue(AgentCapabilities.execute_code, false, { shouldDirty: true });
|
||||||
|
} else {
|
||||||
|
setIsDialogOpen(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = (data: { apiKey: string }) => {
|
||||||
|
reset();
|
||||||
|
installTool(data.apiKey);
|
||||||
|
setIsDialogOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRevokeApiKey = () => {
|
||||||
|
reset();
|
||||||
|
removeTool();
|
||||||
|
setIsDialogOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<HoverCard openDelay={50}>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Controller
|
||||||
|
name={AgentCapabilities.execute_code}
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<Checkbox
|
||||||
|
{...field}
|
||||||
|
checked={runCodeIsEnabled ? runCodeIsEnabled : isToolAuthenticated && field.value}
|
||||||
|
onCheckedChange={handleCheckboxChange}
|
||||||
|
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
|
||||||
|
value={field.value.toString()}
|
||||||
|
disabled={runCodeIsEnabled ? false : !isToolAuthenticated}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex items-center space-x-2"
|
||||||
|
onClick={() => {
|
||||||
|
const value = !getValues(AgentCapabilities.execute_code);
|
||||||
|
handleCheckboxChange(value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
className="form-check-label text-token-text-primary w-full cursor-pointer"
|
||||||
|
htmlFor={AgentCapabilities.execute_code}
|
||||||
|
>
|
||||||
|
{localize('com_agents_execute_code')}
|
||||||
|
</label>
|
||||||
|
</button>
|
||||||
|
<div className="ml-2 flex gap-2">
|
||||||
|
{isUserProvided && (isToolAuthenticated || runCodeIsEnabled) && (
|
||||||
|
<button type="button" onClick={() => setIsDialogOpen(true)}>
|
||||||
|
<KeyRoundIcon className="h-5 w-5 text-text-primary" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<HoverCardTrigger>
|
||||||
|
<CircleHelpIcon className="h-5 w-5 text-gray-500" />
|
||||||
|
</HoverCardTrigger>
|
||||||
|
</div>
|
||||||
|
<HoverCardPortal>
|
||||||
|
<HoverCardContent side={ESide.Top} className="w-80">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-sm text-text-secondary">
|
||||||
|
{/* // TODO: add a Code Interpreter description */}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</HoverCardContent>
|
||||||
|
</HoverCardPortal>
|
||||||
|
</div>
|
||||||
|
</HoverCard>
|
||||||
|
<OGDialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||||
|
<OGDialogTemplate
|
||||||
|
className="w-11/12 sm:w-1/4"
|
||||||
|
title={localize('com_agents_tool_not_authenticated')}
|
||||||
|
main={
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="Enter API Key"
|
||||||
|
autoComplete="one-time-code"
|
||||||
|
readOnly={true}
|
||||||
|
onFocus={(e) => (e.target.readOnly = false)}
|
||||||
|
{...register('apiKey', { required: true })}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
selection={{
|
||||||
|
selectHandler: handleSubmit(onSubmit),
|
||||||
|
selectClasses: 'bg-green-500 hover:bg-green-600 text-white',
|
||||||
|
selectText: localize('com_ui_save'),
|
||||||
|
}}
|
||||||
|
buttons={
|
||||||
|
isUserProvided &&
|
||||||
|
isToolAuthenticated && (
|
||||||
|
<Button
|
||||||
|
onClick={handleRevokeApiKey}
|
||||||
|
className="bg-destructive text-white transition-all duration-200 hover:bg-destructive/80"
|
||||||
|
>
|
||||||
|
{localize('com_ui_revoke')}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
showCancelButton={true}
|
||||||
|
/>
|
||||||
|
</OGDialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,21 +1,22 @@
|
||||||
import { useState, useRef, useEffect } from 'react';
|
import { useState, useRef } from 'react';
|
||||||
|
import { useFormContext } from 'react-hook-form';
|
||||||
import {
|
import {
|
||||||
EToolResources,
|
EToolResources,
|
||||||
EModelEndpoint,
|
EModelEndpoint,
|
||||||
mergeFileConfig,
|
mergeFileConfig,
|
||||||
|
AgentCapabilities,
|
||||||
fileConfig as defaultFileConfig,
|
fileConfig as defaultFileConfig,
|
||||||
} from 'librechat-data-provider';
|
} from 'librechat-data-provider';
|
||||||
import type { EndpointFileConfig } from 'librechat-data-provider';
|
import type { EndpointFileConfig } from 'librechat-data-provider';
|
||||||
import type { ExtendedFile } from '~/common';
|
import type { ExtendedFile, AgentForm } from '~/common';
|
||||||
|
import { useFileHandling, useLocalize, useLazyEffect } from '~/hooks';
|
||||||
import FileRow from '~/components/Chat/Input/Files/FileRow';
|
import FileRow from '~/components/Chat/Input/Files/FileRow';
|
||||||
import { useGetFileConfig } from '~/data-provider';
|
import { useGetFileConfig } from '~/data-provider';
|
||||||
import { useFileHandling } from '~/hooks/Files';
|
|
||||||
import useLocalize from '~/hooks/useLocalize';
|
|
||||||
import { useChatContext } from '~/Providers';
|
import { useChatContext } from '~/Providers';
|
||||||
|
|
||||||
const tool_resource = EToolResources.code_interpreter;
|
const tool_resource = EToolResources.execute_code;
|
||||||
|
|
||||||
export default function CodeFiles({
|
export default function Files({
|
||||||
agent_id,
|
agent_id,
|
||||||
files: _files,
|
files: _files,
|
||||||
}: {
|
}: {
|
||||||
|
@ -24,22 +25,29 @@ export default function CodeFiles({
|
||||||
}) {
|
}) {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const { setFilesLoading } = useChatContext();
|
const { setFilesLoading } = useChatContext();
|
||||||
|
const { watch } = useFormContext<AgentForm>();
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const [files, setFiles] = useState<Map<string, ExtendedFile>>(new Map());
|
const [files, setFiles] = useState<Map<string, ExtendedFile>>(new Map());
|
||||||
const { data: fileConfig = defaultFileConfig } = useGetFileConfig({
|
const { data: fileConfig = defaultFileConfig } = useGetFileConfig({
|
||||||
select: (data) => mergeFileConfig(data),
|
select: (data) => mergeFileConfig(data),
|
||||||
});
|
});
|
||||||
const { handleFileChange } = useFileHandling({
|
const { abortUpload, handleFileChange } = useFileHandling({
|
||||||
|
fileSetter: setFiles,
|
||||||
overrideEndpoint: EModelEndpoint.agents,
|
overrideEndpoint: EModelEndpoint.agents,
|
||||||
additionalMetadata: { agent_id, tool_resource },
|
additionalMetadata: { agent_id, tool_resource },
|
||||||
fileSetter: setFiles,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useLazyEffect(
|
||||||
if (_files) {
|
() => {
|
||||||
setFiles(new Map(_files));
|
if (_files) {
|
||||||
}
|
setFiles(new Map(_files));
|
||||||
}, [_files]);
|
}
|
||||||
|
},
|
||||||
|
[_files],
|
||||||
|
750,
|
||||||
|
);
|
||||||
|
|
||||||
|
const codeChecked = watch(AgentCapabilities.execute_code);
|
||||||
|
|
||||||
const endpointFileConfig = fileConfig.endpoints[EModelEndpoint.agents] as
|
const endpointFileConfig = fileConfig.endpoints[EModelEndpoint.agents] as
|
||||||
| EndpointFileConfig
|
| EndpointFileConfig
|
||||||
|
@ -68,6 +76,7 @@ export default function CodeFiles({
|
||||||
files={files}
|
files={files}
|
||||||
setFiles={setFiles}
|
setFiles={setFiles}
|
||||||
agent_id={agent_id}
|
agent_id={agent_id}
|
||||||
|
abortUpload={abortUpload}
|
||||||
tool_resource={tool_resource}
|
tool_resource={tool_resource}
|
||||||
setFilesLoading={setFilesLoading}
|
setFilesLoading={setFilesLoading}
|
||||||
Wrapper={({ children }) => <div className="flex flex-wrap gap-2">{children}</div>}
|
Wrapper={({ children }) => <div className="flex flex-wrap gap-2">{children}</div>}
|
||||||
|
@ -75,7 +84,7 @@ export default function CodeFiles({
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={!agent_id}
|
disabled={!agent_id || codeChecked === false}
|
||||||
className="btn btn-neutral border-token-border-light relative h-8 w-full rounded-lg font-medium"
|
className="btn btn-neutral border-token-border-light relative h-8 w-full rounded-lg font-medium"
|
||||||
onClick={handleButtonClick}
|
onClick={handleButtonClick}
|
||||||
>
|
>
|
||||||
|
@ -86,7 +95,7 @@ export default function CodeFiles({
|
||||||
style={{ display: 'none' }}
|
style={{ display: 'none' }}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
disabled={!agent_id}
|
disabled={!agent_id || codeChecked === false}
|
||||||
onChange={handleFileChange}
|
onChange={handleFileChange}
|
||||||
/>
|
/>
|
||||||
{localize('com_ui_upload_files')}
|
{localize('com_ui_upload_files')}
|
33
client/src/components/SidePanel/Agents/Code/Form.tsx
Normal file
33
client/src/components/SidePanel/Agents/Code/Form.tsx
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import { Tools } from 'librechat-data-provider';
|
||||||
|
import type { ExtendedFile } from '~/common';
|
||||||
|
import { useVerifyAgentToolAuth } from '~/data-provider';
|
||||||
|
import { useLocalize } from '~/hooks';
|
||||||
|
import Action from './Action';
|
||||||
|
import Files from './Files';
|
||||||
|
|
||||||
|
export default function CodeForm({
|
||||||
|
agent_id,
|
||||||
|
files,
|
||||||
|
}: {
|
||||||
|
agent_id: string;
|
||||||
|
files?: [string, ExtendedFile][];
|
||||||
|
}) {
|
||||||
|
const localize = useLocalize();
|
||||||
|
const { data } = useVerifyAgentToolAuth({ toolId: Tools.execute_code });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="mb-1.5 flex items-center">
|
||||||
|
<span>
|
||||||
|
<label className="text-token-text-primary block font-medium">
|
||||||
|
{localize('com_assistants_capabilities')}
|
||||||
|
</label>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-start gap-2">
|
||||||
|
<Action authType={data?.message} isToolAuthenticated={data?.authenticated} />
|
||||||
|
<Files agent_id={agent_id} files={files} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import { useState, useRef, useEffect } from 'react';
|
import { useState, useRef } from 'react';
|
||||||
import { useFormContext } from 'react-hook-form';
|
import { useFormContext } from 'react-hook-form';
|
||||||
import {
|
import {
|
||||||
EModelEndpoint,
|
EModelEndpoint,
|
||||||
|
@ -9,12 +9,11 @@ import {
|
||||||
fileConfig as defaultFileConfig,
|
fileConfig as defaultFileConfig,
|
||||||
} from 'librechat-data-provider';
|
} from 'librechat-data-provider';
|
||||||
import type { ExtendedFile, AgentForm } from '~/common';
|
import type { ExtendedFile, AgentForm } from '~/common';
|
||||||
|
import { useFileHandling, useLocalize, useLazyEffect } from '~/hooks';
|
||||||
import FileRow from '~/components/Chat/Input/Files/FileRow';
|
import FileRow from '~/components/Chat/Input/Files/FileRow';
|
||||||
import FileSearchCheckbox from './FileSearchCheckbox';
|
import FileSearchCheckbox from './FileSearchCheckbox';
|
||||||
import { useGetFileConfig } from '~/data-provider';
|
import { useGetFileConfig } from '~/data-provider';
|
||||||
import { AttachmentIcon } from '~/components/svg';
|
import { AttachmentIcon } from '~/components/svg';
|
||||||
import { useFileHandling } from '~/hooks/Files';
|
|
||||||
import useLocalize from '~/hooks/useLocalize';
|
|
||||||
import { useChatContext } from '~/Providers';
|
import { useChatContext } from '~/Providers';
|
||||||
|
|
||||||
export default function FileSearch({
|
export default function FileSearch({
|
||||||
|
@ -40,18 +39,22 @@ export default function FileSearch({
|
||||||
fileSetter: setFiles,
|
fileSetter: setFiles,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useLazyEffect(
|
||||||
if (_files) {
|
() => {
|
||||||
setFiles(new Map(_files));
|
if (_files) {
|
||||||
}
|
setFiles(new Map(_files));
|
||||||
}, [_files]);
|
}
|
||||||
|
},
|
||||||
|
[_files],
|
||||||
|
750,
|
||||||
|
);
|
||||||
|
|
||||||
const fileSearchChecked = watch(AgentCapabilities.file_search);
|
const fileSearchChecked = watch(AgentCapabilities.file_search);
|
||||||
|
|
||||||
const endpointFileConfig = fileConfig.endpoints[EModelEndpoint.agents];
|
const endpointFileConfig = fileConfig.endpoints[EModelEndpoint.agents];
|
||||||
const disabled = endpointFileConfig.disabled ?? false;
|
const isUploadDisabled = endpointFileConfig.disabled ?? false;
|
||||||
|
|
||||||
if (disabled === true) {
|
if (isUploadDisabled) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { cn } from '~/utils';
|
||||||
export default function AssistantTool({
|
export default function AssistantTool({
|
||||||
tool,
|
tool,
|
||||||
allTools,
|
allTools,
|
||||||
assistant_id,
|
assistant_id = '',
|
||||||
}: {
|
}: {
|
||||||
tool: string;
|
tool: string;
|
||||||
allTools: TPlugin[];
|
allTools: TPlugin[];
|
||||||
|
@ -28,7 +28,7 @@ export default function AssistantTool({
|
||||||
const removeTool = (tool: string) => {
|
const removeTool = (tool: string) => {
|
||||||
if (tool) {
|
if (tool) {
|
||||||
updateUserPlugins.mutate(
|
updateUserPlugins.mutate(
|
||||||
{ pluginKey: tool, action: 'uninstall', auth: null, isAssistantTool: true },
|
{ pluginKey: tool, action: 'uninstall', auth: null, isEntityTool: true },
|
||||||
{
|
{
|
||||||
onError: (error: unknown) => {
|
onError: (error: unknown) => {
|
||||||
showToast({ message: `Error while deleting the tool: ${error}`, status: 'error' });
|
showToast({ message: `Error while deleting the tool: ${error}`, status: 'error' });
|
||||||
|
@ -78,7 +78,7 @@ export default function AssistantTool({
|
||||||
<OGDialogTrigger asChild>
|
<OGDialogTrigger asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="transition-colors flex h-9 w-9 min-w-9 items-center justify-center rounded-lg duration-200 hover:bg-gray-200 dark:hover:bg-gray-700"
|
className="flex h-9 w-9 min-w-9 items-center justify-center rounded-lg transition-colors duration-200 hover:bg-gray-200 dark:hover:bg-gray-700"
|
||||||
>
|
>
|
||||||
<TrashIcon />
|
<TrashIcon />
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -90,7 +90,7 @@ function ToolSelectDialog({
|
||||||
const onRemoveTool = (tool: string) => {
|
const onRemoveTool = (tool: string) => {
|
||||||
setShowPluginAuthForm(false);
|
setShowPluginAuthForm(false);
|
||||||
updateUserPlugins.mutate(
|
updateUserPlugins.mutate(
|
||||||
{ pluginKey: tool, action: 'uninstall', auth: null, isAssistantTool: true },
|
{ pluginKey: tool, action: 'uninstall', auth: null, isEntityTool: true },
|
||||||
{
|
{
|
||||||
onError: (error: unknown) => {
|
onError: (error: unknown) => {
|
||||||
handleInstallError(error as TError);
|
handleInstallError(error as TError);
|
||||||
|
@ -199,7 +199,7 @@ function ToolSelectDialog({
|
||||||
<PluginAuthForm
|
<PluginAuthForm
|
||||||
plugin={selectedPlugin}
|
plugin={selectedPlugin}
|
||||||
onSubmit={(installActionData: TPluginAction) => handleInstall(installActionData)}
|
onSubmit={(installActionData: TPluginAction) => handleInstall(installActionData)}
|
||||||
isAssistantTool={true}
|
isEntityTool={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -10,13 +10,6 @@ import {
|
||||||
import type * as t from 'librechat-data-provider';
|
import type * as t from 'librechat-data-provider';
|
||||||
import type { UseMutationResult } from '@tanstack/react-query';
|
import type { UseMutationResult } from '@tanstack/react-query';
|
||||||
|
|
||||||
export type TGenTitleMutation = UseMutationResult<
|
|
||||||
t.TGenTitleResponse,
|
|
||||||
unknown,
|
|
||||||
t.TGenTitleRequest,
|
|
||||||
unknown
|
|
||||||
>;
|
|
||||||
|
|
||||||
export const useUploadFileMutation = (
|
export const useUploadFileMutation = (
|
||||||
_options?: t.UploadMutationOptions,
|
_options?: t.UploadMutationOptions,
|
||||||
signal?: AbortSignal | null,
|
signal?: AbortSignal | null,
|
||||||
|
@ -152,9 +145,9 @@ export const useDeleteFilesMutation = (
|
||||||
return useMutation([MutationKeys.fileDelete], {
|
return useMutation([MutationKeys.fileDelete], {
|
||||||
mutationFn: (body: t.DeleteFilesBody) => dataService.deleteFiles(body),
|
mutationFn: (body: t.DeleteFilesBody) => dataService.deleteFiles(body),
|
||||||
...options,
|
...options,
|
||||||
onSuccess: (data, ...args) => {
|
onSuccess: (data, vars, context) => {
|
||||||
queryClient.setQueryData<t.TFile[] | undefined>([QueryKeys.files], (cachefiles) => {
|
queryClient.setQueryData<t.TFile[] | undefined>([QueryKeys.files], (cachefiles) => {
|
||||||
const { files: filesDeleted } = args[0];
|
const { files: filesDeleted } = vars;
|
||||||
|
|
||||||
const fileMap = filesDeleted.reduce((acc, file) => {
|
const fileMap = filesDeleted.reduce((acc, file) => {
|
||||||
acc.set(file.file_id, file);
|
acc.set(file.file_id, file);
|
||||||
|
@ -163,7 +156,10 @@ export const useDeleteFilesMutation = (
|
||||||
|
|
||||||
return (cachefiles ?? []).filter((file) => !fileMap.has(file.file_id));
|
return (cachefiles ?? []).filter((file) => !fileMap.has(file.file_id));
|
||||||
});
|
});
|
||||||
onSuccess?.(data, ...args);
|
onSuccess?.(data, vars, context);
|
||||||
|
if (vars.agent_id != null && vars.agent_id) {
|
||||||
|
queryClient.refetchQueries([QueryKeys.agent, vars.agent_id]);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
2
client/src/data-provider/Tools/index.ts
Normal file
2
client/src/data-provider/Tools/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './queries';
|
||||||
|
// export * from './mutations';
|
20
client/src/data-provider/Tools/queries.ts
Normal file
20
client/src/data-provider/Tools/queries.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { QueryKeys, dataService } from 'librechat-data-provider';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import type { QueryObserverResult, UseQueryOptions } from '@tanstack/react-query';
|
||||||
|
import type t from 'librechat-data-provider';
|
||||||
|
|
||||||
|
export const useVerifyAgentToolAuth = (
|
||||||
|
params: t.VerifyToolAuthParams,
|
||||||
|
config?: UseQueryOptions<t.VerifyToolAuthResponse>,
|
||||||
|
): QueryObserverResult<t.VerifyToolAuthResponse> => {
|
||||||
|
return useQuery<t.VerifyToolAuthResponse>(
|
||||||
|
[QueryKeys.toolAuth, params.toolId],
|
||||||
|
() => dataService.getVerifyAgentToolAuth(params),
|
||||||
|
{
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
refetchOnReconnect: false,
|
||||||
|
refetchOnMount: false,
|
||||||
|
...config,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,4 +1,5 @@
|
||||||
export * from './Files';
|
export * from './Files';
|
||||||
|
export * from './Tools';
|
||||||
export * from './connection';
|
export * from './connection';
|
||||||
export * from './mutations';
|
export * from './mutations';
|
||||||
export * from './prompts';
|
export * from './prompts';
|
||||||
|
|
1
client/src/hooks/Generic/index.ts
Normal file
1
client/src/hooks/Generic/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './useLazyEffect';
|
18
client/src/hooks/Generic/useLazyEffect.ts
Normal file
18
client/src/hooks/Generic/useLazyEffect.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
// https://stackoverflow.com/a/67504622/51500
|
||||||
|
import { DependencyList, EffectCallback, useCallback, useEffect, useRef } from 'react';
|
||||||
|
import debounce from 'lodash/debounce';
|
||||||
|
|
||||||
|
export function useLazyEffect(effect: EffectCallback, deps: DependencyList = [], wait = 300) {
|
||||||
|
const cleanUp = useRef<void | (() => void)>();
|
||||||
|
const effectRef = useRef<EffectCallback>();
|
||||||
|
effectRef.current = useCallback(effect, deps);
|
||||||
|
const lazyEffect = useCallback(
|
||||||
|
debounce(() => (cleanUp.current = effectRef.current?.()), wait),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
useEffect(lazyEffect, deps);
|
||||||
|
useEffect(() => {
|
||||||
|
return () => (cleanUp.current instanceof Function ? cleanUp.current() : undefined);
|
||||||
|
}, []);
|
||||||
|
}
|
|
@ -1,2 +1,3 @@
|
||||||
|
export { default as useAuthCodeTool } from './useAuthCodeTool';
|
||||||
export { default as usePluginInstall } from './usePluginInstall';
|
export { default as usePluginInstall } from './usePluginInstall';
|
||||||
export { default as usePluginDialogHelpers } from './usePluginDialogHelpers';
|
export { default as usePluginDialogHelpers } from './usePluginDialogHelpers';
|
||||||
|
|
53
client/src/hooks/Plugins/useAuthCodeTool.ts
Normal file
53
client/src/hooks/Plugins/useAuthCodeTool.ts
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { AuthType, Tools, QueryKeys } from 'librechat-data-provider';
|
||||||
|
import { useUpdateUserPluginsMutation } from 'librechat-data-provider/react-query';
|
||||||
|
// import { useToastContext } from '~/Providers';
|
||||||
|
|
||||||
|
const useAuthCodeTool = (options?: { isEntityTool: boolean }) => {
|
||||||
|
// const { showToast } = useToastContext();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const isEntityTool = options?.isEntityTool ?? true;
|
||||||
|
const updateUserPlugins = useUpdateUserPluginsMutation({
|
||||||
|
onMutate: (vars) => {
|
||||||
|
queryClient.setQueryData([QueryKeys.toolAuth, Tools.execute_code], () => ({
|
||||||
|
authenticated: vars.action === 'install',
|
||||||
|
message: AuthType.USER_PROVIDED,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries([QueryKeys.toolAuth, Tools.execute_code]);
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
queryClient.invalidateQueries([QueryKeys.toolAuth, Tools.execute_code]);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const installTool = useCallback(
|
||||||
|
(apiKey: string) => {
|
||||||
|
updateUserPlugins.mutate({
|
||||||
|
pluginKey: Tools.execute_code,
|
||||||
|
action: 'install',
|
||||||
|
auth: { LIBRECHAT_CODE_API_KEY: apiKey },
|
||||||
|
isEntityTool,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[updateUserPlugins, isEntityTool],
|
||||||
|
);
|
||||||
|
|
||||||
|
const removeTool = useCallback(() => {
|
||||||
|
updateUserPlugins.mutate({
|
||||||
|
pluginKey: Tools.execute_code,
|
||||||
|
action: 'uninstall',
|
||||||
|
auth: { LIBRECHAT_CODE_API_KEY: null },
|
||||||
|
isEntityTool,
|
||||||
|
});
|
||||||
|
}, [updateUserPlugins, isEntityTool]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
removeTool,
|
||||||
|
installTool,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useAuthCodeTool;
|
|
@ -6,6 +6,7 @@ export * from './Config';
|
||||||
export * from './Conversations';
|
export * from './Conversations';
|
||||||
export * from './Nav';
|
export * from './Nav';
|
||||||
export * from './Files';
|
export * from './Files';
|
||||||
|
export * from './Generic';
|
||||||
export * from './Input';
|
export * from './Input';
|
||||||
export * from './Messages';
|
export * from './Messages';
|
||||||
export * from './Plugins';
|
export * from './Plugins';
|
||||||
|
|
1158
package-lock.json
generated
1158
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -125,10 +125,10 @@ export const assistants = ({
|
||||||
return url;
|
return url;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const agents = ({ path, options }: { path?: string; options?: object }) => {
|
export const agents = ({ path = '', options }: { path?: string; options?: object }) => {
|
||||||
let url = '/api/agents';
|
let url = '/api/agents';
|
||||||
|
|
||||||
if (path) {
|
if (path && path !== '') {
|
||||||
url += `/${path}`;
|
url += `/${path}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -584,6 +584,7 @@ export const alternateName = {
|
||||||
[EModelEndpoint.anthropic]: 'Anthropic',
|
[EModelEndpoint.anthropic]: 'Anthropic',
|
||||||
[EModelEndpoint.custom]: 'Custom',
|
[EModelEndpoint.custom]: 'Custom',
|
||||||
[EModelEndpoint.bedrock]: 'AWS Bedrock',
|
[EModelEndpoint.bedrock]: 'AWS Bedrock',
|
||||||
|
ollama: 'Ollama',
|
||||||
};
|
};
|
||||||
|
|
||||||
const sharedOpenAIModels = [
|
const sharedOpenAIModels = [
|
||||||
|
|
|
@ -304,6 +304,16 @@ export const getAvailableTools = (
|
||||||
return request.get(path);
|
return request.get(path);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getVerifyAgentToolAuth = (
|
||||||
|
params: q.VerifyToolAuthParams,
|
||||||
|
): Promise<q.VerifyToolAuthResponse> => {
|
||||||
|
return request.get(
|
||||||
|
endpoints.agents({
|
||||||
|
path: `tools/${params.toolId}/auth`,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
/* Files */
|
/* Files */
|
||||||
|
|
||||||
export const getFiles = (): Promise<f.TFile[]> => {
|
export const getFiles = (): Promise<f.TFile[]> => {
|
||||||
|
|
|
@ -25,6 +25,7 @@ export enum QueryKeys {
|
||||||
files = 'files',
|
files = 'files',
|
||||||
fileConfig = 'fileConfig',
|
fileConfig = 'fileConfig',
|
||||||
tools = 'tools',
|
tools = 'tools',
|
||||||
|
toolAuth = 'toolAuth',
|
||||||
agentTools = 'agentTools',
|
agentTools = 'agentTools',
|
||||||
actions = 'actions',
|
actions = 'actions',
|
||||||
assistantDocs = 'assistantDocs',
|
assistantDocs = 'assistantDocs',
|
||||||
|
|
|
@ -408,16 +408,16 @@ export const useAvailablePluginsQuery = <TData = s.TPlugin[]>(
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useUpdateUserPluginsMutation = (): UseMutationResult<
|
export const useUpdateUserPluginsMutation = (
|
||||||
t.TUser,
|
_options?: m.UpdatePluginAuthOptions,
|
||||||
unknown,
|
): UseMutationResult<t.TUser, unknown, t.TUpdateUserPlugins, unknown> => {
|
||||||
t.TUpdateUserPlugins,
|
|
||||||
unknown
|
|
||||||
> => {
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const { onSuccess, ...options } = _options ?? {};
|
||||||
return useMutation((payload: t.TUpdateUserPlugins) => dataService.updateUserPlugins(payload), {
|
return useMutation((payload: t.TUpdateUserPlugins) => dataService.updateUserPlugins(payload), {
|
||||||
onSuccess: () => {
|
...options,
|
||||||
|
onSuccess: (...args) => {
|
||||||
queryClient.invalidateQueries([QueryKeys.user]);
|
queryClient.invalidateQueries([QueryKeys.user]);
|
||||||
|
onSuccess?.(...args);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -8,7 +8,7 @@ export const isUUID = z.string().uuid();
|
||||||
export enum AuthType {
|
export enum AuthType {
|
||||||
OVERRIDE_AUTH = 'override_auth',
|
OVERRIDE_AUTH = 'override_auth',
|
||||||
USER_PROVIDED = 'user_provided',
|
USER_PROVIDED = 'user_provided',
|
||||||
SYSTEM_DEFINED = 'SYSTEM_DEFINED',
|
SYSTEM_DEFINED = 'system_defined',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const authTypeSchema = z.nativeEnum(AuthType);
|
export const authTypeSchema = z.nativeEnum(AuthType);
|
||||||
|
@ -369,7 +369,7 @@ export const tPluginSchema = z.object({
|
||||||
pluginKey: z.string(),
|
pluginKey: z.string(),
|
||||||
description: z.string(),
|
description: z.string(),
|
||||||
icon: z.string(),
|
icon: z.string(),
|
||||||
authConfig: z.array(tPluginAuthConfigSchema),
|
authConfig: z.array(tPluginAuthConfigSchema).optional(),
|
||||||
authenticated: z.boolean().optional(),
|
authenticated: z.boolean().optional(),
|
||||||
isButton: z.boolean().optional(),
|
isButton: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
|
@ -72,14 +72,13 @@ export type TPluginAction = {
|
||||||
pluginKey: string;
|
pluginKey: string;
|
||||||
action: 'install' | 'uninstall';
|
action: 'install' | 'uninstall';
|
||||||
auth?: unknown;
|
auth?: unknown;
|
||||||
isAssistantTool?: boolean;
|
isEntityTool?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GroupedConversations = [key: string, TConversation[]][];
|
export type GroupedConversations = [key: string, TConversation[]][];
|
||||||
|
|
||||||
export type TUpdateUserPlugins = {
|
export type TUpdateUserPlugins = {
|
||||||
isAssistantTool?: boolean;
|
isEntityTool?: boolean;
|
||||||
isAgentTool?: boolean;
|
|
||||||
pluginKey: string;
|
pluginKey: string;
|
||||||
action: string;
|
action: string;
|
||||||
auth?: unknown;
|
auth?: unknown;
|
||||||
|
|
|
@ -69,6 +69,7 @@ export type TFile = {
|
||||||
height?: number;
|
height?: number;
|
||||||
expiresAt?: string | Date;
|
expiresAt?: string | Date;
|
||||||
preview?: string;
|
preview?: string;
|
||||||
|
metadata?: { fileIdentifier?: string };
|
||||||
createdAt?: string | Date;
|
createdAt?: string | Date;
|
||||||
updatedAt?: string | Date;
|
updatedAt?: string | Date;
|
||||||
};
|
};
|
||||||
|
|
|
@ -248,3 +248,6 @@ export type AcceptTermsMutationOptions = MutationOptions<
|
||||||
unknown,
|
unknown,
|
||||||
void
|
void
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/* Tools */
|
||||||
|
export type UpdatePluginAuthOptions = MutationOptions<types.TUser, types.TUpdateUserPlugins>;
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import type { InfiniteData } from '@tanstack/react-query';
|
import type { InfiniteData } from '@tanstack/react-query';
|
||||||
|
import type * as s from '../schemas';
|
||||||
import type * as t from '../types';
|
import type * as t from '../types';
|
||||||
import type { TMessage, TConversation, TSharedLink, TConversationTag } from '../schemas';
|
|
||||||
|
|
||||||
export type Conversation = {
|
export type Conversation = {
|
||||||
id: string;
|
id: string;
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
participants: string[];
|
participants: string[];
|
||||||
lastMessage: string;
|
lastMessage: string;
|
||||||
conversations: TConversation[];
|
conversations: s.TConversation[];
|
||||||
};
|
};
|
||||||
|
|
||||||
// Parameters for listing conversations (e.g., for pagination)
|
// Parameters for listing conversations (e.g., for pagination)
|
||||||
|
@ -24,33 +24,33 @@ export type ConversationListParams = {
|
||||||
|
|
||||||
// Type for the response from the conversation list API
|
// Type for the response from the conversation list API
|
||||||
export type ConversationListResponse = {
|
export type ConversationListResponse = {
|
||||||
conversations: TConversation[];
|
conversations: s.TConversation[];
|
||||||
pageNumber: string;
|
pageNumber: string;
|
||||||
pageSize: string | number;
|
pageSize: string | number;
|
||||||
pages: string | number;
|
pages: string | number;
|
||||||
messages: TMessage[];
|
messages: s.TMessage[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ConversationData = InfiniteData<ConversationListResponse>;
|
export type ConversationData = InfiniteData<ConversationListResponse>;
|
||||||
export type ConversationUpdater = (
|
export type ConversationUpdater = (
|
||||||
data: ConversationData,
|
data: ConversationData,
|
||||||
conversation: TConversation,
|
conversation: s.TConversation,
|
||||||
) => ConversationData;
|
) => ConversationData;
|
||||||
|
|
||||||
export type SharedMessagesResponse = Omit<TSharedLink, 'messages'> & {
|
export type SharedMessagesResponse = Omit<s.TSharedLink, 'messages'> & {
|
||||||
messages: TMessage[];
|
messages: s.TMessage[];
|
||||||
};
|
};
|
||||||
export type SharedLinkListParams = Omit<ConversationListParams, 'isArchived' | 'conversationId'> & {
|
export type SharedLinkListParams = Omit<ConversationListParams, 'isArchived' | 'conversationId'> & {
|
||||||
isPublic?: boolean;
|
isPublic?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SharedLinksResponse = Omit<ConversationListResponse, 'conversations' | 'messages'> & {
|
export type SharedLinksResponse = Omit<ConversationListResponse, 'conversations' | 'messages'> & {
|
||||||
sharedLinks: TSharedLink[];
|
sharedLinks: s.TSharedLink[];
|
||||||
};
|
};
|
||||||
|
|
||||||
// Type for the response from the conversation list API
|
// Type for the response from the conversation list API
|
||||||
export type SharedLinkListResponse = {
|
export type SharedLinkListResponse = {
|
||||||
sharedLinks: TSharedLink[];
|
sharedLinks: s.TSharedLink[];
|
||||||
pageNumber: string;
|
pageNumber: string;
|
||||||
pageSize: string | number;
|
pageSize: string | number;
|
||||||
pages: string | number;
|
pages: string | number;
|
||||||
|
@ -71,4 +71,7 @@ export type AllPromptGroupsFilterRequest = {
|
||||||
|
|
||||||
export type AllPromptGroupsResponse = t.TPromptGroup[];
|
export type AllPromptGroupsResponse = t.TPromptGroup[];
|
||||||
|
|
||||||
export type ConversationTagsResponse = TConversationTag[];
|
export type ConversationTagsResponse = s.TConversationTag[];
|
||||||
|
|
||||||
|
export type VerifyToolAuthParams = { toolId: string };
|
||||||
|
export type VerifyToolAuthResponse = { authenticated: boolean; message?: string | s.AuthType };
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue