mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-24 04:10:15 +01:00
Merge branch 'main' into refactor/openid-strategy
This commit is contained in:
commit
083710d4c9
72 changed files with 2674 additions and 1707 deletions
|
|
@ -4,6 +4,7 @@ const {
|
|||
Constants,
|
||||
ErrorTypes,
|
||||
EModelEndpoint,
|
||||
parseTextParts,
|
||||
anthropicSettings,
|
||||
getResponseSender,
|
||||
validateVisionModel,
|
||||
|
|
@ -696,15 +697,8 @@ class AnthropicClient extends BaseClient {
|
|||
if (msg.text != null && msg.text && msg.text.startsWith(':::thinking')) {
|
||||
msg.text = msg.text.replace(/:::thinking.*?:::/gs, '').trim();
|
||||
} else if (msg.content != null) {
|
||||
/** @type {import('@librechat/agents').MessageContentComplex} */
|
||||
const newContent = [];
|
||||
for (let part of msg.content) {
|
||||
if (part.think != null) {
|
||||
continue;
|
||||
}
|
||||
newContent.push(part);
|
||||
}
|
||||
msg.content = newContent;
|
||||
msg.text = parseTextParts(msg.content, true);
|
||||
delete msg.content;
|
||||
}
|
||||
|
||||
return msg;
|
||||
|
|
|
|||
|
|
@ -676,7 +676,8 @@ class BaseClient {
|
|||
responseMessage.text = addSpaceIfNeeded(generation) + completion;
|
||||
} else if (
|
||||
Array.isArray(completion) &&
|
||||
isParamEndpoint(this.options.endpoint, this.options.endpointType)
|
||||
(this.clientName === EModelEndpoint.agents ||
|
||||
isParamEndpoint(this.options.endpoint, this.options.endpointType))
|
||||
) {
|
||||
responseMessage.text = '';
|
||||
responseMessage.content = completion;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ const {
|
|||
validateVisionModel,
|
||||
getResponseSender,
|
||||
endpointSettings,
|
||||
parseTextParts,
|
||||
EModelEndpoint,
|
||||
ContentTypes,
|
||||
VisionModes,
|
||||
|
|
@ -774,6 +775,22 @@ class GoogleClient extends BaseClient {
|
|||
return this.usage;
|
||||
}
|
||||
|
||||
getMessageMapMethod() {
|
||||
/**
|
||||
* @param {TMessage} msg
|
||||
*/
|
||||
return (msg) => {
|
||||
if (msg.text != null && msg.text && msg.text.startsWith(':::thinking')) {
|
||||
msg.text = msg.text.replace(/:::thinking.*?:::/gs, '').trim();
|
||||
} else if (msg.content != null) {
|
||||
msg.text = parseTextParts(msg.content, true);
|
||||
delete msg.content;
|
||||
}
|
||||
|
||||
return msg;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the correct token count for the current user message based on the token count map and API usage.
|
||||
* Edge case: If the calculation results in a negative value, it returns the original estimate.
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ const {
|
|||
Constants,
|
||||
ImageDetail,
|
||||
ContentTypes,
|
||||
parseTextParts,
|
||||
EModelEndpoint,
|
||||
resolveHeaders,
|
||||
KnownEndpoints,
|
||||
|
|
@ -1121,15 +1122,8 @@ ${convo}
|
|||
if (msg.text != null && msg.text && msg.text.startsWith(':::thinking')) {
|
||||
msg.text = msg.text.replace(/:::thinking.*?:::/gs, '').trim();
|
||||
} else if (msg.content != null) {
|
||||
/** @type {import('@librechat/agents').MessageContentComplex} */
|
||||
const newContent = [];
|
||||
for (let part of msg.content) {
|
||||
if (part.think != null) {
|
||||
continue;
|
||||
}
|
||||
newContent.push(part);
|
||||
}
|
||||
msg.content = newContent;
|
||||
msg.text = parseTextParts(msg.content, true);
|
||||
delete msg.content;
|
||||
}
|
||||
|
||||
return msg;
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ function createLLM({
|
|||
let credentials = { openAIApiKey };
|
||||
let configuration = {
|
||||
apiKey: openAIApiKey,
|
||||
...(configOptions.basePath && { baseURL: configOptions.basePath }),
|
||||
};
|
||||
|
||||
/** @type {AzureOptions} */
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
const axios = require('axios');
|
||||
const { EventSource } = require('eventsource');
|
||||
const { Time, CacheKeys } = require('librechat-data-provider');
|
||||
const { MCPManager, FlowStateManager } = require('librechat-mcp');
|
||||
const logger = require('./winston');
|
||||
|
||||
global.EventSource = EventSource;
|
||||
|
|
@ -9,11 +10,10 @@ let mcpManager = null;
|
|||
let flowManager = null;
|
||||
|
||||
/**
|
||||
* @returns {Promise<MCPManager>}
|
||||
* @returns {MCPManager}
|
||||
*/
|
||||
async function getMCPManager() {
|
||||
function getMCPManager() {
|
||||
if (!mcpManager) {
|
||||
const { MCPManager } = await import('librechat-mcp');
|
||||
mcpManager = MCPManager.getInstance(logger);
|
||||
}
|
||||
return mcpManager;
|
||||
|
|
@ -21,11 +21,10 @@ async function getMCPManager() {
|
|||
|
||||
/**
|
||||
* @param {(key: string) => Keyv} getLogStores
|
||||
* @returns {Promise<FlowStateManager>}
|
||||
* @returns {FlowStateManager}
|
||||
*/
|
||||
async function getFlowStateManager(getLogStores) {
|
||||
function getFlowStateManager(getLogStores) {
|
||||
if (!flowManager) {
|
||||
const { FlowStateManager } = await import('librechat-mcp');
|
||||
flowManager = new FlowStateManager(getLogStores(CacheKeys.FLOWS), {
|
||||
ttl: Time.ONE_MINUTE * 3,
|
||||
logger,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
const mongoose = require('mongoose');
|
||||
const { SystemRoles } = require('librechat-data-provider');
|
||||
const { GLOBAL_PROJECT_NAME } = require('librechat-data-provider').Constants;
|
||||
const { agentSchema } = require('@librechat/data-schemas');
|
||||
const { SystemRoles, Tools } = require('librechat-data-provider');
|
||||
const { GLOBAL_PROJECT_NAME, EPHEMERAL_AGENT_ID, mcp_delimiter } =
|
||||
require('librechat-data-provider').Constants;
|
||||
const { CONFIG_STORE, STARTUP_CONFIG } = require('librechat-data-provider').CacheKeys;
|
||||
const {
|
||||
getProjectByName,
|
||||
|
|
@ -9,7 +11,6 @@ const {
|
|||
removeAgentFromAllProjects,
|
||||
} = require('./Project');
|
||||
const getLogStores = require('~/cache/getLogStores');
|
||||
const { agentSchema } = require('@librechat/data-schemas');
|
||||
|
||||
const Agent = mongoose.model('agent', agentSchema);
|
||||
|
||||
|
|
@ -39,9 +40,61 @@ const getAgent = async (searchParameter) => await Agent.findOne(searchParameter)
|
|||
* @param {Object} params
|
||||
* @param {ServerRequest} params.req
|
||||
* @param {string} params.agent_id
|
||||
* @param {string} params.endpoint
|
||||
* @param {import('@librechat/agents').ClientOptions} [params.model_parameters]
|
||||
* @returns {Agent|null} The agent document as a plain object, or null if not found.
|
||||
*/
|
||||
const loadEphemeralAgent = ({ req, agent_id, endpoint, model_parameters: _m }) => {
|
||||
const { model, ...model_parameters } = _m;
|
||||
/** @type {Record<string, FunctionTool>} */
|
||||
const availableTools = req.app.locals.availableTools;
|
||||
const mcpServers = new Set(req.body.ephemeralAgent?.mcp);
|
||||
/** @type {string[]} */
|
||||
const tools = [];
|
||||
if (req.body.ephemeralAgent?.execute_code === true) {
|
||||
tools.push(Tools.execute_code);
|
||||
}
|
||||
|
||||
if (mcpServers.size > 0) {
|
||||
for (const toolName of Object.keys(availableTools)) {
|
||||
if (!toolName.includes(mcp_delimiter)) {
|
||||
continue;
|
||||
}
|
||||
const mcpServer = toolName.split(mcp_delimiter)?.[1];
|
||||
if (mcpServer && mcpServers.has(mcpServer)) {
|
||||
tools.push(toolName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const instructions = req.body.promptPrefix;
|
||||
return {
|
||||
id: agent_id,
|
||||
instructions,
|
||||
provider: endpoint,
|
||||
model_parameters,
|
||||
model,
|
||||
tools,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Load an agent based on the provided ID
|
||||
*
|
||||
* @param {Object} params
|
||||
* @param {ServerRequest} params.req
|
||||
* @param {string} params.agent_id
|
||||
* @param {string} params.endpoint
|
||||
* @param {import('@librechat/agents').ClientOptions} [params.model_parameters]
|
||||
* @returns {Promise<Agent|null>} The agent document as a plain object, or null if not found.
|
||||
*/
|
||||
const loadAgent = async ({ req, agent_id }) => {
|
||||
const loadAgent = async ({ req, agent_id, endpoint, model_parameters }) => {
|
||||
if (!agent_id) {
|
||||
return null;
|
||||
}
|
||||
if (agent_id === EPHEMERAL_AGENT_ID) {
|
||||
return loadEphemeralAgent({ req, agent_id, endpoint, model_parameters });
|
||||
}
|
||||
const agent = await getAgent({
|
||||
id: agent_id,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -99,6 +99,25 @@ async function updateAccessPermissions(roleName, permissionsUpdate) {
|
|||
const updatedPermissions = { ...currentPermissions };
|
||||
let hasChanges = false;
|
||||
|
||||
const unsetFields = {};
|
||||
const permissionTypes = Object.keys(permissionsSchema.shape || {});
|
||||
for (const permType of permissionTypes) {
|
||||
if (role[permType] && typeof role[permType] === 'object') {
|
||||
logger.info(
|
||||
`Migrating '${roleName}' role from old schema: found '${permType}' at top level`,
|
||||
);
|
||||
|
||||
updatedPermissions[permType] = {
|
||||
...updatedPermissions[permType],
|
||||
...role[permType],
|
||||
};
|
||||
|
||||
unsetFields[permType] = 1;
|
||||
hasChanges = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Process the current updates
|
||||
for (const [permissionType, permissions] of Object.entries(updates)) {
|
||||
const currentTypePermissions = currentPermissions[permissionType] || {};
|
||||
updatedPermissions[permissionType] = { ...currentTypePermissions };
|
||||
|
|
@ -115,8 +134,36 @@ async function updateAccessPermissions(roleName, permissionsUpdate) {
|
|||
}
|
||||
|
||||
if (hasChanges) {
|
||||
// Update only the permissions field.
|
||||
await updateRoleByName(roleName, { permissions: updatedPermissions });
|
||||
const updateObj = { permissions: updatedPermissions };
|
||||
|
||||
if (Object.keys(unsetFields).length > 0) {
|
||||
logger.info(
|
||||
`Unsetting old schema fields for '${roleName}' role: ${Object.keys(unsetFields).join(', ')}`,
|
||||
);
|
||||
|
||||
try {
|
||||
await Role.updateOne(
|
||||
{ name: roleName },
|
||||
{
|
||||
$set: updateObj,
|
||||
$unset: unsetFields,
|
||||
},
|
||||
);
|
||||
|
||||
const cache = getLogStores(CacheKeys.ROLES);
|
||||
const updatedRole = await Role.findOne({ name: roleName }).select('-__v').lean().exec();
|
||||
await cache.set(roleName, updatedRole);
|
||||
|
||||
logger.info(`Updated role '${roleName}' and removed old schema fields`);
|
||||
} catch (updateError) {
|
||||
logger.error(`Error during role migration update: ${updateError.message}`);
|
||||
throw updateError;
|
||||
}
|
||||
} else {
|
||||
// Standard update if no migration needed
|
||||
await updateRoleByName(roleName, updateObj);
|
||||
}
|
||||
|
||||
logger.info(`Updated '${roleName}' role permissions`);
|
||||
} else {
|
||||
logger.info(`No changes needed for '${roleName}' role permissions`);
|
||||
|
|
@ -155,10 +202,90 @@ const initializeRoles = async function () {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Migrates roles from old schema to new schema structure.
|
||||
* This can be called directly to fix existing roles.
|
||||
*
|
||||
* @param {string} [roleName] - Optional specific role to migrate. If not provided, migrates all roles.
|
||||
* @returns {Promise<number>} Number of roles migrated.
|
||||
*/
|
||||
const migrateRoleSchema = async function (roleName) {
|
||||
try {
|
||||
// Get roles to migrate
|
||||
let roles;
|
||||
if (roleName) {
|
||||
const role = await Role.findOne({ name: roleName });
|
||||
roles = role ? [role] : [];
|
||||
} else {
|
||||
roles = await Role.find({});
|
||||
}
|
||||
|
||||
logger.info(`Migrating ${roles.length} roles to new schema structure`);
|
||||
let migratedCount = 0;
|
||||
|
||||
for (const role of roles) {
|
||||
const permissionTypes = Object.keys(permissionsSchema.shape || {});
|
||||
const unsetFields = {};
|
||||
let hasOldSchema = false;
|
||||
|
||||
// Check for old schema fields
|
||||
for (const permType of permissionTypes) {
|
||||
if (role[permType] && typeof role[permType] === 'object') {
|
||||
hasOldSchema = true;
|
||||
|
||||
// Ensure permissions object exists
|
||||
role.permissions = role.permissions || {};
|
||||
|
||||
// Migrate permissions from old location to new
|
||||
role.permissions[permType] = {
|
||||
...role.permissions[permType],
|
||||
...role[permType],
|
||||
};
|
||||
|
||||
// Mark field for removal
|
||||
unsetFields[permType] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasOldSchema) {
|
||||
try {
|
||||
logger.info(`Migrating role '${role.name}' from old schema structure`);
|
||||
|
||||
// Simple update operation
|
||||
await Role.updateOne(
|
||||
{ _id: role._id },
|
||||
{
|
||||
$set: { permissions: role.permissions },
|
||||
$unset: unsetFields,
|
||||
},
|
||||
);
|
||||
|
||||
// Refresh cache
|
||||
const cache = getLogStores(CacheKeys.ROLES);
|
||||
const updatedRole = await Role.findById(role._id).lean().exec();
|
||||
await cache.set(role.name, updatedRole);
|
||||
|
||||
migratedCount++;
|
||||
logger.info(`Migrated role '${role.name}'`);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to migrate role '${role.name}': ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Migration complete: ${migratedCount} roles migrated`);
|
||||
return migratedCount;
|
||||
} catch (error) {
|
||||
logger.error(`Role schema migration failed: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
Role,
|
||||
getRoleByName,
|
||||
initializeRoles,
|
||||
updateRoleByName,
|
||||
updateAccessPermissions,
|
||||
migrateRoleSchema,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -44,12 +44,12 @@
|
|||
"@googleapis/youtube": "^20.0.0",
|
||||
"@keyv/mongo": "^2.1.8",
|
||||
"@keyv/redis": "^2.8.1",
|
||||
"@langchain/community": "^0.3.34",
|
||||
"@langchain/core": "^0.3.40",
|
||||
"@langchain/google-genai": "^0.1.11",
|
||||
"@langchain/google-vertexai": "^0.2.2",
|
||||
"@langchain/community": "^0.3.39",
|
||||
"@langchain/core": "^0.3.43",
|
||||
"@langchain/google-genai": "^0.2.2",
|
||||
"@langchain/google-vertexai": "^0.2.3",
|
||||
"@langchain/textsplitters": "^0.1.0",
|
||||
"@librechat/agents": "^2.3.95",
|
||||
"@librechat/agents": "^2.4.12",
|
||||
"@librechat/data-schemas": "*",
|
||||
"@waylaidwanderer/fetch-event-source": "^3.0.1",
|
||||
"axios": "^1.8.2",
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ const getAvailableTools = async (req, res) => {
|
|||
const pluginManifest = availableTools;
|
||||
const customConfig = await getCustomConfig();
|
||||
if (customConfig?.mcpServers != null) {
|
||||
const mcpManager = await getMCPManager();
|
||||
const mcpManager = getMCPManager();
|
||||
await mcpManager.loadManifestTools(pluginManifest);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,11 +20,9 @@ const {
|
|||
const {
|
||||
Constants,
|
||||
VisionModes,
|
||||
openAISchema,
|
||||
ContentTypes,
|
||||
EModelEndpoint,
|
||||
KnownEndpoints,
|
||||
anthropicSchema,
|
||||
isAgentsEndpoint,
|
||||
AgentCapabilities,
|
||||
bedrockInputSchema,
|
||||
|
|
@ -43,11 +41,18 @@ const { createRun } = require('./run');
|
|||
/** @typedef {import('@librechat/agents').MessageContentComplex} MessageContentComplex */
|
||||
/** @typedef {import('@langchain/core/runnables').RunnableConfig} RunnableConfig */
|
||||
|
||||
const providerParsers = {
|
||||
[EModelEndpoint.openAI]: openAISchema.parse,
|
||||
[EModelEndpoint.azureOpenAI]: openAISchema.parse,
|
||||
[EModelEndpoint.anthropic]: anthropicSchema.parse,
|
||||
[EModelEndpoint.bedrock]: bedrockInputSchema.parse,
|
||||
/**
|
||||
* @param {ServerRequest} req
|
||||
* @param {Agent} agent
|
||||
* @param {string} endpoint
|
||||
*/
|
||||
const payloadParser = ({ req, agent, endpoint }) => {
|
||||
if (isAgentsEndpoint(endpoint)) {
|
||||
return { model: undefined };
|
||||
} else if (endpoint === EModelEndpoint.bedrock) {
|
||||
return bedrockInputSchema.parse(agent.model_parameters);
|
||||
}
|
||||
return req.body.endpointOption.model_parameters;
|
||||
};
|
||||
|
||||
const legacyContentEndpoints = new Set([KnownEndpoints.groq, KnownEndpoints.deepseek]);
|
||||
|
|
@ -180,28 +185,19 @@ class AgentClient extends BaseClient {
|
|||
}
|
||||
|
||||
getSaveOptions() {
|
||||
const parseOptions = providerParsers[this.options.endpoint];
|
||||
let runOptions =
|
||||
this.options.endpoint === EModelEndpoint.agents
|
||||
? {
|
||||
model: undefined,
|
||||
// TODO:
|
||||
// would need to be override settings; otherwise, model needs to be undefined
|
||||
// model: this.override.model,
|
||||
// instructions: this.override.instructions,
|
||||
// additional_instructions: this.override.additional_instructions,
|
||||
}
|
||||
: {};
|
||||
|
||||
if (parseOptions) {
|
||||
try {
|
||||
runOptions = parseOptions(this.options.agent.model_parameters);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
'[api/server/controllers/agents/client.js #getSaveOptions] Error parsing options',
|
||||
error,
|
||||
);
|
||||
}
|
||||
// TODO:
|
||||
// would need to be override settings; otherwise, model needs to be undefined
|
||||
// model: this.override.model,
|
||||
// instructions: this.override.instructions,
|
||||
// additional_instructions: this.override.additional_instructions,
|
||||
let runOptions = {};
|
||||
try {
|
||||
runOptions = payloadParser(this.options);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
'[api/server/controllers/agents/client.js #getSaveOptions] Error parsing options',
|
||||
error,
|
||||
);
|
||||
}
|
||||
|
||||
return removeNullishValues(
|
||||
|
|
|
|||
|
|
@ -11,6 +11,13 @@ const { providerEndpointMap, KnownEndpoints } = require('librechat-data-provider
|
|||
* @typedef {import('@librechat/agents').IState} IState
|
||||
*/
|
||||
|
||||
const customProviders = new Set([
|
||||
Providers.XAI,
|
||||
Providers.OLLAMA,
|
||||
Providers.DEEPSEEK,
|
||||
Providers.OPENROUTER,
|
||||
]);
|
||||
|
||||
/**
|
||||
* Creates a new Run instance with custom handlers and configuration.
|
||||
*
|
||||
|
|
@ -43,8 +50,11 @@ async function createRun({
|
|||
agent.model_parameters,
|
||||
);
|
||||
|
||||
/** Resolves Mistral type strictness due to new OpenAI usage field */
|
||||
if (agent.endpoint?.toLowerCase().includes(KnownEndpoints.mistral)) {
|
||||
/** Resolves issues with new OpenAI usage field */
|
||||
if (
|
||||
customProviders.has(agent.provider) ||
|
||||
(agent.provider === Providers.OPENAI && agent.endpoint !== agent.provider)
|
||||
) {
|
||||
llmConfig.streamUsage = false;
|
||||
llmConfig.usage = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
const { parseCompactConvo, EModelEndpoint, isAgentsEndpoint } = require('librechat-data-provider');
|
||||
const { getModelsConfig } = require('~/server/controllers/ModelController');
|
||||
const {
|
||||
parseCompactConvo,
|
||||
EModelEndpoint,
|
||||
isAgentsEndpoint,
|
||||
EndpointURLs,
|
||||
} = require('librechat-data-provider');
|
||||
const azureAssistants = require('~/server/services/Endpoints/azureAssistants');
|
||||
const { getModelsConfig } = require('~/server/controllers/ModelController');
|
||||
const assistants = require('~/server/services/Endpoints/assistants');
|
||||
const gptPlugins = require('~/server/services/Endpoints/gptPlugins');
|
||||
const { processFiles } = require('~/server/services/Files/process');
|
||||
|
|
@ -77,8 +82,9 @@ async function buildEndpointOption(req, res, next) {
|
|||
}
|
||||
|
||||
try {
|
||||
const isAgents = isAgentsEndpoint(endpoint);
|
||||
const endpointFn = buildFunction[endpointType ?? endpoint];
|
||||
const isAgents =
|
||||
isAgentsEndpoint(endpoint) || req.baseUrl.startsWith(EndpointURLs[EModelEndpoint.agents]);
|
||||
const endpointFn = buildFunction[isAgents ? EModelEndpoint.agents : (endpointType ?? endpoint)];
|
||||
const builder = isAgents ? (...args) => endpointFn(req, ...args) : endpointFn;
|
||||
|
||||
// TODO: use object params
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ router.get('/:action_id/oauth/callback', async (req, res) => {
|
|||
const { action_id } = req.params;
|
||||
const { code, state } = req.query;
|
||||
|
||||
const flowManager = await getFlowStateManager(getLogStores);
|
||||
const flowManager = getFlowStateManager(getLogStores);
|
||||
let identifier = action_id;
|
||||
try {
|
||||
let decodedState;
|
||||
|
|
|
|||
|
|
@ -20,24 +20,33 @@ router.post('/abort', handleAbort());
|
|||
|
||||
const checkAgentAccess = generateCheckAccess(PermissionTypes.AGENTS, [Permissions.USE]);
|
||||
|
||||
router.use(checkAgentAccess);
|
||||
router.use(validateConvoAccess);
|
||||
router.use(buildEndpointOption);
|
||||
router.use(setHeaders);
|
||||
|
||||
const controller = async (req, res, next) => {
|
||||
await AgentController(req, res, next, initializeClient, addTitle);
|
||||
};
|
||||
|
||||
/**
|
||||
* @route POST /
|
||||
* @route POST / (regular endpoint)
|
||||
* @desc Chat with an assistant
|
||||
* @access Public
|
||||
* @param {express.Request} req - The request object, containing the request data.
|
||||
* @param {express.Response} res - The response object, used to send back a response.
|
||||
* @returns {void}
|
||||
*/
|
||||
router.post(
|
||||
'/',
|
||||
// validateModel,
|
||||
checkAgentAccess,
|
||||
validateConvoAccess,
|
||||
buildEndpointOption,
|
||||
setHeaders,
|
||||
async (req, res, next) => {
|
||||
await AgentController(req, res, next, initializeClient, addTitle);
|
||||
},
|
||||
);
|
||||
router.post('/', controller);
|
||||
|
||||
/**
|
||||
* @route POST /:endpoint (ephemeral agents)
|
||||
* @desc Chat with an assistant
|
||||
* @access Public
|
||||
* @param {express.Request} req - The request object, containing the request data.
|
||||
* @param {express.Response} res - The response object, used to send back a response.
|
||||
* @returns {void}
|
||||
*/
|
||||
router.post('/:endpoint', controller);
|
||||
|
||||
module.exports = router;
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ router.put('/:roleName/prompts', checkAdmin, async (req, res) => {
|
|||
const { roleName: _r } = req.params;
|
||||
// TODO: TEMP, use a better parsing for roleName
|
||||
const roleName = _r.toUpperCase();
|
||||
/** @type {TRole['PROMPTS']} */
|
||||
/** @type {TRole['permissions']['PROMPTS']} */
|
||||
const updates = req.body;
|
||||
|
||||
try {
|
||||
|
|
@ -59,10 +59,16 @@ router.put('/:roleName/prompts', checkAdmin, async (req, res) => {
|
|||
return res.status(404).send({ message: 'Role not found' });
|
||||
}
|
||||
|
||||
const currentPermissions =
|
||||
role.permissions?.[PermissionTypes.PROMPTS] || role[PermissionTypes.PROMPTS] || {};
|
||||
|
||||
const mergedUpdates = {
|
||||
[PermissionTypes.PROMPTS]: {
|
||||
...role[PermissionTypes.PROMPTS],
|
||||
...parsedUpdates,
|
||||
permissions: {
|
||||
...role.permissions,
|
||||
[PermissionTypes.PROMPTS]: {
|
||||
...currentPermissions,
|
||||
...parsedUpdates,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -81,7 +87,7 @@ router.put('/:roleName/agents', checkAdmin, async (req, res) => {
|
|||
const { roleName: _r } = req.params;
|
||||
// TODO: TEMP, use a better parsing for roleName
|
||||
const roleName = _r.toUpperCase();
|
||||
/** @type {TRole['AGENTS']} */
|
||||
/** @type {TRole['permissions']['AGENTS']} */
|
||||
const updates = req.body;
|
||||
|
||||
try {
|
||||
|
|
@ -92,17 +98,23 @@ router.put('/:roleName/agents', checkAdmin, async (req, res) => {
|
|||
return res.status(404).send({ message: 'Role not found' });
|
||||
}
|
||||
|
||||
const currentPermissions =
|
||||
role.permissions?.[PermissionTypes.AGENTS] || role[PermissionTypes.AGENTS] || {};
|
||||
|
||||
const mergedUpdates = {
|
||||
[PermissionTypes.AGENTS]: {
|
||||
...role[PermissionTypes.AGENTS],
|
||||
...parsedUpdates,
|
||||
permissions: {
|
||||
...role.permissions,
|
||||
[PermissionTypes.AGENTS]: {
|
||||
...currentPermissions,
|
||||
...parsedUpdates,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const updatedRole = await updateRoleByName(roleName, mergedUpdates);
|
||||
res.status(200).send(updatedRole);
|
||||
} catch (error) {
|
||||
return res.status(400).send({ message: 'Invalid prompt permissions.', error: error.errors });
|
||||
return res.status(400).send({ message: 'Invalid agent permissions.', error: error.errors });
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -189,26 +189,32 @@ async function createActionTool({
|
|||
expires_at: Date.now() + Time.TWO_MINUTES,
|
||||
},
|
||||
};
|
||||
const flowManager = await getFlowStateManager(getLogStores);
|
||||
const flowManager = getFlowStateManager(getLogStores);
|
||||
await flowManager.createFlowWithHandler(
|
||||
`${identifier}:login`,
|
||||
`${identifier}:oauth_login:${config.metadata.thread_id}:${config.metadata.run_id}`,
|
||||
'oauth_login',
|
||||
async () => {
|
||||
sendEvent(res, { event: GraphEvents.ON_RUN_STEP_DELTA, data });
|
||||
logger.debug('Sent OAuth login request to client', { action_id, identifier });
|
||||
return true;
|
||||
},
|
||||
config?.signal,
|
||||
);
|
||||
logger.debug('Waiting for OAuth Authorization response', { action_id, identifier });
|
||||
const result = await flowManager.createFlow(identifier, 'oauth', {
|
||||
state: stateToken,
|
||||
userId: req.user.id,
|
||||
client_url: metadata.auth.client_url,
|
||||
redirect_uri: `${process.env.DOMAIN_CLIENT}/api/actions/${action_id}/oauth/callback`,
|
||||
/** Encrypted values */
|
||||
encrypted_oauth_client_id: encrypted.oauth_client_id,
|
||||
encrypted_oauth_client_secret: encrypted.oauth_client_secret,
|
||||
});
|
||||
const result = await flowManager.createFlow(
|
||||
identifier,
|
||||
'oauth',
|
||||
{
|
||||
state: stateToken,
|
||||
userId: req.user.id,
|
||||
client_url: metadata.auth.client_url,
|
||||
redirect_uri: `${process.env.DOMAIN_CLIENT}/api/actions/${action_id}/oauth/callback`,
|
||||
/** Encrypted values */
|
||||
encrypted_oauth_client_id: encrypted.oauth_client_id,
|
||||
encrypted_oauth_client_secret: encrypted.oauth_client_secret,
|
||||
},
|
||||
config?.signal,
|
||||
);
|
||||
logger.debug('Received OAuth Authorization response', { action_id, identifier });
|
||||
data.delta.auth = undefined;
|
||||
data.delta.expires_at = undefined;
|
||||
|
|
@ -259,11 +265,12 @@ async function createActionTool({
|
|||
encrypted_oauth_client_id: encrypted.oauth_client_id,
|
||||
encrypted_oauth_client_secret: encrypted.oauth_client_secret,
|
||||
});
|
||||
const flowManager = await getFlowStateManager(getLogStores);
|
||||
const flowManager = getFlowStateManager(getLogStores);
|
||||
const refreshData = await flowManager.createFlowWithHandler(
|
||||
`${identifier}:refresh`,
|
||||
'oauth_refresh',
|
||||
refreshTokens,
|
||||
config?.signal,
|
||||
);
|
||||
metadata.oauth_access_token = refreshData.access_token;
|
||||
if (refreshData.refresh_token) {
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ const AppService = async (app) => {
|
|||
});
|
||||
|
||||
if (config.mcpServers != null) {
|
||||
const mcpManager = await getMCPManager();
|
||||
const mcpManager = getMCPManager();
|
||||
await mcpManager.initializeMCP(config.mcpServers, processMCPEnv);
|
||||
await mcpManager.mapAvailableTools(availableTools);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
const { isAgentsEndpoint, Constants } = require('librechat-data-provider');
|
||||
const { loadAgent } = require('~/models/Agent');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
const buildOptions = (req, endpoint, parsedBody) => {
|
||||
const buildOptions = (req, endpoint, parsedBody, endpointType) => {
|
||||
const { spec, iconURL, agent_id, instructions, maxContextTokens, ...model_parameters } =
|
||||
parsedBody;
|
||||
const agentPromise = loadAgent({
|
||||
req,
|
||||
agent_id,
|
||||
agent_id: isAgentsEndpoint(endpoint) ? agent_id : Constants.EPHEMERAL_AGENT_ID,
|
||||
endpoint,
|
||||
model_parameters,
|
||||
}).catch((error) => {
|
||||
logger.error(`[/agents/:${agent_id}] Error retrieving agent during build options step`, error);
|
||||
return undefined;
|
||||
|
|
@ -17,6 +20,7 @@ const buildOptions = (req, endpoint, parsedBody) => {
|
|||
iconURL,
|
||||
endpoint,
|
||||
agent_id,
|
||||
endpointType,
|
||||
instructions,
|
||||
maxContextTokens,
|
||||
model_parameters,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
const { createContentAggregator, Providers } = require('@librechat/agents');
|
||||
const {
|
||||
Constants,
|
||||
ErrorTypes,
|
||||
EModelEndpoint,
|
||||
getResponseSender,
|
||||
|
|
@ -322,10 +323,14 @@ const initializeClient = async ({ req, res, endpointOption }) => {
|
|||
agent: primaryConfig,
|
||||
spec: endpointOption.spec,
|
||||
iconURL: endpointOption.iconURL,
|
||||
endpoint: EModelEndpoint.agents,
|
||||
attachments: primaryConfig.attachments,
|
||||
endpointType: endpointOption.endpointType,
|
||||
maxContextTokens: primaryConfig.maxContextTokens,
|
||||
resendFiles: primaryConfig.model_parameters?.resendFiles ?? true,
|
||||
endpoint:
|
||||
primaryConfig.id === Constants.EPHEMERAL_AGENT_ID
|
||||
? primaryConfig.endpoint
|
||||
: EModelEndpoint.agents,
|
||||
});
|
||||
|
||||
return { client };
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ async function createMCPTool({ req, toolKey, provider }) {
|
|||
/** @type {(toolArguments: Object | string, config?: GraphRunnableConfig) => Promise<unknown>} */
|
||||
const _call = async (toolArguments, config) => {
|
||||
try {
|
||||
const mcpManager = await getMCPManager();
|
||||
const mcpManager = getMCPManager();
|
||||
const result = await mcpManager.callTool({
|
||||
serverName,
|
||||
toolName,
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@
|
|||
"tailwindcss": "^3.4.1",
|
||||
"ts-jest": "^29.2.5",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^6.2.3",
|
||||
"vite": "^6.2.5",
|
||||
"vite-plugin-compression2": "^1.3.3",
|
||||
"vite-plugin-node-polyfills": "^0.23.0",
|
||||
"vite-plugin-pwa": "^0.21.2"
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ import {
|
|||
SandpackCodeEditor,
|
||||
SandpackProvider as StyledProvider,
|
||||
} from '@codesandbox/sandpack-react';
|
||||
import { SandpackProviderProps } from '@codesandbox/sandpack-react/unstyled';
|
||||
import type { SandpackProviderProps } from '@codesandbox/sandpack-react/unstyled';
|
||||
import type { SandpackBundlerFile } from '@codesandbox/sandpack-client';
|
||||
import type { CodeEditorRef } from '@codesandbox/sandpack-react';
|
||||
import type { ArtifactFiles, Artifact } from '~/common';
|
||||
import { useEditArtifact, useGetStartupConfig } from '~/data-provider';
|
||||
|
|
@ -66,7 +67,7 @@ const CodeEditor = ({
|
|||
return;
|
||||
}
|
||||
|
||||
const currentCode = sandpack.files['/' + fileKey].code;
|
||||
const currentCode = (sandpack.files['/' + fileKey] as SandpackBundlerFile | undefined)?.code;
|
||||
|
||||
if (currentCode && artifact.content != null && currentCode.trim() !== artifact.content.trim()) {
|
||||
setCurrentCode(currentCode);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React, {
|
||||
memo,
|
||||
useState,
|
||||
useRef,
|
||||
useEffect,
|
||||
|
|
@ -9,14 +10,18 @@ import React, {
|
|||
} from 'react';
|
||||
import { useRecoilValue, useRecoilCallback } from 'recoil';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
import CodeInterpreter from './CodeInterpreter';
|
||||
import type { BadgeItem } from '~/common';
|
||||
import { useChatBadges } from '~/hooks';
|
||||
import { Badge } from '~/components/ui';
|
||||
import MCPSelect from './MCPSelect';
|
||||
import store from '~/store';
|
||||
|
||||
interface BadgeRowProps {
|
||||
showEphemeralBadges?: boolean;
|
||||
onChange: (badges: Pick<BadgeItem, 'id'>[]) => void;
|
||||
onToggle?: (badgeId: string, currentActive: boolean) => void;
|
||||
conversationId?: string | null;
|
||||
isInChat: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -33,7 +38,8 @@ interface BadgeWrapperProps {
|
|||
const BadgeWrapper = React.memo(
|
||||
forwardRef<HTMLDivElement, BadgeWrapperProps>(
|
||||
({ badge, isEditing, isInChat, onToggle, onDelete, onMouseDown, badgeRefs }, ref) => {
|
||||
const isActive = badge.atom ? useRecoilValue(badge.atom) : false;
|
||||
const atomBadge = useRecoilValue(badge.atom);
|
||||
const isActive = badge.atom ? atomBadge : false;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -126,7 +132,13 @@ const dragReducer = (state: DragState, action: DragAction): DragState => {
|
|||
}
|
||||
};
|
||||
|
||||
export function BadgeRow({ onChange, onToggle, isInChat }: BadgeRowProps) {
|
||||
function BadgeRow({
|
||||
showEphemeralBadges,
|
||||
conversationId,
|
||||
onChange,
|
||||
onToggle,
|
||||
isInChat,
|
||||
}: BadgeRowProps) {
|
||||
const [orderedBadges, setOrderedBadges] = useState<BadgeItem[]>([]);
|
||||
const [dragState, dispatch] = useReducer(dragReducer, {
|
||||
draggedBadge: null,
|
||||
|
|
@ -141,7 +153,7 @@ export function BadgeRow({ onChange, onToggle, isInChat }: BadgeRowProps) {
|
|||
const animationFrame = useRef<number | null>(null);
|
||||
const containerRectRef = useRef<DOMRect | null>(null);
|
||||
|
||||
const allBadges = useChatBadges() || [];
|
||||
const allBadges = useChatBadges();
|
||||
const isEditing = useRecoilValue(store.isEditingBadges);
|
||||
|
||||
const badges = useMemo(
|
||||
|
|
@ -340,6 +352,12 @@ export function BadgeRow({ onChange, onToggle, isInChat }: BadgeRowProps) {
|
|||
/>
|
||||
</div>
|
||||
)}
|
||||
{showEphemeralBadges === true && (
|
||||
<>
|
||||
<CodeInterpreter conversationId={conversationId} />
|
||||
<MCPSelect conversationId={conversationId} />
|
||||
</>
|
||||
)}
|
||||
{ghostBadge && (
|
||||
<div
|
||||
className="ghost-badge h-full"
|
||||
|
|
@ -367,3 +385,5 @@ export function BadgeRow({ onChange, onToggle, isInChat }: BadgeRowProps) {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(BadgeRow);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { memo, useRef, useMemo, useEffect, useState, useCallback } from 'react';
|
||||
import { useWatch } from 'react-hook-form';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { Constants, isAssistantsEndpoint } from 'librechat-data-provider';
|
||||
import { Constants, isAssistantsEndpoint, isAgentsEndpoint } from 'librechat-data-provider';
|
||||
import {
|
||||
useChatContext,
|
||||
useChatFormContext,
|
||||
|
|
@ -28,8 +28,8 @@ import CollapseChat from './CollapseChat';
|
|||
import StreamAudio from './StreamAudio';
|
||||
import StopButton from './StopButton';
|
||||
import SendButton from './SendButton';
|
||||
import { BadgeRow } from './BadgeRow';
|
||||
import EditBadges from './EditBadges';
|
||||
import BadgeRow from './BadgeRow';
|
||||
import Mention from './Mention';
|
||||
import store from '~/store';
|
||||
|
||||
|
|
@ -289,7 +289,9 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
|
|||
<AttachFileChat disableInputs={disableInputs} />
|
||||
</div>
|
||||
<BadgeRow
|
||||
onChange={(newBadges) => setBadges(newBadges)}
|
||||
showEphemeralBadges={!isAgentsEndpoint(endpoint) && !isAssistantsEndpoint(endpoint)}
|
||||
conversationId={conversation?.conversationId ?? Constants.NEW_CONVO}
|
||||
onChange={setBadges}
|
||||
isInChat={
|
||||
Array.isArray(conversation?.messages) && conversation.messages.length >= 1
|
||||
}
|
||||
|
|
|
|||
119
client/src/components/Chat/Input/CodeInterpreter.tsx
Normal file
119
client/src/components/Chat/Input/CodeInterpreter.tsx
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
import debounce from 'lodash/debounce';
|
||||
import React, { memo, useMemo, useCallback } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { TerminalSquareIcon } from 'lucide-react';
|
||||
import {
|
||||
Tools,
|
||||
AuthType,
|
||||
Constants,
|
||||
LocalStorageKeys,
|
||||
PermissionTypes,
|
||||
Permissions,
|
||||
} from 'librechat-data-provider';
|
||||
import ApiKeyDialog from '~/components/SidePanel/Agents/Code/ApiKeyDialog';
|
||||
import { useLocalize, useHasAccess, useCodeApiKeyForm } from '~/hooks';
|
||||
import CheckboxButton from '~/components/ui/CheckboxButton';
|
||||
import useLocalStorage from '~/hooks/useLocalStorageAlt';
|
||||
import { useVerifyAgentToolAuth } from '~/data-provider';
|
||||
import { ephemeralAgentByConvoId } from '~/store';
|
||||
|
||||
const storageCondition = (value: unknown, rawCurrentValue?: string | null) => {
|
||||
if (rawCurrentValue) {
|
||||
try {
|
||||
const currentValue = rawCurrentValue?.trim() ?? '';
|
||||
if (currentValue === 'true' && value === false) {
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
return value !== undefined && value !== null && value !== '' && value !== false;
|
||||
};
|
||||
|
||||
function CodeInterpreter({ conversationId }: { conversationId?: string | null }) {
|
||||
const localize = useLocalize();
|
||||
const key = conversationId ?? Constants.NEW_CONVO;
|
||||
|
||||
const canRunCode = useHasAccess({
|
||||
permissionType: PermissionTypes.RUN_CODE,
|
||||
permission: Permissions.USE,
|
||||
});
|
||||
const [ephemeralAgent, setEphemeralAgent] = useRecoilState(ephemeralAgentByConvoId(key));
|
||||
const isCodeToggleEnabled = useMemo(() => {
|
||||
return ephemeralAgent?.execute_code ?? false;
|
||||
}, [ephemeralAgent?.execute_code]);
|
||||
|
||||
const { data } = useVerifyAgentToolAuth(
|
||||
{ toolId: Tools.execute_code },
|
||||
{
|
||||
retry: 1,
|
||||
},
|
||||
);
|
||||
const authType = useMemo(() => data?.message ?? false, [data?.message]);
|
||||
const isAuthenticated = useMemo(() => data?.authenticated ?? false, [data?.authenticated]);
|
||||
const { methods, onSubmit, isDialogOpen, setIsDialogOpen, handleRevokeApiKey } =
|
||||
useCodeApiKeyForm({});
|
||||
|
||||
const setValue = useCallback(
|
||||
(isChecked: boolean) => {
|
||||
setEphemeralAgent((prev) => ({
|
||||
...prev,
|
||||
execute_code: isChecked,
|
||||
}));
|
||||
},
|
||||
[setEphemeralAgent],
|
||||
);
|
||||
|
||||
const [runCode, setRunCode] = useLocalStorage<boolean>(
|
||||
`${LocalStorageKeys.LAST_CODE_TOGGLE_}${key}`,
|
||||
isCodeToggleEnabled,
|
||||
setValue,
|
||||
storageCondition,
|
||||
);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(isChecked: boolean) => {
|
||||
if (!isAuthenticated) {
|
||||
setIsDialogOpen(true);
|
||||
return;
|
||||
}
|
||||
setRunCode(isChecked);
|
||||
},
|
||||
[setRunCode, setIsDialogOpen, isAuthenticated],
|
||||
);
|
||||
|
||||
const debouncedChange = useMemo(
|
||||
() => debounce(handleChange, 50, { leading: true }),
|
||||
[handleChange],
|
||||
);
|
||||
|
||||
if (!canRunCode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<CheckboxButton
|
||||
className="max-w-fit"
|
||||
defaultChecked={runCode}
|
||||
setValue={debouncedChange}
|
||||
label={localize('com_assistants_code_interpreter')}
|
||||
isCheckedClassName="border-purple-600/40 bg-purple-500/10 hover:bg-purple-700/10"
|
||||
icon={<TerminalSquareIcon className="icon-md" />}
|
||||
/>
|
||||
<ApiKeyDialog
|
||||
onSubmit={onSubmit}
|
||||
isOpen={isDialogOpen}
|
||||
register={methods.register}
|
||||
onRevoke={handleRevokeApiKey}
|
||||
onOpenChange={setIsDialogOpen}
|
||||
handleSubmit={methods.handleSubmit}
|
||||
isToolAuthenticated={isAuthenticated}
|
||||
isUserProvided={authType === AuthType.USER_PROVIDED}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(CodeInterpreter);
|
||||
|
|
@ -1,24 +1,31 @@
|
|||
import { memo, useMemo } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import {
|
||||
Constants,
|
||||
supportsFiles,
|
||||
mergeFileConfig,
|
||||
isAgentsEndpoint,
|
||||
isEphemeralAgent,
|
||||
EndpointFileConfig,
|
||||
fileConfig as defaultFileConfig,
|
||||
} from 'librechat-data-provider';
|
||||
import { useChatContext } from '~/Providers';
|
||||
import { useGetFileConfig } from '~/data-provider';
|
||||
import { ephemeralAgentByConvoId } from '~/store';
|
||||
import AttachFileMenu from './AttachFileMenu';
|
||||
import AttachFile from './AttachFile';
|
||||
import store from '~/store';
|
||||
|
||||
function AttachFileChat({ disableInputs }: { disableInputs: boolean }) {
|
||||
const { conversation } = useChatContext();
|
||||
|
||||
const { endpoint: _endpoint, endpointType } = conversation ?? { endpoint: null };
|
||||
|
||||
const isAgents = useMemo(() => isAgentsEndpoint(_endpoint), [_endpoint]);
|
||||
const key = conversation?.conversationId ?? Constants.NEW_CONVO;
|
||||
const ephemeralAgent = useRecoilValue(ephemeralAgentByConvoId(key));
|
||||
const isAgents = useMemo(
|
||||
() => isAgentsEndpoint(_endpoint) || isEphemeralAgent(_endpoint, ephemeralAgent),
|
||||
[_endpoint, ephemeralAgent],
|
||||
);
|
||||
|
||||
const { data: fileConfig = defaultFileConfig } = useGetFileConfig({
|
||||
select: (data) => mergeFileConfig(data),
|
||||
|
|
|
|||
|
|
@ -18,7 +18,9 @@ const AttachFile = ({ disabled }: AttachFileProps) => {
|
|||
const [isPopoverActive, setIsPopoverActive] = useState(false);
|
||||
const [toolResource, setToolResource] = useState<EToolResources | undefined>();
|
||||
const { data: endpointsConfig } = useGetEndpointsQuery();
|
||||
const { handleFileChange } = useFileHandling();
|
||||
const { handleFileChange } = useFileHandling({
|
||||
overrideEndpoint: EModelEndpoint.agents,
|
||||
});
|
||||
|
||||
const capabilities = useMemo(
|
||||
() => endpointsConfig?.[EModelEndpoint.agents]?.capabilities ?? [],
|
||||
|
|
|
|||
|
|
@ -1,22 +1,40 @@
|
|||
import type { TFile } from 'librechat-data-provider';
|
||||
import type { ExtendedFile } from '~/common';
|
||||
import { getFileType, cn } from '~/utils';
|
||||
import FilePreview from './FilePreview';
|
||||
import RemoveFile from './RemoveFile';
|
||||
import { getFileType } from '~/utils';
|
||||
|
||||
const FileContainer = ({
|
||||
file,
|
||||
overrideType,
|
||||
buttonClassName,
|
||||
containerClassName,
|
||||
onDelete,
|
||||
onClick,
|
||||
}: {
|
||||
file: ExtendedFile | TFile;
|
||||
file: Partial<ExtendedFile | TFile>;
|
||||
overrideType?: string;
|
||||
buttonClassName?: string;
|
||||
containerClassName?: string;
|
||||
onDelete?: () => void;
|
||||
onClick?: React.MouseEventHandler<HTMLButtonElement>;
|
||||
}) => {
|
||||
const fileType = getFileType(file.type);
|
||||
const fileType = getFileType(overrideType ?? file.type);
|
||||
|
||||
return (
|
||||
<div className="group relative inline-block text-sm text-text-primary">
|
||||
<div className="relative overflow-hidden rounded-2xl border border-border-light">
|
||||
<div className="w-56 bg-surface-hover-alt p-1.5">
|
||||
<div
|
||||
className={cn('group relative inline-block text-sm text-text-primary', containerClassName)}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
aria-label={file.filename}
|
||||
className={cn(
|
||||
'relative overflow-hidden rounded-2xl border border-border-light bg-surface-hover-alt',
|
||||
buttonClassName,
|
||||
)}
|
||||
>
|
||||
<div className="w-56 p-1.5">
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<FilePreview file={file} fileType={fileType} className="relative" />
|
||||
<div className="overflow-hidden">
|
||||
|
|
@ -29,7 +47,7 @@ const FileContainer = ({
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
{onDelete && <RemoveFile onRemove={onDelete} />}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ const FilePreview = ({
|
|||
fileType,
|
||||
className = '',
|
||||
}: {
|
||||
file?: ExtendedFile | TFile;
|
||||
file?: Partial<ExtendedFile | TFile>;
|
||||
fileType: {
|
||||
paths: React.FC;
|
||||
fill: string;
|
||||
|
|
|
|||
123
client/src/components/Chat/Input/MCPSelect.tsx
Normal file
123
client/src/components/Chat/Input/MCPSelect.tsx
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
import React, { memo, useRef, useMemo, useEffect, useCallback } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { Constants, EModelEndpoint, LocalStorageKeys } from 'librechat-data-provider';
|
||||
import { useAvailableToolsQuery } from '~/data-provider';
|
||||
import useLocalStorage from '~/hooks/useLocalStorageAlt';
|
||||
import MultiSelect from '~/components/ui/MultiSelect';
|
||||
import { ephemeralAgentByConvoId } from '~/store';
|
||||
import MCPIcon from '~/components/ui/MCPIcon';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
const storageCondition = (value: unknown, rawCurrentValue?: string | null) => {
|
||||
if (rawCurrentValue) {
|
||||
try {
|
||||
const currentValue = rawCurrentValue?.trim() ?? '';
|
||||
if (currentValue.length > 2) {
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
return Array.isArray(value) && value.length > 0;
|
||||
};
|
||||
|
||||
function MCPSelect({ conversationId }: { conversationId?: string | null }) {
|
||||
const localize = useLocalize();
|
||||
const key = conversationId ?? Constants.NEW_CONVO;
|
||||
const hasSetFetched = useRef<string | null>(null);
|
||||
|
||||
const { data: mcpServerSet, isFetched } = useAvailableToolsQuery(EModelEndpoint.agents, {
|
||||
select: (data) => {
|
||||
const serverNames = new Set<string>();
|
||||
data.forEach((tool) => {
|
||||
if (tool.pluginKey.includes(Constants.mcp_delimiter)) {
|
||||
const parts = tool.pluginKey.split(Constants.mcp_delimiter);
|
||||
serverNames.add(parts[parts.length - 1]);
|
||||
}
|
||||
});
|
||||
return serverNames;
|
||||
},
|
||||
});
|
||||
|
||||
const [ephemeralAgent, setEphemeralAgent] = useRecoilState(ephemeralAgentByConvoId(key));
|
||||
const mcpState = useMemo(() => {
|
||||
return ephemeralAgent?.mcp ?? [];
|
||||
}, [ephemeralAgent?.mcp]);
|
||||
|
||||
const setSelectedValues = useCallback(
|
||||
(values: string[] | null | undefined) => {
|
||||
if (!values) {
|
||||
return;
|
||||
}
|
||||
if (!Array.isArray(values)) {
|
||||
return;
|
||||
}
|
||||
setEphemeralAgent((prev) => ({
|
||||
...prev,
|
||||
mcp: values,
|
||||
}));
|
||||
},
|
||||
[setEphemeralAgent],
|
||||
);
|
||||
const [mcpValues, setMCPValues] = useLocalStorage<string[]>(
|
||||
`${LocalStorageKeys.LAST_MCP_}${key}`,
|
||||
mcpState,
|
||||
setSelectedValues,
|
||||
storageCondition,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasSetFetched.current === key) {
|
||||
return;
|
||||
}
|
||||
if (!isFetched) {
|
||||
return;
|
||||
}
|
||||
hasSetFetched.current = key;
|
||||
if ((mcpServerSet?.size ?? 0) > 0) {
|
||||
setMCPValues(mcpValues.filter((mcp) => mcpServerSet?.has(mcp)));
|
||||
return;
|
||||
}
|
||||
setMCPValues([]);
|
||||
}, [isFetched, setMCPValues, mcpServerSet, key, mcpValues]);
|
||||
|
||||
const renderSelectedValues = useCallback(
|
||||
(values: string[], placeholder?: string) => {
|
||||
if (values.length === 0) {
|
||||
return placeholder || localize('com_ui_select') + '...';
|
||||
}
|
||||
if (values.length === 1) {
|
||||
return values[0];
|
||||
}
|
||||
return localize('com_ui_x_selected', { 0: values.length });
|
||||
},
|
||||
[localize],
|
||||
);
|
||||
|
||||
const mcpServers = useMemo(() => {
|
||||
return Array.from(mcpServerSet ?? []);
|
||||
}, [mcpServerSet]);
|
||||
|
||||
if (!mcpServerSet || mcpServerSet.size === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<MultiSelect
|
||||
items={mcpServers ?? []}
|
||||
selectedValues={mcpValues ?? []}
|
||||
setSelectedValues={setMCPValues}
|
||||
defaultSelectedValues={mcpValues ?? []}
|
||||
renderSelectedValues={renderSelectedValues}
|
||||
placeholder={localize('com_ui_mcp_servers')}
|
||||
popoverClassName="min-w-fit"
|
||||
className="badge-icon min-w-fit"
|
||||
selectIcon={<MCPIcon className="icon-md text-text-primary" />}
|
||||
selectItemsClassName="border border-blue-600/50 bg-blue-500/10 hover:bg-blue-700/10"
|
||||
selectClassName="group relative inline-flex items-center justify-center md:justify-start gap-1.5 rounded-full border border-border-medium text-sm font-medium transition-shadow md:w-full size-9 p-2 md:p-3 bg-surface-chat shadow-sm hover:bg-surface-hover hover:shadow-md active:shadow-inner"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(MCPSelect);
|
||||
|
|
@ -52,7 +52,7 @@ export default function Landing({ centerFormOnLanding }: { centerFormOnLanding:
|
|||
});
|
||||
|
||||
const name = entity?.name ?? '';
|
||||
const description = entity?.description ?? '';
|
||||
const description = (entity?.description || conversation?.greeting) ?? '';
|
||||
|
||||
const getGreeting = useCallback(() => {
|
||||
if (typeof startupConfig?.interface?.customWelcome === 'string') {
|
||||
|
|
@ -186,8 +186,8 @@ export default function Landing({ centerFormOnLanding }: { centerFormOnLanding:
|
|||
/>
|
||||
)}
|
||||
</div>
|
||||
{(isAgent || isAssistant) && description && (
|
||||
<div className="animate-fadeIn mt-2 max-w-md text-center text-sm font-normal text-text-primary">
|
||||
{description && (
|
||||
<div className="animate-fadeIn mt-4 max-w-md text-center text-sm font-normal text-text-primary">
|
||||
{description}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,27 @@
|
|||
import { memo } from 'react';
|
||||
import { imageExtRegex } from 'librechat-data-provider';
|
||||
import type { TAttachment, TFile, TAttachmentMetadata } from 'librechat-data-provider';
|
||||
import FileContainer from '~/components/Chat/Input/Files/FileContainer';
|
||||
import Image from '~/components/Chat/Messages/Content/Image';
|
||||
import { useAttachmentLink } from './LogLink';
|
||||
|
||||
const FileAttachment = memo(({ attachment }: { attachment: TAttachment }) => {
|
||||
const { handleDownload } = useAttachmentLink({
|
||||
href: attachment.filepath,
|
||||
filename: attachment.filename,
|
||||
});
|
||||
const extension = attachment.filename.split('.').pop();
|
||||
|
||||
return (
|
||||
<FileContainer
|
||||
file={attachment}
|
||||
onClick={handleDownload}
|
||||
overrideType={extension}
|
||||
containerClassName="max-w-fit"
|
||||
buttonClassName="hover:cursor-pointer hover:bg-surface-secondary active:bg-surface-secondary focus:bg-surface-secondary hover:border-border-heavy active:border-border-heavy"
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default function Attachment({ attachment }: { attachment?: TAttachment }) {
|
||||
if (!attachment) {
|
||||
|
|
@ -21,5 +42,5 @@ export default function Attachment({ attachment }: { attachment?: TAttachment })
|
|||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
return <FileAttachment attachment={attachment} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import MarkdownLite from '~/components/Chat/Messages/Content/MarkdownLite';
|
|||
import { useProgress, useLocalize } from '~/hooks';
|
||||
import { CodeInProgress } from './CodeProgress';
|
||||
import Attachment from './Attachment';
|
||||
import LogContent from './LogContent';
|
||||
import Stdout from './Stdout';
|
||||
import store from '~/store';
|
||||
|
||||
interface ParsedArgs {
|
||||
|
|
@ -17,8 +17,17 @@ interface ParsedArgs {
|
|||
|
||||
export function useParseArgs(args: string): ParsedArgs {
|
||||
return useMemo(() => {
|
||||
let parsedArgs: ParsedArgs | string = args;
|
||||
try {
|
||||
parsedArgs = JSON.parse(args);
|
||||
} catch {
|
||||
// console.error('Failed to parse args:', e);
|
||||
}
|
||||
if (typeof parsedArgs === 'object') {
|
||||
return parsedArgs;
|
||||
}
|
||||
const langMatch = args.match(/"lang"\s*:\s*"(\w+)"/);
|
||||
const codeMatch = args.match(/"code"\s*:\s*"(.+?)(?="\s*,\s*"args"|$)/s);
|
||||
const codeMatch = args.match(/"code"\s*:\s*"(.+?)(?="\s*,\s*"(session_id|args)"|"\s*})/s);
|
||||
|
||||
let code = '';
|
||||
if (codeMatch) {
|
||||
|
|
@ -26,7 +35,7 @@ export function useParseArgs(args: string): ParsedArgs {
|
|||
if (code.endsWith('"}')) {
|
||||
code = code.slice(0, -2);
|
||||
}
|
||||
code = code.replace(/\\n/g, '\n').replace(/\\/g, '');
|
||||
code = code.replace(/\\n/g, '\n').replace(/\\"/g, '"').replace(/\\\\/g, '\\');
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -99,15 +108,17 @@ export default function ExecuteCode({
|
|||
color: 'white',
|
||||
}}
|
||||
>
|
||||
<pre className="shrink-0">
|
||||
<LogContent output={output} attachments={attachments} />
|
||||
</pre>
|
||||
<Stdout output={output} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{attachments?.map((attachment, index) => <Attachment attachment={attachment} key={index} />)}
|
||||
<div className="mb-2 flex flex-wrap items-center gap-2.5">
|
||||
{attachments?.map((attachment, index) => (
|
||||
<Attachment attachment={attachment} key={index} />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,11 +8,11 @@ interface LogLinkProps {
|
|||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const LogLink: React.FC<LogLinkProps> = ({ href, filename, children }) => {
|
||||
export const useAttachmentLink = ({ href, filename }: Pick<LogLinkProps, 'href' | 'filename'>) => {
|
||||
const { showToast } = useToastContext();
|
||||
const { refetch: downloadFile } = useCodeOutputDownload(href);
|
||||
|
||||
const handleDownload = async (event: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
const handleDownload = async (event: React.MouseEvent<HTMLAnchorElement | HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
try {
|
||||
const stream = await downloadFile();
|
||||
|
|
@ -36,6 +36,11 @@ const LogLink: React.FC<LogLinkProps> = ({ href, filename, children }) => {
|
|||
}
|
||||
};
|
||||
|
||||
return { handleDownload };
|
||||
};
|
||||
|
||||
const LogLink: React.FC<LogLinkProps> = ({ href, filename, children }) => {
|
||||
const { handleDownload } = useAttachmentLink({ href, filename });
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
|
|
|
|||
26
client/src/components/Chat/Messages/Content/Parts/Stdout.tsx
Normal file
26
client/src/components/Chat/Messages/Content/Parts/Stdout.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import React, { useMemo } from 'react';
|
||||
|
||||
interface StdoutProps {
|
||||
output?: string;
|
||||
}
|
||||
|
||||
const Stdout: React.FC<StdoutProps> = ({ output = '' }) => {
|
||||
const processedContent = useMemo(() => {
|
||||
if (!output) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const parts = output.split('Generated files:');
|
||||
return parts[0].trim();
|
||||
}, [output]);
|
||||
|
||||
return (
|
||||
processedContent && (
|
||||
<pre className="shrink-0">
|
||||
<div>{processedContent}</div>
|
||||
</pre>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default Stdout;
|
||||
|
|
@ -3,6 +3,7 @@ import { useClearConversationsMutation } from 'librechat-data-provider/react-que
|
|||
import { Label, Button, OGDialog, OGDialogTrigger, Spinner } from '~/components';
|
||||
import { useLocalize, useNewConvo } from '~/hooks';
|
||||
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
|
||||
import { clearAllConversationStorage } from '~/utils';
|
||||
|
||||
export const ClearChats = () => {
|
||||
const localize = useLocalize();
|
||||
|
|
@ -15,6 +16,7 @@ export const ClearChats = () => {
|
|||
{},
|
||||
{
|
||||
onSuccess: () => {
|
||||
clearAllConversationStorage();
|
||||
newConversation();
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -80,10 +80,10 @@ const AdminSettings = () => {
|
|||
const [selectedRole, setSelectedRole] = useState<SystemRoles>(SystemRoles.USER);
|
||||
|
||||
const defaultValues = useMemo(() => {
|
||||
if (roles?.[selectedRole]) {
|
||||
return roles[selectedRole][PermissionTypes.PROMPTS];
|
||||
if (roles?.[selectedRole]?.permissions) {
|
||||
return roles[selectedRole].permissions[PermissionTypes.PROMPTS];
|
||||
}
|
||||
return roleDefaults[selectedRole][PermissionTypes.PROMPTS];
|
||||
return roleDefaults[selectedRole].permissions[PermissionTypes.PROMPTS];
|
||||
}, [roles, selectedRole]);
|
||||
|
||||
const {
|
||||
|
|
@ -99,10 +99,10 @@ const AdminSettings = () => {
|
|||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (roles?.[selectedRole]?.[PermissionTypes.PROMPTS]) {
|
||||
reset(roles[selectedRole][PermissionTypes.PROMPTS]);
|
||||
if (roles?.[selectedRole]?.permissions?.[PermissionTypes.PROMPTS]) {
|
||||
reset(roles[selectedRole].permissions[PermissionTypes.PROMPTS]);
|
||||
} else {
|
||||
reset(roleDefaults[selectedRole][PermissionTypes.PROMPTS]);
|
||||
reset(roleDefaults[selectedRole].permissions[PermissionTypes.PROMPTS]);
|
||||
}
|
||||
}, [roles, selectedRole, reset]);
|
||||
|
||||
|
|
|
|||
|
|
@ -72,10 +72,10 @@ const AdminSettings = () => {
|
|||
const [selectedRole, setSelectedRole] = useState<SystemRoles>(SystemRoles.USER);
|
||||
|
||||
const defaultValues = useMemo(() => {
|
||||
if (roles?.[selectedRole]) {
|
||||
return roles[selectedRole][PermissionTypes.AGENTS];
|
||||
if (roles?.[selectedRole]?.permissions) {
|
||||
return roles[selectedRole].permissions[PermissionTypes.AGENTS];
|
||||
}
|
||||
return roleDefaults[selectedRole][PermissionTypes.AGENTS];
|
||||
return roleDefaults[selectedRole].permissions[PermissionTypes.AGENTS];
|
||||
}, [roles, selectedRole]);
|
||||
|
||||
const {
|
||||
|
|
@ -91,10 +91,10 @@ const AdminSettings = () => {
|
|||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (roles?.[selectedRole]?.[PermissionTypes.AGENTS]) {
|
||||
reset(roles[selectedRole][PermissionTypes.AGENTS]);
|
||||
if (roles?.[selectedRole]?.permissions?.[PermissionTypes.AGENTS]) {
|
||||
reset(roles[selectedRole].permissions[PermissionTypes.AGENTS]);
|
||||
} else {
|
||||
reset(roleDefaults[selectedRole][PermissionTypes.AGENTS]);
|
||||
reset(roleDefaults[selectedRole].permissions[PermissionTypes.AGENTS]);
|
||||
}
|
||||
}, [roles, selectedRole, reset]);
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ export default function FileIcon({
|
|||
file,
|
||||
fileType,
|
||||
}: {
|
||||
file?: ExtendedFile | TFile;
|
||||
file?: Partial<ExtendedFile | TFile>;
|
||||
fileType: {
|
||||
fill: string;
|
||||
paths: React.FC;
|
||||
|
|
|
|||
62
client/src/components/ui/CheckboxButton.tsx
Normal file
62
client/src/components/ui/CheckboxButton.tsx
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { useEffect } from 'react';
|
||||
import { Checkbox, useStoreState, useCheckboxStore } from '@ariakit/react';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
export default function CheckboxButton({
|
||||
label,
|
||||
icon,
|
||||
setValue,
|
||||
className,
|
||||
defaultChecked,
|
||||
isCheckedClassName,
|
||||
}: {
|
||||
label: string;
|
||||
className?: string;
|
||||
icon?: React.ReactNode;
|
||||
defaultChecked?: boolean;
|
||||
isCheckedClassName?: string;
|
||||
setValue?: (isChecked: boolean) => void;
|
||||
}) {
|
||||
const checkbox = useCheckboxStore();
|
||||
const isChecked = useStoreState(checkbox, (state) => state?.value);
|
||||
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
e.stopPropagation();
|
||||
if (typeof isChecked !== 'boolean') {
|
||||
return;
|
||||
}
|
||||
setValue?.(!isChecked);
|
||||
};
|
||||
useEffect(() => {
|
||||
if (defaultChecked) {
|
||||
checkbox.setValue(defaultChecked);
|
||||
}
|
||||
}, [defaultChecked, checkbox]);
|
||||
|
||||
return (
|
||||
<Checkbox
|
||||
store={checkbox}
|
||||
onChange={onChange}
|
||||
defaultChecked={defaultChecked}
|
||||
className={cn(
|
||||
// Base styling from MultiSelect's selectClassName
|
||||
'group relative inline-flex items-center justify-center gap-1.5',
|
||||
'rounded-full border border-border-medium text-sm font-medium',
|
||||
'size-9 p-2 transition-shadow md:w-full md:p-3',
|
||||
'bg-surface-chat shadow-sm hover:bg-surface-hover hover:shadow-md active:shadow-inner',
|
||||
|
||||
// Checked state styling
|
||||
isChecked && isCheckedClassName && isCheckedClassName,
|
||||
|
||||
// Additional custom classes
|
||||
className,
|
||||
)}
|
||||
render={<button type="button" aria-label={label} />}
|
||||
>
|
||||
{/* Icon if provided */}
|
||||
{icon && <span className="icon-md text-text-primary">{icon}</span>}
|
||||
|
||||
{/* Show the label on larger screens */}
|
||||
<span className="hidden truncate md:block">{label}</span>
|
||||
</Checkbox>
|
||||
);
|
||||
}
|
||||
|
|
@ -83,6 +83,7 @@ const DropdownPopup: React.FC<DropdownProps> = ({
|
|||
)}
|
||||
{item.label}
|
||||
{item.kbd != null && (
|
||||
// eslint-disable-next-line i18next/no-literal-string
|
||||
<kbd className="ml-auto hidden font-sans text-xs text-black/50 group-hover:inline group-focus:inline dark:text-white/50">
|
||||
⌘{item.kbd}
|
||||
</kbd>
|
||||
|
|
|
|||
31
client/src/components/ui/MCPIcon.tsx
Normal file
31
client/src/components/ui/MCPIcon.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
export default function MCPIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
width="195"
|
||||
height="195"
|
||||
viewBox="0 2 195 195"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
d="M25 97.8528L92.8823 29.9706C102.255 20.598 117.451 20.598 126.823 29.9706V29.9706C136.196 39.3431 136.196 54.5391 126.823 63.9117L75.5581 115.177"
|
||||
stroke="currentColor"
|
||||
strokeWidth="12"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M76.2653 114.47L126.823 63.9117C136.196 54.5391 151.392 54.5391 160.765 63.9117L161.118 64.2652C170.491 73.6378 170.491 88.8338 161.118 98.2063L99.7248 159.6C96.6006 162.724 96.6006 167.789 99.7248 170.913L112.331 183.52"
|
||||
stroke="currentColor"
|
||||
strokeWidth="12"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M109.853 46.9411L59.6482 97.1457C50.2757 106.518 50.2757 121.714 59.6482 131.087V131.087C69.0208 140.459 84.2168 140.459 93.5894 131.087L143.794 80.8822"
|
||||
stroke="currentColor"
|
||||
strokeWidth="12"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
129
client/src/components/ui/MultiSelect.tsx
Normal file
129
client/src/components/ui/MultiSelect.tsx
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
import React, { useRef } from 'react';
|
||||
import {
|
||||
Select,
|
||||
SelectArrow,
|
||||
SelectItem,
|
||||
SelectItemCheck,
|
||||
SelectLabel,
|
||||
SelectPopover,
|
||||
SelectProvider,
|
||||
} from '@ariakit/react';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
interface MultiSelectProps<T extends string> {
|
||||
items: T[];
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
defaultSelectedValues?: T[];
|
||||
onSelectedValuesChange?: (values: T[]) => void;
|
||||
renderSelectedValues?: (values: T[], placeholder?: string) => React.ReactNode;
|
||||
className?: string;
|
||||
itemClassName?: string;
|
||||
labelClassName?: string;
|
||||
selectClassName?: string;
|
||||
selectIcon?: React.ReactNode;
|
||||
popoverClassName?: string;
|
||||
selectItemsClassName?: string;
|
||||
selectedValues: T[];
|
||||
setSelectedValues: (values: T[]) => void;
|
||||
}
|
||||
|
||||
function defaultRender<T extends string>(values: T[], placeholder?: string) {
|
||||
if (values.length === 0) {
|
||||
return placeholder || 'Select...';
|
||||
}
|
||||
if (values.length === 1) {
|
||||
return values[0];
|
||||
}
|
||||
return `${values.length} items selected`;
|
||||
}
|
||||
|
||||
export default function MultiSelect<T extends string>({
|
||||
items,
|
||||
label,
|
||||
placeholder = 'Select...',
|
||||
defaultSelectedValues = [],
|
||||
onSelectedValuesChange,
|
||||
renderSelectedValues = defaultRender,
|
||||
className,
|
||||
selectIcon,
|
||||
itemClassName,
|
||||
labelClassName,
|
||||
selectClassName,
|
||||
popoverClassName,
|
||||
selectItemsClassName,
|
||||
selectedValues = [],
|
||||
setSelectedValues,
|
||||
}: MultiSelectProps<T>) {
|
||||
const selectRef = useRef<HTMLButtonElement>(null);
|
||||
// const [selectedValues, setSelectedValues] = React.useState<T[]>(defaultSelectedValues);
|
||||
|
||||
const handleValueChange = (values: T[]) => {
|
||||
setSelectedValues(values);
|
||||
if (onSelectedValuesChange) {
|
||||
onSelectedValuesChange(values);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<SelectProvider value={selectedValues} setValue={handleValueChange}>
|
||||
{label && (
|
||||
<SelectLabel className={cn('mb-1 block text-sm text-text-primary', labelClassName)}>
|
||||
{label}
|
||||
</SelectLabel>
|
||||
)}
|
||||
<Select
|
||||
ref={selectRef}
|
||||
className={cn(
|
||||
'flex items-center justify-between gap-2 rounded-xl px-3 py-2 text-sm',
|
||||
'bg-surface-tertiary text-text-primary shadow-sm hover:cursor-pointer hover:bg-surface-hover',
|
||||
'outline-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75',
|
||||
selectClassName,
|
||||
selectedValues.length > 0 && selectItemsClassName != null && selectItemsClassName,
|
||||
)}
|
||||
onChange={(e) => e.stopPropagation()}
|
||||
>
|
||||
{selectIcon && selectIcon}
|
||||
<span className="mr-auto hidden truncate md:block">
|
||||
{renderSelectedValues(selectedValues, placeholder)}
|
||||
</span>
|
||||
<SelectArrow className="ml-1 hidden stroke-1 text-base opacity-75 md:block" />
|
||||
</Select>
|
||||
<SelectPopover
|
||||
gutter={4}
|
||||
sameWidth
|
||||
modal
|
||||
unmountOnHide
|
||||
finalFocus={selectRef}
|
||||
className={cn(
|
||||
'animate-popover z-50 flex max-h-[300px]',
|
||||
'flex-col overflow-auto overscroll-contain rounded-xl',
|
||||
'bg-surface-secondary px-1.5 py-1 text-text-primary shadow-lg',
|
||||
'border border-border-light',
|
||||
'outline-none',
|
||||
popoverClassName,
|
||||
)}
|
||||
>
|
||||
{items.map((value) => (
|
||||
<SelectItem
|
||||
key={value}
|
||||
value={value}
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-lg px-2 py-1.5 hover:cursor-pointer',
|
||||
'scroll-m-1 outline-none transition-colors',
|
||||
'hover:bg-black/[0.075] dark:hover:bg-white/10',
|
||||
'data-[active-item]:bg-black/[0.075] dark:data-[active-item]:bg-white/10',
|
||||
'w-full min-w-0 text-sm',
|
||||
itemClassName,
|
||||
)}
|
||||
>
|
||||
<SelectItemCheck className="text-primary" />
|
||||
<span className="truncate">{value}</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectPopover>
|
||||
</SelectProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -27,6 +27,7 @@ export * from './Pagination';
|
|||
export * from './Progress';
|
||||
export * from './InputOTP';
|
||||
export { default as Badge } from './Badge';
|
||||
export { default as MCPIcon } from './MCPIcon';
|
||||
export { default as Combobox } from './Combobox';
|
||||
export { default as Dropdown } from './Dropdown';
|
||||
export { default as SplitText } from './SplitText';
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { MutationKeys, QueryKeys, dataService, request } from 'librechat-data-pr
|
|||
import type { UseMutationResult } from '@tanstack/react-query';
|
||||
import type * as t from 'librechat-data-provider';
|
||||
import useClearStates from '~/hooks/Config/useClearStates';
|
||||
import { clearAllConversationStorage } from '~/utils';
|
||||
import store from '~/store';
|
||||
|
||||
/* login/logout */
|
||||
|
|
@ -79,6 +80,7 @@ export const useDeleteUserMutation = (
|
|||
onSuccess: (...args) => {
|
||||
resetDefaultPreset();
|
||||
clearStates();
|
||||
clearAllConversationStorage();
|
||||
queryClient.removeQueries();
|
||||
options?.onSuccess?.(...args);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {
|
|||
updateConvoFields,
|
||||
updateConversation,
|
||||
deleteConversation,
|
||||
clearConversationStorage,
|
||||
} from '~/utils';
|
||||
|
||||
export type TGenTitleMutation = UseMutationResult<
|
||||
|
|
@ -562,6 +563,7 @@ export const useDeleteConversationMutation = (
|
|||
const current = queryClient.getQueryData<t.ConversationData>([QueryKeys.allConversations]);
|
||||
refetch({ refetchPage: (page, index) => index === (current?.pages.length ?? 1) - 1 });
|
||||
onSuccess?.(_data, vars, context);
|
||||
clearConversationStorage(conversationId);
|
||||
},
|
||||
..._options,
|
||||
},
|
||||
|
|
@ -897,7 +899,7 @@ export const useUploadAssistantAvatarMutation = (
|
|||
unknown // context
|
||||
> => {
|
||||
return useMutation([MutationKeys.assistantAvatarUpload], {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
|
||||
mutationFn: ({ postCreation, ...variables }: t.AssistantAvatarVariables) =>
|
||||
dataService.uploadAssistantAvatar(variables),
|
||||
...(options || {}),
|
||||
|
|
|
|||
|
|
@ -191,9 +191,10 @@ export const useConversationTagsQuery = (
|
|||
/**
|
||||
* Hook for getting all available tools for Assistants
|
||||
*/
|
||||
export const useAvailableToolsQuery = (
|
||||
export const useAvailableToolsQuery = <TData = t.TPlugin[]>(
|
||||
endpoint: t.AssistantsEndpoint | EModelEndpoint.agents,
|
||||
): QueryObserverResult<TPlugin[]> => {
|
||||
config?: UseQueryOptions<t.TPlugin[], unknown, TData>,
|
||||
): QueryObserverResult<TData> => {
|
||||
const queryClient = useQueryClient();
|
||||
const endpointsConfig = queryClient.getQueryData<TEndpointsConfig>([QueryKeys.endpoints]);
|
||||
const keyExpiry = queryClient.getQueryData<TCheckUserKeyResponse>([QueryKeys.name, endpoint]);
|
||||
|
|
@ -202,7 +203,7 @@ export const useAvailableToolsQuery = (
|
|||
const enabled = !!endpointsConfig?.[endpoint] && keyProvided;
|
||||
const version: string | number | undefined =
|
||||
endpointsConfig?.[endpoint]?.version ?? defaultAssistantsVersion[endpoint];
|
||||
return useQuery<TPlugin[]>(
|
||||
return useQuery<t.TPlugin[], unknown, TData>(
|
||||
[QueryKeys.tools],
|
||||
() => dataService.getAvailableTools(endpoint, version),
|
||||
{
|
||||
|
|
@ -210,6 +211,7 @@ export const useAvailableToolsQuery = (
|
|||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
enabled,
|
||||
...config,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -20,10 +20,10 @@ import type { SetterOrUpdater } from 'recoil';
|
|||
import type { TAskFunction, ExtendedFile } from '~/common';
|
||||
import useSetFilesToDelete from '~/hooks/Files/useSetFilesToDelete';
|
||||
import useGetSender from '~/hooks/Conversations/useGetSender';
|
||||
import store, { useGetEphemeralAgent } from '~/store';
|
||||
import { getArtifactsMode } from '~/utils/artifacts';
|
||||
import { getEndpointField, logger } from '~/utils';
|
||||
import useUserKey from '~/hooks/Input/useUserKey';
|
||||
import store from '~/store';
|
||||
|
||||
const logChatRequest = (request: Record<string, unknown>) => {
|
||||
logger.log('=====================================\nAsk function called with:');
|
||||
|
|
@ -64,6 +64,7 @@ export default function useChatFunctions({
|
|||
setSubmission: SetterOrUpdater<TSubmission | null>;
|
||||
setLatestMessage?: SetterOrUpdater<TMessage | null>;
|
||||
}) {
|
||||
const getEphemeralAgent = useGetEphemeralAgent();
|
||||
const codeArtifacts = useRecoilValue(store.codeArtifacts);
|
||||
const includeShadcnui = useRecoilValue(store.includeShadcnui);
|
||||
const customPromptMode = useRecoilValue(store.customPromptMode);
|
||||
|
|
@ -118,6 +119,7 @@ export default function useChatFunctions({
|
|||
return;
|
||||
}
|
||||
|
||||
const ephemeralAgent = getEphemeralAgent(conversationId ?? Constants.NEW_CONVO);
|
||||
const isEditOrContinue = isEdited || isContinued;
|
||||
|
||||
let currentMessages: TMessage[] | null = overrideMessages ?? getMessages() ?? [];
|
||||
|
|
@ -297,6 +299,7 @@ export default function useChatFunctions({
|
|||
isRegenerate,
|
||||
initialResponse,
|
||||
isTemporary,
|
||||
ephemeralAgent,
|
||||
};
|
||||
|
||||
if (isRegenerate) {
|
||||
|
|
|
|||
|
|
@ -4,22 +4,28 @@ import { useRecoilValue } from 'recoil';
|
|||
import { NativeTypes } from 'react-dnd-html5-backend';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
isAgentsEndpoint,
|
||||
EModelEndpoint,
|
||||
AgentCapabilities,
|
||||
Constants,
|
||||
QueryKeys,
|
||||
EModelEndpoint,
|
||||
isAgentsEndpoint,
|
||||
isEphemeralAgent,
|
||||
AgentCapabilities,
|
||||
} from 'librechat-data-provider';
|
||||
import type * as t from 'librechat-data-provider';
|
||||
import type { DropTargetMonitor } from 'react-dnd';
|
||||
import useFileHandling from './useFileHandling';
|
||||
import store from '~/store';
|
||||
import store, { ephemeralAgentByConvoId } from '~/store';
|
||||
|
||||
export default function useDragHelpers() {
|
||||
const queryClient = useQueryClient();
|
||||
const { handleFiles } = useFileHandling();
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [draggedFiles, setDraggedFiles] = useState<File[]>([]);
|
||||
const conversation = useRecoilValue(store.conversationByIndex(0)) || undefined;
|
||||
const key = useMemo(
|
||||
() => conversation?.conversationId ?? Constants.NEW_CONVO,
|
||||
[conversation?.conversationId],
|
||||
);
|
||||
const ephemeralAgent = useRecoilValue(ephemeralAgentByConvoId(key));
|
||||
|
||||
const handleOptionSelect = (toolResource: string | undefined) => {
|
||||
handleFiles(draggedFiles, toolResource);
|
||||
|
|
@ -28,10 +34,16 @@ export default function useDragHelpers() {
|
|||
};
|
||||
|
||||
const isAgents = useMemo(
|
||||
() => isAgentsEndpoint(conversation?.endpoint),
|
||||
[conversation?.endpoint],
|
||||
() =>
|
||||
isAgentsEndpoint(conversation?.endpoint) ||
|
||||
isEphemeralAgent(conversation?.endpoint, ephemeralAgent),
|
||||
[conversation?.endpoint, ephemeralAgent],
|
||||
);
|
||||
|
||||
const { handleFiles } = useFileHandling({
|
||||
overrideEndpoint: isAgents ? EModelEndpoint.agents : undefined,
|
||||
});
|
||||
|
||||
const [{ canDrop, isOver }, drop] = useDrop(
|
||||
() => ({
|
||||
accept: [NativeTypes.FILE],
|
||||
|
|
@ -61,7 +73,7 @@ export default function useDragHelpers() {
|
|||
canDrop: monitor.canDrop(),
|
||||
}),
|
||||
}),
|
||||
[],
|
||||
[handleFiles],
|
||||
);
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ export const useAutoSave = ({
|
|||
|
||||
const handleInput = debounce((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const value = e.target.value;
|
||||
if (value) {
|
||||
if (value && value.length > 1) {
|
||||
localStorage.setItem(
|
||||
`${LocalStorageKeys.TEXT_DRAFT}${conversationId}`,
|
||||
encodeBase64(value),
|
||||
|
|
|
|||
|
|
@ -31,11 +31,11 @@ import {
|
|||
} from '~/utils';
|
||||
import useAttachmentHandler from '~/hooks/SSE/useAttachmentHandler';
|
||||
import useContentHandler from '~/hooks/SSE/useContentHandler';
|
||||
import store, { useApplyNewAgentTemplate } from '~/store';
|
||||
import useStepHandler from '~/hooks/SSE/useStepHandler';
|
||||
import { useAuthContext } from '~/hooks/AuthContext';
|
||||
import { MESSAGE_UPDATE_INTERVAL } from '~/common';
|
||||
import { useLiveAnnouncer } from '~/Providers';
|
||||
import store from '~/store';
|
||||
|
||||
type TSyncData = {
|
||||
sync: boolean;
|
||||
|
|
@ -140,8 +140,9 @@ export default function useEventHandlers({
|
|||
resetLatestMessage,
|
||||
}: EventHandlerParams) {
|
||||
const queryClient = useQueryClient();
|
||||
const setAbortScroll = useSetRecoilState(store.abortScroll);
|
||||
const { announcePolite } = useLiveAnnouncer();
|
||||
const applyAgentTemplate = useApplyNewAgentTemplate();
|
||||
const setAbortScroll = useSetRecoilState(store.abortScroll);
|
||||
|
||||
const lastAnnouncementTimeRef = useRef(Date.now());
|
||||
const { conversationId: paramId } = useParams();
|
||||
|
|
@ -364,6 +365,9 @@ export default function useEventHandlers({
|
|||
});
|
||||
|
||||
let update = {} as TConversation;
|
||||
if (conversationId) {
|
||||
applyAgentTemplate(conversationId, submission.conversation.conversationId);
|
||||
}
|
||||
if (setConversation && !isAddedRequest) {
|
||||
setConversation((prevState) => {
|
||||
let title = prevState?.title;
|
||||
|
|
|
|||
|
|
@ -123,11 +123,14 @@ export default function useStepHandler({
|
|||
} else if (contentType === ContentTypes.TOOL_CALL && 'tool_call' in contentPart) {
|
||||
const existingContent = updatedContent[index] as Agents.ToolCallContent | undefined;
|
||||
const existingToolCall = existingContent?.tool_call;
|
||||
const toolCallArgs = (contentPart.tool_call.args as unknown as string | undefined) ?? '';
|
||||
|
||||
const args = finalUpdate
|
||||
? contentPart.tool_call.args
|
||||
: (existingToolCall?.args ?? '') + toolCallArgs;
|
||||
const toolCallArgs = (contentPart.tool_call as Agents.ToolCall).args;
|
||||
/** When args are a valid object, they are likely already invoked */
|
||||
const args =
|
||||
finalUpdate ||
|
||||
typeof existingToolCall?.args === 'object' ||
|
||||
typeof toolCallArgs === 'object'
|
||||
? contentPart.tool_call.args
|
||||
: (existingToolCall?.args ?? '') + (toolCallArgs ?? '');
|
||||
|
||||
const id = getNonEmptyValue([contentPart.tool_call.id, existingToolCall?.id]) ?? '';
|
||||
const name = getNonEmptyValue([contentPart.tool_call.name, existingToolCall?.name]) ?? '';
|
||||
|
|
@ -195,12 +198,31 @@ export default function useStepHandler({
|
|||
|
||||
// Store tool call IDs if present
|
||||
if (runStep.stepDetails.type === StepTypes.TOOL_CALLS) {
|
||||
runStep.stepDetails.tool_calls.forEach((toolCall) => {
|
||||
let updatedResponse = { ...response };
|
||||
(runStep.stepDetails.tool_calls as Agents.ToolCall[]).forEach((toolCall) => {
|
||||
const toolCallId = toolCall.id ?? '';
|
||||
if ('id' in toolCall && toolCallId) {
|
||||
toolCallIdMap.current.set(runStep.id, toolCallId);
|
||||
}
|
||||
|
||||
const contentPart: Agents.MessageContentComplex = {
|
||||
type: ContentTypes.TOOL_CALL,
|
||||
tool_call: {
|
||||
name: toolCall.name ?? '',
|
||||
args: toolCall.args,
|
||||
id: toolCallId,
|
||||
},
|
||||
};
|
||||
|
||||
updatedResponse = updateContent(updatedResponse, runStep.index, contentPart);
|
||||
});
|
||||
|
||||
messageMap.current.set(responseMessageId, updatedResponse);
|
||||
const updatedMessages = messages.map((msg) =>
|
||||
msg.messageId === runStep.runId ? updatedResponse : msg,
|
||||
);
|
||||
|
||||
setMessages(updatedMessages);
|
||||
}
|
||||
} else if (event === 'on_agent_update') {
|
||||
const { agent_update } = data as Agents.AgentUpdate;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { useMemo } from 'react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { MessageCircleDashed, Box } from 'lucide-react';
|
||||
import type { BadgeItem } from '~/common';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { useLocalize, TranslationKeys } from '~/hooks';
|
||||
import store from '~/store';
|
||||
|
||||
interface ChatBadgeConfig {
|
||||
|
|
@ -25,15 +26,22 @@ const badgeConfig: ReadonlyArray<ChatBadgeConfig> = [
|
|||
export default function useChatBadges(): BadgeItem[] {
|
||||
const localize = useLocalize();
|
||||
const activeBadges = useRecoilValue(store.chatBadges) as Array<{ id: string }>;
|
||||
const activeBadgeIds = new Set(activeBadges.map((badge) => badge.id));
|
||||
|
||||
return badgeConfig.map((cfg) => ({
|
||||
id: cfg.id,
|
||||
label: localize(cfg.label),
|
||||
icon: cfg.icon,
|
||||
atom: cfg.atom,
|
||||
isAvailable: activeBadgeIds.has(cfg.id),
|
||||
}));
|
||||
const activeBadgeIds = useMemo(
|
||||
() => new Set(activeBadges.map((badge) => badge.id)),
|
||||
[activeBadges],
|
||||
);
|
||||
const allBadges = useMemo(() => {
|
||||
return (
|
||||
badgeConfig.map((cfg) => ({
|
||||
id: cfg.id,
|
||||
label: localize(cfg.label as TranslationKeys),
|
||||
icon: cfg.icon,
|
||||
atom: cfg.atom,
|
||||
isAvailable: activeBadgeIds.has(cfg.id),
|
||||
})) || []
|
||||
);
|
||||
}, [activeBadgeIds, localize]);
|
||||
return allBadges;
|
||||
}
|
||||
|
||||
export function useResetChatBadges() {
|
||||
|
|
|
|||
69
client/src/hooks/useLocalStorageAlt.tsx
Normal file
69
client/src/hooks/useLocalStorageAlt.tsx
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
/* `useLocalStorage`
|
||||
*
|
||||
* Features:
|
||||
* - JSON Serializing
|
||||
* - Also value will be updated everywhere, when value updated (via `storage` event)
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export default function useLocalStorage<T>(
|
||||
key: string,
|
||||
defaultValue: T,
|
||||
globalSetState?: (value: T) => void,
|
||||
storageCondition?: (value: T, rawCurrentValue?: string | null) => boolean,
|
||||
): [T, (value: T) => void] {
|
||||
const [value, setValue] = useState(defaultValue);
|
||||
|
||||
useEffect(() => {
|
||||
const item = localStorage.getItem(key);
|
||||
|
||||
if (!item && !storageCondition) {
|
||||
localStorage.setItem(key, JSON.stringify(defaultValue));
|
||||
} else if (!item && storageCondition && storageCondition(defaultValue)) {
|
||||
localStorage.setItem(key, JSON.stringify(defaultValue));
|
||||
}
|
||||
|
||||
const initialValue = item && item !== 'undefined' ? JSON.parse(item) : defaultValue;
|
||||
setValue(initialValue);
|
||||
if (globalSetState) {
|
||||
globalSetState(initialValue);
|
||||
}
|
||||
|
||||
function handler(e: StorageEvent) {
|
||||
if (e.key !== key) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lsi = localStorage.getItem(key);
|
||||
setValue(JSON.parse(lsi ?? ''));
|
||||
}
|
||||
|
||||
window.addEventListener('storage', handler);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('storage', handler);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [key, globalSetState]);
|
||||
|
||||
const setValueWrap = (value: T) => {
|
||||
try {
|
||||
setValue(value);
|
||||
const storeLocal = () => {
|
||||
localStorage.setItem(key, JSON.stringify(value));
|
||||
window?.dispatchEvent(new StorageEvent('storage', { key }));
|
||||
};
|
||||
if (!storageCondition) {
|
||||
storeLocal();
|
||||
} else if (storageCondition(value, localStorage.getItem(key))) {
|
||||
storeLocal();
|
||||
}
|
||||
globalSetState?.(value);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
return [value, setValueWrap];
|
||||
}
|
||||
|
|
@ -482,7 +482,7 @@
|
|||
"com_ui_agent_editing_allowed": "Other users can already edit this agent",
|
||||
"com_ui_agent_recursion_limit": "Max Agent Steps",
|
||||
"com_ui_agent_recursion_limit_info": "Limits how many steps the agent can take in a run before giving a final response. Default is 25 steps. A step is either an AI API request or a tool usage round. For example, a basic tool interaction takes 3 steps: initial request, tool usage, and follow-up request.",
|
||||
"com_ui_agent_shared_to_all": "something needs to go here. was empty",
|
||||
"com_ui_agent_shared_to_all": "Agent is currently shared to all",
|
||||
"com_ui_agent_var": "{{0}} agent",
|
||||
"com_ui_agents": "Agents",
|
||||
"com_ui_agents_allow_create": "Allow creating Agents",
|
||||
|
|
@ -689,6 +689,7 @@
|
|||
"com_ui_include_shadcnui_agent": "Include shadcn/ui instructions",
|
||||
"com_ui_input": "Input",
|
||||
"com_ui_instructions": "Instructions",
|
||||
"com_ui_x_selected": "{{0}} selected",
|
||||
"com_ui_late_night": "Happy late night",
|
||||
"com_ui_latest_footer": "Every AI for Everyone.",
|
||||
"com_ui_latest_production_version": "Latest production version",
|
||||
|
|
@ -701,6 +702,7 @@
|
|||
"com_ui_logo": "{{0}} Logo",
|
||||
"com_ui_manage": "Manage",
|
||||
"com_ui_max_tags": "Maximum number allowed is {{0}}, using latest values.",
|
||||
"com_ui_mcp_servers": "MCP Servers",
|
||||
"com_ui_mention": "Mention an endpoint, assistant, or preset to quickly switch to it",
|
||||
"com_ui_min_tags": "Cannot remove more values, a minimum of {{0}} are required.",
|
||||
"com_ui_misc": "Misc.",
|
||||
|
|
@ -855,12 +857,6 @@
|
|||
"com_ui_write": "Writing",
|
||||
"com_ui_yes": "Yes",
|
||||
"com_ui_zoom": "Zoom",
|
||||
"com_ui_save_badge_changes": "Save badge changes?",
|
||||
"com_ui_late_night": "Happy late night",
|
||||
"com_ui_weekend_morning": "Happy weekend",
|
||||
"com_ui_good_morning": "Good morning",
|
||||
"com_ui_good_afternoon": "Good afternoon",
|
||||
"com_ui_good_evening": "Good evening",
|
||||
"com_endpoint_deprecated": "Deprecated",
|
||||
"com_endpoint_deprecated_info": "This endpoint is deprecated and may be removed in future versions, please use the agent endpoint instead",
|
||||
"com_endpoint_deprecated_info_a11y": "The plugin endpoint is deprecated and may be removed in future versions, please use the agent endpoint instead",
|
||||
|
|
|
|||
88
client/src/store/agents.ts
Normal file
88
client/src/store/agents.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import { Constants } from 'librechat-data-provider';
|
||||
import { atomFamily, useRecoilCallback } from 'recoil';
|
||||
import type { TEphemeralAgent } from 'librechat-data-provider';
|
||||
import { logger } from '~/utils';
|
||||
|
||||
export const ephemeralAgentByConvoId = atomFamily<TEphemeralAgent | null, string>({
|
||||
key: 'ephemeralAgentByConvoId',
|
||||
default: null,
|
||||
effects: [
|
||||
({ onSet, node }) => {
|
||||
onSet(async (newValue) => {
|
||||
const conversationId = node.key.split('__')[1]?.replaceAll('"', '');
|
||||
logger.log('agents', 'Setting ephemeral agent:', { conversationId, newValue });
|
||||
});
|
||||
},
|
||||
] as const,
|
||||
});
|
||||
|
||||
/**
|
||||
* Creates a callback function to apply the ephemeral agent state
|
||||
* from the "new" conversation template to a specified conversation ID.
|
||||
*/
|
||||
export function useApplyNewAgentTemplate() {
|
||||
const applyTemplate = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
async (targetId: string, _sourceId: string | null = Constants.NEW_CONVO) => {
|
||||
const sourceId = _sourceId || Constants.NEW_CONVO;
|
||||
logger.log('agents', `Attempting to apply template from "${sourceId}" to "${targetId}"`);
|
||||
|
||||
if (targetId === sourceId) {
|
||||
logger.warn('agents', `Attempted to apply template to itself ("${sourceId}"). Skipping.`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. Get the current agent state from the "new" conversation template using snapshot
|
||||
// getPromise reads the value without subscribing
|
||||
const agentTemplate = await snapshot.getPromise(ephemeralAgentByConvoId(sourceId));
|
||||
|
||||
// 2. Check if a template state actually exists
|
||||
if (agentTemplate) {
|
||||
logger.log('agents', `Applying agent template to "${targetId}":`, agentTemplate);
|
||||
// 3. Set the state for the target conversation ID using the template value
|
||||
set(ephemeralAgentByConvoId(targetId), agentTemplate);
|
||||
} else {
|
||||
// 4. Handle the case where the "new" template has no agent state (is null)
|
||||
logger.warn(
|
||||
'agents',
|
||||
`Agent template from "${sourceId}" is null or unset. Setting agent for "${targetId}" to null.`,
|
||||
);
|
||||
// Explicitly set to null (or a default empty state if preferred)
|
||||
set(ephemeralAgentByConvoId(targetId), null);
|
||||
// Example: Or set to a default empty state:
|
||||
// set(ephemeralAgentByConvoId(targetId), { mcp: [] });
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
'agents',
|
||||
`Error applying agent template from "${sourceId}" to "${targetId}":`,
|
||||
error,
|
||||
);
|
||||
set(ephemeralAgentByConvoId(targetId), null);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return applyTemplate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a callback function to get the current ephemeral agent state
|
||||
* for a specified conversation ID without subscribing the component.
|
||||
* Returns a Loadable object synchronously.
|
||||
*/
|
||||
export function useGetEphemeralAgent() {
|
||||
const getEphemeralAgent = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
(conversationId: string): TEphemeralAgent | null => {
|
||||
logger.log('agents', `[useGetEphemeralAgent] Getting loadable for ID: ${conversationId}`);
|
||||
const agentLoadable = snapshot.getLoadable(ephemeralAgentByConvoId(conversationId));
|
||||
return agentLoadable.contents as TEphemeralAgent | null;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return getEphemeralAgent;
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ import lang from './language';
|
|||
import settings from './settings';
|
||||
import misc from './misc';
|
||||
import isTemporary from './temporary';
|
||||
export * from './agents';
|
||||
|
||||
export default {
|
||||
...artifacts,
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ export const fileTypes = {
|
|||
title: 'File',
|
||||
},
|
||||
text: textDocument,
|
||||
txt: textDocument,
|
||||
// application:,
|
||||
|
||||
/* Partial matches */
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { LocalStorageKeys, TConversation } from 'librechat-data-provider';
|
||||
import { LocalStorageKeys, TConversation, isUUID } from 'librechat-data-provider';
|
||||
|
||||
export function getLocalStorageItems() {
|
||||
const items = {
|
||||
|
|
@ -31,6 +31,8 @@ export function clearLocalStorage(skipFirst?: boolean) {
|
|||
return;
|
||||
}
|
||||
if (
|
||||
key.startsWith(LocalStorageKeys.LAST_MCP_) ||
|
||||
key.startsWith(LocalStorageKeys.LAST_CODE_TOGGLE_) ||
|
||||
key.startsWith(LocalStorageKeys.ASST_ID_PREFIX) ||
|
||||
key.startsWith(LocalStorageKeys.AGENT_ID_PREFIX) ||
|
||||
key.startsWith(LocalStorageKeys.LAST_CONVO_SETUP) ||
|
||||
|
|
@ -43,3 +45,36 @@ export function clearLocalStorage(skipFirst?: boolean) {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function clearConversationStorage(conversationId?: string | null) {
|
||||
if (!conversationId) {
|
||||
return;
|
||||
}
|
||||
if (!isUUID.safeParse(conversationId)?.success) {
|
||||
console.warn(
|
||||
`Conversation ID ${conversationId} is not a valid UUID. Skipping local storage cleanup.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const keys = Object.keys(localStorage);
|
||||
keys.forEach((key) => {
|
||||
if (key.includes(conversationId)) {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
export function clearAllConversationStorage() {
|
||||
const keys = Object.keys(localStorage);
|
||||
keys.forEach((key) => {
|
||||
if (
|
||||
key.startsWith(LocalStorageKeys.LAST_MCP_) ||
|
||||
key.startsWith(LocalStorageKeys.LAST_CODE_TOGGLE_) ||
|
||||
key.startsWith(LocalStorageKeys.TEXT_DRAFT) ||
|
||||
key.startsWith(LocalStorageKeys.ASST_ID_PREFIX) ||
|
||||
key.startsWith(LocalStorageKeys.AGENT_ID_PREFIX) ||
|
||||
key.startsWith(LocalStorageKeys.LAST_CONVO_SETUP)
|
||||
) {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
2414
package-lock.json
generated
2414
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1246,6 +1246,8 @@ export enum Constants {
|
|||
GLOBAL_PROJECT_NAME = 'instance',
|
||||
/** Delimiter for MCP tools */
|
||||
mcp_delimiter = '_mcp_',
|
||||
/** Placeholder Agent ID for Ephemeral Agents */
|
||||
EPHEMERAL_AGENT_ID = 'ephemeral',
|
||||
}
|
||||
|
||||
export enum LocalStorageKeys {
|
||||
|
|
@ -1281,6 +1283,10 @@ export enum LocalStorageKeys {
|
|||
ENABLE_USER_MSG_MARKDOWN = 'enableUserMsgMarkdown',
|
||||
/** Key for displaying analysis tool code input */
|
||||
SHOW_ANALYSIS_CODE = 'showAnalysisCode',
|
||||
/** Last selected MCP values per conversation ID */
|
||||
LAST_MCP_ = 'LAST_MCP_',
|
||||
/** Last checked toggle for Code Interpreter API per conversation ID */
|
||||
LAST_CODE_TOGGLE_ = 'LAST_CODE_TOGGLE_',
|
||||
}
|
||||
|
||||
export enum ForkOptions {
|
||||
|
|
|
|||
|
|
@ -3,8 +3,15 @@ import { EndpointURLs } from './config';
|
|||
import * as s from './schemas';
|
||||
|
||||
export default function createPayload(submission: t.TSubmission) {
|
||||
const { conversation, userMessage, endpointOption, isEdited, isContinued, isTemporary } =
|
||||
submission;
|
||||
const {
|
||||
conversation,
|
||||
userMessage,
|
||||
endpointOption,
|
||||
isEdited,
|
||||
isContinued,
|
||||
isTemporary,
|
||||
ephemeralAgent,
|
||||
} = submission;
|
||||
const { conversationId } = s.tConvoUpdateSchema.parse(conversation);
|
||||
const { endpoint, endpointType } = endpointOption as {
|
||||
endpoint: s.EModelEndpoint;
|
||||
|
|
@ -12,16 +19,20 @@ export default function createPayload(submission: t.TSubmission) {
|
|||
};
|
||||
|
||||
let server = EndpointURLs[endpointType ?? endpoint];
|
||||
const isEphemeral = s.isEphemeralAgent(endpoint, ephemeralAgent);
|
||||
|
||||
if (isEdited && s.isAssistantsEndpoint(endpoint)) {
|
||||
server += '/modify';
|
||||
} else if (isEdited) {
|
||||
server = server.replace('/ask/', '/edit/');
|
||||
} else if (isEphemeral) {
|
||||
server = `${EndpointURLs[s.EModelEndpoint.agents]}/${endpoint}`;
|
||||
}
|
||||
|
||||
const payload: t.TPayload = {
|
||||
...userMessage,
|
||||
...endpointOption,
|
||||
ephemeralAgent: isEphemeral ? ephemeralAgent : undefined,
|
||||
isContinued: !!(isEdited && isContinued),
|
||||
conversationId,
|
||||
isTemporary,
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ export const excelMimeTypes =
|
|||
/^application\/(vnd\.ms-excel|msexcel|x-msexcel|x-ms-excel|x-excel|x-dos_ms_excel|xls|x-xls|vnd\.openxmlformats-officedocument\.spreadsheetml\.sheet)$/;
|
||||
|
||||
export const textMimeTypes =
|
||||
/^(text\/(x-c|x-csharp|x-c\+\+|x-java|html|markdown|x-php|x-python|x-script\.python|x-ruby|x-tex|plain|css|vtt|javascript|csv))$/;
|
||||
/^(text\/(x-c|x-csharp|tab-separated-values|x-c\+\+|x-java|html|markdown|x-php|x-python|x-script\.python|x-ruby|x-tex|plain|css|vtt|javascript|csv))$/;
|
||||
|
||||
export const applicationMimeTypes =
|
||||
/^(application\/(epub\+zip|csv|json|pdf|x-tar|typescript|vnd\.openxmlformats-officedocument\.(wordprocessingml\.document|presentationml\.presentation|spreadsheetml\.sheet)|xml|zip))$/;
|
||||
|
|
@ -152,6 +152,7 @@ export const codeTypeMapping: { [key: string]: string } = {
|
|||
yml: 'application/x-yaml',
|
||||
yaml: 'application/x-yaml',
|
||||
log: 'text/plain',
|
||||
tsv: 'text/tab-separated-values',
|
||||
};
|
||||
|
||||
export const retrievalMimeTypes = [
|
||||
|
|
@ -230,7 +231,7 @@ export const convertStringsToRegex = (patterns: string[]): RegExp[] =>
|
|||
const regex = new RegExp(pattern);
|
||||
acc.push(regex);
|
||||
} catch (error) {
|
||||
console.error(`Invalid regex pattern "${pattern}" skipped.`);
|
||||
console.error(`Invalid regex pattern "${pattern}" skipped.`, error);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
|
|
|
|||
|
|
@ -13,8 +13,6 @@ import {
|
|||
// agentsSchema,
|
||||
compactAgentsSchema,
|
||||
compactGoogleSchema,
|
||||
compactChatGPTSchema,
|
||||
chatGPTBrowserSchema,
|
||||
compactPluginsSchema,
|
||||
compactAssistantSchema,
|
||||
} from './schemas';
|
||||
|
|
@ -26,19 +24,19 @@ type EndpointSchema =
|
|||
| typeof openAISchema
|
||||
| typeof googleSchema
|
||||
| typeof anthropicSchema
|
||||
| typeof chatGPTBrowserSchema
|
||||
| typeof gptPluginsSchema
|
||||
| typeof assistantSchema
|
||||
| typeof compactAgentsSchema
|
||||
| typeof bedrockInputSchema;
|
||||
|
||||
const endpointSchemas: Record<EModelEndpoint, EndpointSchema> = {
|
||||
type EndpointSchemaKey = Exclude<EModelEndpoint, EModelEndpoint.chatGPTBrowser>;
|
||||
|
||||
const endpointSchemas: Record<EndpointSchemaKey, EndpointSchema> = {
|
||||
[EModelEndpoint.openAI]: openAISchema,
|
||||
[EModelEndpoint.azureOpenAI]: openAISchema,
|
||||
[EModelEndpoint.custom]: openAISchema,
|
||||
[EModelEndpoint.google]: googleSchema,
|
||||
[EModelEndpoint.anthropic]: anthropicSchema,
|
||||
[EModelEndpoint.chatGPTBrowser]: chatGPTBrowserSchema,
|
||||
[EModelEndpoint.gptPlugins]: gptPluginsSchema,
|
||||
[EModelEndpoint.assistants]: assistantSchema,
|
||||
[EModelEndpoint.azureAssistants]: assistantSchema,
|
||||
|
|
@ -167,8 +165,8 @@ export const parseConvo = ({
|
|||
conversation,
|
||||
possibleValues,
|
||||
}: {
|
||||
endpoint: EModelEndpoint;
|
||||
endpointType?: EModelEndpoint | null;
|
||||
endpoint: EndpointSchemaKey;
|
||||
endpointType?: EndpointSchemaKey | null;
|
||||
conversation: Partial<s.TConversation | s.TPreset> | null;
|
||||
possibleValues?: TPossibleValues;
|
||||
// TODO: POC for default schema
|
||||
|
|
@ -252,7 +250,7 @@ export const getResponseSender = (endpointOption: t.TEndpointOption): string =>
|
|||
return modelLabel;
|
||||
} else if (model && extractOmniVersion(model)) {
|
||||
return extractOmniVersion(model);
|
||||
} else if (model && model.includes('mistral')) {
|
||||
} else if (model && (model.includes('mistral') || model.includes('codestral'))) {
|
||||
return 'Mistral';
|
||||
} else if (model && model.includes('gpt-')) {
|
||||
const gptVersion = extractGPTVersion(model);
|
||||
|
|
@ -288,7 +286,7 @@ export const getResponseSender = (endpointOption: t.TEndpointOption): string =>
|
|||
return chatGptLabel;
|
||||
} else if (model && extractOmniVersion(model)) {
|
||||
return extractOmniVersion(model);
|
||||
} else if (model && model.includes('mistral')) {
|
||||
} else if (model && (model.includes('mistral') || model.includes('codestral'))) {
|
||||
return 'Mistral';
|
||||
} else if (model && model.includes('gpt-')) {
|
||||
const gptVersion = extractGPTVersion(model);
|
||||
|
|
@ -309,11 +307,10 @@ type CompactEndpointSchema =
|
|||
| typeof compactAgentsSchema
|
||||
| typeof compactGoogleSchema
|
||||
| typeof anthropicSchema
|
||||
| typeof compactChatGPTSchema
|
||||
| typeof bedrockInputSchema
|
||||
| typeof compactPluginsSchema;
|
||||
|
||||
const compactEndpointSchemas: Record<string, CompactEndpointSchema> = {
|
||||
const compactEndpointSchemas: Record<EndpointSchemaKey, CompactEndpointSchema> = {
|
||||
[EModelEndpoint.openAI]: openAISchema,
|
||||
[EModelEndpoint.azureOpenAI]: openAISchema,
|
||||
[EModelEndpoint.custom]: openAISchema,
|
||||
|
|
@ -323,7 +320,6 @@ const compactEndpointSchemas: Record<string, CompactEndpointSchema> = {
|
|||
[EModelEndpoint.google]: compactGoogleSchema,
|
||||
[EModelEndpoint.bedrock]: bedrockInputSchema,
|
||||
[EModelEndpoint.anthropic]: anthropicSchema,
|
||||
[EModelEndpoint.chatGPTBrowser]: compactChatGPTSchema,
|
||||
[EModelEndpoint.gptPlugins]: compactPluginsSchema,
|
||||
};
|
||||
|
||||
|
|
@ -333,8 +329,8 @@ export const parseCompactConvo = ({
|
|||
conversation,
|
||||
possibleValues,
|
||||
}: {
|
||||
endpoint?: EModelEndpoint;
|
||||
endpointType?: EModelEndpoint | null;
|
||||
endpoint?: EndpointSchemaKey;
|
||||
endpointType?: EndpointSchemaKey | null;
|
||||
conversation: Partial<s.TConversation | s.TPreset>;
|
||||
possibleValues?: TPossibleValues;
|
||||
// TODO: POC for default schema
|
||||
|
|
@ -371,7 +367,10 @@ export const parseCompactConvo = ({
|
|||
return convo;
|
||||
};
|
||||
|
||||
export function parseTextParts(contentParts: a.TMessageContentParts[]): string {
|
||||
export function parseTextParts(
|
||||
contentParts: a.TMessageContentParts[],
|
||||
skipReasoning: boolean = false,
|
||||
): string {
|
||||
let result = '';
|
||||
|
||||
for (const part of contentParts) {
|
||||
|
|
@ -390,7 +389,7 @@ export function parseTextParts(contentParts: a.TMessageContentParts[]): string {
|
|||
result += ' ';
|
||||
}
|
||||
result += textValue;
|
||||
} else if (part.type === ContentTypes.THINK) {
|
||||
} else if (part.type === ContentTypes.THINK && !skipReasoning) {
|
||||
const textValue = typeof part.think === 'string' ? part.think : '';
|
||||
if (
|
||||
result.length > 0 &&
|
||||
|
|
|
|||
|
|
@ -74,12 +74,28 @@ export const roleDefaults = defaultRolesSchema.parse({
|
|||
[SystemRoles.ADMIN]: {
|
||||
name: SystemRoles.ADMIN,
|
||||
permissions: {
|
||||
[PermissionTypes.PROMPTS]: {},
|
||||
[PermissionTypes.BOOKMARKS]: {},
|
||||
[PermissionTypes.AGENTS]: {},
|
||||
[PermissionTypes.MULTI_CONVO]: {},
|
||||
[PermissionTypes.TEMPORARY_CHAT]: {},
|
||||
[PermissionTypes.RUN_CODE]: {},
|
||||
[PermissionTypes.PROMPTS]: {
|
||||
[Permissions.SHARED_GLOBAL]: true,
|
||||
[Permissions.USE]: true,
|
||||
[Permissions.CREATE]: true,
|
||||
},
|
||||
[PermissionTypes.BOOKMARKS]: {
|
||||
[Permissions.USE]: true,
|
||||
},
|
||||
[PermissionTypes.AGENTS]: {
|
||||
[Permissions.SHARED_GLOBAL]: true,
|
||||
[Permissions.USE]: true,
|
||||
[Permissions.CREATE]: true,
|
||||
},
|
||||
[PermissionTypes.MULTI_CONVO]: {
|
||||
[Permissions.USE]: true,
|
||||
},
|
||||
[PermissionTypes.TEMPORARY_CHAT]: {
|
||||
[Permissions.USE]: true,
|
||||
},
|
||||
[PermissionTypes.RUN_CODE]: {
|
||||
[Permissions.USE]: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
[SystemRoles.USER]: {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { z } from 'zod';
|
||||
import { Tools } from './types/assistants';
|
||||
import type { TMessageContentParts, FunctionTool, FunctionToolCall } from './types/assistants';
|
||||
import type { TEphemeralAgent } from './types';
|
||||
import type { TFile } from './types/files';
|
||||
|
||||
export const isUUID = z.string().uuid();
|
||||
|
|
@ -88,6 +89,21 @@ export const isAgentsEndpoint = (_endpoint?: EModelEndpoint.agents | null | stri
|
|||
return endpoint === EModelEndpoint.agents;
|
||||
};
|
||||
|
||||
export const isEphemeralAgent = (
|
||||
endpoint?: EModelEndpoint.agents | null | string,
|
||||
ephemeralAgent?: TEphemeralAgent | null,
|
||||
) => {
|
||||
if (!ephemeralAgent) {
|
||||
return false;
|
||||
}
|
||||
if (isAgentsEndpoint(endpoint)) {
|
||||
return false;
|
||||
}
|
||||
const hasMCPSelected = (ephemeralAgent?.mcp?.length ?? 0) > 0;
|
||||
const hasCodeSelected = (ephemeralAgent?.execute_code ?? false) === true;
|
||||
return hasMCPSelected || hasCodeSelected;
|
||||
};
|
||||
|
||||
export const isParamEndpoint = (
|
||||
endpoint: EModelEndpoint | string,
|
||||
endpointType?: EModelEndpoint | string,
|
||||
|
|
@ -752,22 +768,23 @@ export const tConversationTagSchema = z.object({
|
|||
});
|
||||
export type TConversationTag = z.infer<typeof tConversationTagSchema>;
|
||||
|
||||
export const googleSchema = tConversationSchema
|
||||
.pick({
|
||||
model: true,
|
||||
modelLabel: true,
|
||||
promptPrefix: true,
|
||||
examples: true,
|
||||
temperature: true,
|
||||
maxOutputTokens: true,
|
||||
artifacts: true,
|
||||
topP: true,
|
||||
topK: true,
|
||||
iconURL: true,
|
||||
greeting: true,
|
||||
spec: true,
|
||||
maxContextTokens: true,
|
||||
})
|
||||
export const googleBaseSchema = tConversationSchema.pick({
|
||||
model: true,
|
||||
modelLabel: true,
|
||||
promptPrefix: true,
|
||||
examples: true,
|
||||
temperature: true,
|
||||
maxOutputTokens: true,
|
||||
artifacts: true,
|
||||
topP: true,
|
||||
topK: true,
|
||||
iconURL: true,
|
||||
greeting: true,
|
||||
spec: true,
|
||||
maxContextTokens: true,
|
||||
});
|
||||
|
||||
export const googleSchema = googleBaseSchema
|
||||
.transform((obj: Partial<TConversation>) => removeNullishValues(obj))
|
||||
.catch(() => ({}));
|
||||
|
||||
|
|
@ -790,36 +807,25 @@ export const googleGenConfigSchema = z
|
|||
.strip()
|
||||
.optional();
|
||||
|
||||
export const chatGPTBrowserSchema = tConversationSchema
|
||||
.pick({
|
||||
model: true,
|
||||
})
|
||||
.transform((obj) => ({
|
||||
...obj,
|
||||
model: obj.model ?? 'text-davinci-002-render-sha',
|
||||
}))
|
||||
.catch(() => ({
|
||||
model: 'text-davinci-002-render-sha',
|
||||
}));
|
||||
const gptPluginsBaseSchema = tConversationSchema.pick({
|
||||
model: true,
|
||||
modelLabel: true,
|
||||
chatGptLabel: true,
|
||||
promptPrefix: true,
|
||||
temperature: true,
|
||||
artifacts: true,
|
||||
top_p: true,
|
||||
presence_penalty: true,
|
||||
frequency_penalty: true,
|
||||
tools: true,
|
||||
agentOptions: true,
|
||||
iconURL: true,
|
||||
greeting: true,
|
||||
spec: true,
|
||||
maxContextTokens: true,
|
||||
});
|
||||
|
||||
export const gptPluginsSchema = tConversationSchema
|
||||
.pick({
|
||||
model: true,
|
||||
modelLabel: true,
|
||||
chatGptLabel: true,
|
||||
promptPrefix: true,
|
||||
temperature: true,
|
||||
artifacts: true,
|
||||
top_p: true,
|
||||
presence_penalty: true,
|
||||
frequency_penalty: true,
|
||||
tools: true,
|
||||
agentOptions: true,
|
||||
iconURL: true,
|
||||
greeting: true,
|
||||
spec: true,
|
||||
maxContextTokens: true,
|
||||
})
|
||||
export const gptPluginsSchema = gptPluginsBaseSchema
|
||||
.transform((obj) => {
|
||||
const result = {
|
||||
...obj,
|
||||
|
|
@ -889,18 +895,19 @@ export function removeNullishValues<T extends Record<string, unknown>>(
|
|||
return newObj;
|
||||
}
|
||||
|
||||
export const assistantSchema = tConversationSchema
|
||||
.pick({
|
||||
model: true,
|
||||
assistant_id: true,
|
||||
instructions: true,
|
||||
artifacts: true,
|
||||
promptPrefix: true,
|
||||
iconURL: true,
|
||||
greeting: true,
|
||||
spec: true,
|
||||
append_current_datetime: true,
|
||||
})
|
||||
const assistantBaseSchema = tConversationSchema.pick({
|
||||
model: true,
|
||||
assistant_id: true,
|
||||
instructions: true,
|
||||
artifacts: true,
|
||||
promptPrefix: true,
|
||||
iconURL: true,
|
||||
greeting: true,
|
||||
spec: true,
|
||||
append_current_datetime: true,
|
||||
});
|
||||
|
||||
export const assistantSchema = assistantBaseSchema
|
||||
.transform((obj) => ({
|
||||
...obj,
|
||||
model: obj.model ?? openAISettings.model.default,
|
||||
|
|
@ -923,37 +930,39 @@ export const assistantSchema = tConversationSchema
|
|||
append_current_datetime: false,
|
||||
}));
|
||||
|
||||
export const compactAssistantSchema = tConversationSchema
|
||||
.pick({
|
||||
model: true,
|
||||
assistant_id: true,
|
||||
instructions: true,
|
||||
promptPrefix: true,
|
||||
artifacts: true,
|
||||
iconURL: true,
|
||||
greeting: true,
|
||||
spec: true,
|
||||
})
|
||||
const compactAssistantBaseSchema = tConversationSchema.pick({
|
||||
model: true,
|
||||
assistant_id: true,
|
||||
instructions: true,
|
||||
promptPrefix: true,
|
||||
artifacts: true,
|
||||
iconURL: true,
|
||||
greeting: true,
|
||||
spec: true,
|
||||
});
|
||||
|
||||
export const compactAssistantSchema = compactAssistantBaseSchema
|
||||
.transform((obj) => removeNullishValues(obj))
|
||||
.catch(() => ({}));
|
||||
|
||||
export const agentsSchema = tConversationSchema
|
||||
.pick({
|
||||
model: true,
|
||||
modelLabel: true,
|
||||
temperature: true,
|
||||
top_p: true,
|
||||
presence_penalty: true,
|
||||
frequency_penalty: true,
|
||||
resendFiles: true,
|
||||
imageDetail: true,
|
||||
agent_id: true,
|
||||
instructions: true,
|
||||
promptPrefix: true,
|
||||
iconURL: true,
|
||||
greeting: true,
|
||||
maxContextTokens: true,
|
||||
})
|
||||
export const agentsBaseSchema = tConversationSchema.pick({
|
||||
model: true,
|
||||
modelLabel: true,
|
||||
temperature: true,
|
||||
top_p: true,
|
||||
presence_penalty: true,
|
||||
frequency_penalty: true,
|
||||
resendFiles: true,
|
||||
imageDetail: true,
|
||||
agent_id: true,
|
||||
instructions: true,
|
||||
promptPrefix: true,
|
||||
iconURL: true,
|
||||
greeting: true,
|
||||
maxContextTokens: true,
|
||||
});
|
||||
|
||||
export const agentsSchema = agentsBaseSchema
|
||||
.transform((obj) => ({
|
||||
...obj,
|
||||
model: obj.model ?? agentsSettings.model.default,
|
||||
|
|
@ -989,46 +998,32 @@ export const agentsSchema = tConversationSchema
|
|||
maxContextTokens: undefined,
|
||||
}));
|
||||
|
||||
export const openAISchema = tConversationSchema
|
||||
.pick({
|
||||
model: true,
|
||||
modelLabel: true,
|
||||
chatGptLabel: true,
|
||||
promptPrefix: true,
|
||||
temperature: true,
|
||||
top_p: true,
|
||||
presence_penalty: true,
|
||||
frequency_penalty: true,
|
||||
resendFiles: true,
|
||||
artifacts: true,
|
||||
imageDetail: true,
|
||||
stop: true,
|
||||
iconURL: true,
|
||||
greeting: true,
|
||||
spec: true,
|
||||
maxContextTokens: true,
|
||||
max_tokens: true,
|
||||
reasoning_effort: true,
|
||||
})
|
||||
export const openAIBaseSchema = tConversationSchema.pick({
|
||||
model: true,
|
||||
modelLabel: true,
|
||||
chatGptLabel: true,
|
||||
promptPrefix: true,
|
||||
temperature: true,
|
||||
top_p: true,
|
||||
presence_penalty: true,
|
||||
frequency_penalty: true,
|
||||
resendFiles: true,
|
||||
artifacts: true,
|
||||
imageDetail: true,
|
||||
stop: true,
|
||||
iconURL: true,
|
||||
greeting: true,
|
||||
spec: true,
|
||||
maxContextTokens: true,
|
||||
max_tokens: true,
|
||||
reasoning_effort: true,
|
||||
});
|
||||
|
||||
export const openAISchema = openAIBaseSchema
|
||||
.transform((obj: Partial<TConversation>) => removeNullishValues(obj))
|
||||
.catch(() => ({}));
|
||||
|
||||
export const compactGoogleSchema = tConversationSchema
|
||||
.pick({
|
||||
model: true,
|
||||
modelLabel: true,
|
||||
promptPrefix: true,
|
||||
examples: true,
|
||||
temperature: true,
|
||||
maxOutputTokens: true,
|
||||
artifacts: true,
|
||||
topP: true,
|
||||
topK: true,
|
||||
iconURL: true,
|
||||
greeting: true,
|
||||
spec: true,
|
||||
maxContextTokens: true,
|
||||
})
|
||||
export const compactGoogleSchema = googleBaseSchema
|
||||
.transform((obj) => {
|
||||
const newObj: Partial<TConversation> = { ...obj };
|
||||
if (newObj.temperature === google.temperature.default) {
|
||||
|
|
@ -1048,55 +1043,30 @@ export const compactGoogleSchema = tConversationSchema
|
|||
})
|
||||
.catch(() => ({}));
|
||||
|
||||
export const anthropicSchema = tConversationSchema
|
||||
.pick({
|
||||
model: true,
|
||||
modelLabel: true,
|
||||
promptPrefix: true,
|
||||
temperature: true,
|
||||
maxOutputTokens: true,
|
||||
topP: true,
|
||||
topK: true,
|
||||
resendFiles: true,
|
||||
promptCache: true,
|
||||
thinking: true,
|
||||
thinkingBudget: true,
|
||||
artifacts: true,
|
||||
iconURL: true,
|
||||
greeting: true,
|
||||
spec: true,
|
||||
maxContextTokens: true,
|
||||
})
|
||||
export const anthropicBaseSchema = tConversationSchema.pick({
|
||||
model: true,
|
||||
modelLabel: true,
|
||||
promptPrefix: true,
|
||||
temperature: true,
|
||||
maxOutputTokens: true,
|
||||
topP: true,
|
||||
topK: true,
|
||||
resendFiles: true,
|
||||
promptCache: true,
|
||||
thinking: true,
|
||||
thinkingBudget: true,
|
||||
artifacts: true,
|
||||
iconURL: true,
|
||||
greeting: true,
|
||||
spec: true,
|
||||
maxContextTokens: true,
|
||||
});
|
||||
|
||||
export const anthropicSchema = anthropicBaseSchema
|
||||
.transform((obj) => removeNullishValues(obj))
|
||||
.catch(() => ({}));
|
||||
|
||||
export const compactChatGPTSchema = tConversationSchema
|
||||
.pick({
|
||||
model: true,
|
||||
})
|
||||
.transform((obj) => {
|
||||
const newObj: Partial<TConversation> = { ...obj };
|
||||
return removeNullishValues(newObj);
|
||||
})
|
||||
.catch(() => ({}));
|
||||
|
||||
export const compactPluginsSchema = tConversationSchema
|
||||
.pick({
|
||||
model: true,
|
||||
modelLabel: true,
|
||||
chatGptLabel: true,
|
||||
promptPrefix: true,
|
||||
temperature: true,
|
||||
top_p: true,
|
||||
presence_penalty: true,
|
||||
frequency_penalty: true,
|
||||
tools: true,
|
||||
agentOptions: true,
|
||||
iconURL: true,
|
||||
greeting: true,
|
||||
spec: true,
|
||||
maxContextTokens: true,
|
||||
})
|
||||
export const compactPluginsSchema = gptPluginsBaseSchema
|
||||
.transform((obj) => {
|
||||
const newObj: Partial<TConversation> = { ...obj };
|
||||
if (newObj.modelLabel === null) {
|
||||
|
|
@ -1149,15 +1119,16 @@ export const tBannerSchema = z.object({
|
|||
});
|
||||
export type TBanner = z.infer<typeof tBannerSchema>;
|
||||
|
||||
export const compactAgentsSchema = tConversationSchema
|
||||
.pick({
|
||||
spec: true,
|
||||
// model: true,
|
||||
iconURL: true,
|
||||
greeting: true,
|
||||
agent_id: true,
|
||||
instructions: true,
|
||||
additional_instructions: true,
|
||||
})
|
||||
export const compactAgentsBaseSchema = tConversationSchema.pick({
|
||||
spec: true,
|
||||
// model: true,
|
||||
iconURL: true,
|
||||
greeting: true,
|
||||
agent_id: true,
|
||||
instructions: true,
|
||||
additional_instructions: true,
|
||||
});
|
||||
|
||||
export const compactAgentsSchema = compactAgentsBaseSchema
|
||||
.transform((obj) => removeNullishValues(obj))
|
||||
.catch(() => ({}));
|
||||
|
|
|
|||
|
|
@ -41,12 +41,18 @@ export type TEndpointOption = {
|
|||
overrideUserMessageId?: string;
|
||||
};
|
||||
|
||||
export type TEphemeralAgent = {
|
||||
mcp?: string[];
|
||||
execute_code?: boolean;
|
||||
};
|
||||
|
||||
export type TPayload = Partial<TMessage> &
|
||||
Partial<TEndpointOption> & {
|
||||
isContinued: boolean;
|
||||
conversationId: string | null;
|
||||
messages?: TMessages;
|
||||
isTemporary: boolean;
|
||||
ephemeralAgent?: TEphemeralAgent | null;
|
||||
};
|
||||
|
||||
export type TSubmission = {
|
||||
|
|
@ -63,6 +69,7 @@ export type TSubmission = {
|
|||
conversation: Partial<TConversation>;
|
||||
endpointOption: TEndpointOption;
|
||||
clientTimestamp?: string;
|
||||
ephemeralAgent?: TEphemeralAgent | null;
|
||||
};
|
||||
|
||||
export type EventSubmission = Omit<TSubmission, 'initialResponse'> & { initialResponse: TMessage };
|
||||
|
|
|
|||
|
|
@ -1,21 +1,21 @@
|
|||
{
|
||||
"name": "librechat-mcp",
|
||||
"version": "1.1.0",
|
||||
"type": "module",
|
||||
"version": "1.2.0",
|
||||
"type": "commonjs",
|
||||
"description": "MCP services for LibreChat",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.es.js",
|
||||
"types": "./dist/types/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.es.js",
|
||||
"require": "./dist/index.js",
|
||||
"types": "./dist/types/index.d.ts"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rimraf dist",
|
||||
"build": "npm run clean && rollup -c --configPlugin=@rollup/plugin-typescript",
|
||||
"build:watch": "rollup -c -w --configPlugin=@rollup/plugin-typescript",
|
||||
"build": "npm run clean && rollup -c --bundleConfigAsCjs",
|
||||
"build:watch": "rollup -c -w --bundleConfigAsCjs",
|
||||
"test": "jest --coverage --watch",
|
||||
"test:ci": "jest --coverage --ci",
|
||||
"verify": "npm run test:ci",
|
||||
|
|
@ -60,7 +60,6 @@
|
|||
"rollup": "^4.22.4",
|
||||
"rollup-plugin-generate-package-json": "^3.2.0",
|
||||
"rollup-plugin-peer-deps-external": "^2.2.4",
|
||||
"rollup-plugin-typescript2": "^0.35.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.0.4"
|
||||
},
|
||||
|
|
@ -68,7 +67,7 @@
|
|||
"registry": "https://registry.npmjs.org/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.8.0",
|
||||
"@modelcontextprotocol/sdk": "^1.9.0",
|
||||
"diff": "^7.0.0",
|
||||
"eventsource": "^3.0.2",
|
||||
"express": "^4.21.2"
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
// rollup.config.js
|
||||
import typescript from 'rollup-plugin-typescript2';
|
||||
import resolve from '@rollup/plugin-node-resolve';
|
||||
import peerDepsExternal from 'rollup-plugin-peer-deps-external';
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import replace from '@rollup/plugin-replace';
|
||||
import terser from '@rollup/plugin-terser';
|
||||
import { readFileSync } from 'fs';
|
||||
import terser from '@rollup/plugin-terser';
|
||||
import replace from '@rollup/plugin-replace';
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import resolve from '@rollup/plugin-node-resolve';
|
||||
import typescript from '@rollup/plugin-typescript';
|
||||
import peerDepsExternal from 'rollup-plugin-peer-deps-external';
|
||||
|
||||
const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8'));
|
||||
|
||||
|
|
@ -24,16 +24,18 @@ const plugins = [
|
|||
}),
|
||||
typescript({
|
||||
tsconfig: './tsconfig.json',
|
||||
useTsconfigDeclarationDir: true,
|
||||
outDir: './dist',
|
||||
sourceMap: true,
|
||||
inlineSourceMap: true,
|
||||
}),
|
||||
terser(),
|
||||
];
|
||||
|
||||
const esmBuild = {
|
||||
const cjsBuild = {
|
||||
input: 'src/index.ts',
|
||||
output: {
|
||||
file: pkg.module,
|
||||
format: 'esm',
|
||||
file: pkg.main,
|
||||
format: 'cjs',
|
||||
sourcemap: true,
|
||||
exports: 'named',
|
||||
},
|
||||
|
|
@ -42,4 +44,4 @@ const esmBuild = {
|
|||
plugins,
|
||||
};
|
||||
|
||||
export default esmBuild;
|
||||
export default cjsBuild;
|
||||
|
|
|
|||
|
|
@ -55,13 +55,18 @@ export class FlowStateManager<T = unknown> {
|
|||
/**
|
||||
* Creates a new flow and waits for its completion
|
||||
*/
|
||||
async createFlow(flowId: string, type: string, metadata: FlowMetadata = {}): Promise<T> {
|
||||
async createFlow(
|
||||
flowId: string,
|
||||
type: string,
|
||||
metadata: FlowMetadata = {},
|
||||
signal?: AbortSignal,
|
||||
): Promise<T> {
|
||||
const flowKey = this.getFlowKey(flowId, type);
|
||||
|
||||
let existingState = (await this.keyv.get(flowKey)) as FlowState<T> | undefined;
|
||||
if (existingState) {
|
||||
this.logger.debug(`[${flowKey}] Flow already exists`);
|
||||
return this.monitorFlow(flowKey, type);
|
||||
return this.monitorFlow(flowKey, type, signal);
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||
|
|
@ -69,7 +74,7 @@ export class FlowStateManager<T = unknown> {
|
|||
existingState = (await this.keyv.get(flowKey)) as FlowState<T> | undefined;
|
||||
if (existingState) {
|
||||
this.logger.debug(`[${flowKey}] Flow exists on 2nd check`);
|
||||
return this.monitorFlow(flowKey, type);
|
||||
return this.monitorFlow(flowKey, type, signal);
|
||||
}
|
||||
|
||||
const initialState: FlowState = {
|
||||
|
|
@ -81,10 +86,10 @@ export class FlowStateManager<T = unknown> {
|
|||
|
||||
this.logger.debug('Creating initial flow state:', flowKey);
|
||||
await this.keyv.set(flowKey, initialState, this.ttl);
|
||||
return this.monitorFlow(flowKey, type);
|
||||
return this.monitorFlow(flowKey, type, signal);
|
||||
}
|
||||
|
||||
private monitorFlow(flowKey: string, type: string): Promise<T> {
|
||||
private monitorFlow(flowKey: string, type: string, signal?: AbortSignal): Promise<T> {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const checkInterval = 2000;
|
||||
let elapsedTime = 0;
|
||||
|
|
@ -101,6 +106,16 @@ export class FlowStateManager<T = unknown> {
|
|||
return;
|
||||
}
|
||||
|
||||
if (signal?.aborted) {
|
||||
clearInterval(intervalId);
|
||||
this.intervals.delete(intervalId);
|
||||
this.logger.warn(`[${flowKey}] Flow aborted`);
|
||||
const message = `${type} flow aborted`;
|
||||
await this.keyv.delete(flowKey);
|
||||
reject(new Error(message));
|
||||
return;
|
||||
}
|
||||
|
||||
if (flowState.status !== 'PENDING') {
|
||||
clearInterval(intervalId);
|
||||
this.intervals.delete(intervalId);
|
||||
|
|
@ -197,19 +212,19 @@ export class FlowStateManager<T = unknown> {
|
|||
* @param flowId - The ID of the flow
|
||||
* @param type - The type of flow
|
||||
* @param handler - Async function to execute if no existing flow is found
|
||||
* @param metadata - Optional metadata for the flow
|
||||
* @param signal - Optional AbortSignal to cancel the flow
|
||||
*/
|
||||
async createFlowWithHandler(
|
||||
flowId: string,
|
||||
type: string,
|
||||
handler: () => Promise<T>,
|
||||
metadata: FlowMetadata = {},
|
||||
signal?: AbortSignal,
|
||||
): Promise<T> {
|
||||
const flowKey = this.getFlowKey(flowId, type);
|
||||
let existingState = (await this.keyv.get(flowKey)) as FlowState<T> | undefined;
|
||||
if (existingState) {
|
||||
this.logger.debug(`[${flowKey}] Flow already exists`);
|
||||
return this.monitorFlow(flowKey, type);
|
||||
return this.monitorFlow(flowKey, type, signal);
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||
|
|
@ -217,13 +232,13 @@ export class FlowStateManager<T = unknown> {
|
|||
existingState = (await this.keyv.get(flowKey)) as FlowState<T> | undefined;
|
||||
if (existingState) {
|
||||
this.logger.debug(`[${flowKey}] Flow exists on 2nd check`);
|
||||
return this.monitorFlow(flowKey, type);
|
||||
return this.monitorFlow(flowKey, type, signal);
|
||||
}
|
||||
|
||||
const initialState: FlowState = {
|
||||
type,
|
||||
status: 'PENDING',
|
||||
metadata,
|
||||
metadata: {},
|
||||
createdAt: Date.now(),
|
||||
};
|
||||
this.logger.debug(`[${flowKey}] Creating initial flow state`);
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
} from 'librechat-data-provider';
|
||||
import type { JsonSchemaType, TPlugin } from 'librechat-data-provider';
|
||||
import { ToolSchema, ListToolsResultSchema } from '@modelcontextprotocol/sdk/types.js';
|
||||
import type * as t from '@modelcontextprotocol/sdk/types.js';
|
||||
|
||||
export type StdioOptions = z.infer<typeof StdioOptionsSchema>;
|
||||
export type WebSocketOptions = z.infer<typeof WebSocketOptionsSchema>;
|
||||
|
|
@ -44,25 +45,7 @@ export type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'err
|
|||
|
||||
export type MCPTool = z.infer<typeof ToolSchema>;
|
||||
export type MCPToolListResponse = z.infer<typeof ListToolsResultSchema>;
|
||||
export type ToolContentPart =
|
||||
| {
|
||||
type: 'text';
|
||||
text: string;
|
||||
}
|
||||
| {
|
||||
type: 'image';
|
||||
data: string;
|
||||
mimeType: string;
|
||||
}
|
||||
| {
|
||||
type: 'resource';
|
||||
resource: {
|
||||
uri: string;
|
||||
mimeType?: string;
|
||||
text?: string;
|
||||
blob?: string;
|
||||
};
|
||||
};
|
||||
export type ToolContentPart = t.TextContent | t.ImageContent | t.EmbeddedResource | t.AudioContent;
|
||||
export type ImageContent = Extract<ToolContentPart, { type: 'image' }>;
|
||||
export type MCPToolCallResponse =
|
||||
| undefined
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue