mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-18 09:20:15 +01:00
* feat: Code Interpreter API & File Search Agent Uploads chore: add back code files wip: first pass, abstract key dialog refactor: influence checkbox on key changes refactor: update localization keys for 'execute code' to 'run code' wip: run code button refactor: add throwError parameter to loadAuthValues and getUserPluginAuthValue functions feat: first pass, API tool calling fix: handle missing toolId in callTool function and return 404 for non-existent tools feat: show code outputs fix: improve error handling in callTool function and log errors fix: handle potential null value for filepath in attachment destructuring fix: normalize language before rendering and prevent null return fix: add loading indicator in RunCode component while executing code feat: add support for conditional code execution in Markdown components feat: attachments refactor: remove bash fix: pass abort signal to graph/run refactor: debounce and rate limit tool call refactor: increase debounce delay for execute function feat: set code output attachments feat: image attachments refactor: apply message context refactor: pass `partIndex` feat: toolCall schema/model/methods feat: block indexing feat: get tool calls chore: imports chore: typing chore: condense type imports feat: get tool calls fix: block indexing chore: typing refactor: update tool calls mapping to support multiple results fix: add unique key to nav link for rendering wip: first pass, tool call results refactor: update query cache from successful tool call mutation style: improve result switcher styling chore: note on using \`.toObject()\` feat: add agent_id field to conversation schema chore: typing refactor: rename agentMap to agentsMap for consistency feat: Agent Name as chat input placeholder chore: bump agents 📦 chore: update @langchain dependencies to latest versions to match agents package 📦 chore: update @librechat/agents dependency to version 1.8.0 fix: Aborting agent stream removes sender; fix(bedrock): completion removes preset name label refactor: remove direct file parameter to use req.file, add `processAgentFileUpload` for image uploads feat: upload menu feat: prime message_file resources feat: implement conversation access validation in chat route refactor: remove file parameter from processFileUpload and use req.file instead feat: add savedMessageIds set to track saved message IDs in BaseClient, to prevent unnecessary double-write to db feat: prevent duplicate message saves by checking savedMessageIds in AgentController refactor: skip legacy RAG API handling for agents feat: add files field to convoSchema refactor: update request type annotations from Express.Request to ServerRequest in file processing functions feat: track conversation files fix: resendFiles, addPreviousAttachments handling feat: add ID validation for session_id and file_id in download route feat: entity_id for code file uploads/downloads fix: code file edge cases feat: delete related tool calls feat: add stream rate handling for LLM configuration feat: enhance system content with attached file information fix: improve error logging in resource priming function * WIP: PoC, sequential agents WIP: PoC Sequential Agents, first pass content data + bump agents package fix: package-lock WIP: PoC, o1 support, refactor bufferString feat: convertJsonSchemaToZod fix: form issues and schema defining erroneous model fix: max length issue on agent form instructions, limit conversation messages to sequential agents feat: add abort signal support to createRun function and AgentClient feat: PoC, hide prior sequential agent steps fix: update parameter naming from config to metadata in event handlers for clarity, add model to usage data refactor: use only last contentData, track model for usage data chore: bump agents package fix: content parts issue refactor: filter contentParts to include tool calls and relevant indices feat: show function calls refactor: filter context messages to exclude tool calls when no tools are available to the agent fix: ensure tool call content is not undefined in formatMessages feat: add agent_id field to conversationPreset schema feat: hide sequential agents feat: increase upload toast duration to 10 seconds * refactor: tool context handling & update Code API Key Dialog feat: toolContextMap chore: skipSpecs -> useSpecs ci: fix handleTools tests feat: API Key Dialog * feat: Agent Permissions Admin Controls feat: replace label with button for prompt permission toggle feat: update agent permissions feat: enable experimental agents and streamline capability configuration feat: implement access control for agents and enhance endpoint menu items feat: add welcome message for agent selection in localization feat: add agents permission to access control and update version to 0.7.57 * fix: update types in useAssistantListMap and useMentions hooks for better null handling * feat: mention agents * fix: agent tool resource race conditions when deleting agent tool resource files * feat: add error handling for code execution with user feedback * refactor: rename AdminControls to AdminSettings for clarity * style: add gap to button in AdminSettings for improved layout * refactor: separate agent query hooks and check access to enable fetching * fix: remove unused provider from agent initialization options, creates issue with custom endpoints * refactor: remove redundant/deprecated modelOptions from AgentClient processes * chore: update @librechat/agents to version 1.8.5 in package.json and package-lock.json * fix: minor styling issues + agent panel uniformity * fix: agent edge cases when set endpoint is no longer defined * refactor: remove unused cleanup function call from AppService * fix: update link in ApiKeyDialog to point to pricing page * fix: improve type handling and layout calculations in SidePanel component * fix: add missing localization string for agent selection in SidePanel * chore: form styling and localizations for upload filesearch/code interpreter * fix: model selection placeholder logic in AgentConfig component * style: agent capabilities * fix: add localization for provider selection and improve dropdown styling in ModelPanel * refactor: use gpt-4o-mini > gpt-3.5-turbo * fix: agents configuration for loadDefaultInterface and update related tests * feat: DALLE Agents support
490 lines
15 KiB
JavaScript
490 lines
15 KiB
JavaScript
const fs = require('fs');
|
|
const path = require('path');
|
|
const { zodToJsonSchema } = require('zod-to-json-schema');
|
|
const { tool: toolFn, Tool } = require('@langchain/core/tools');
|
|
const { Calculator } = require('@langchain/community/tools/calculator');
|
|
const {
|
|
Tools,
|
|
ContentTypes,
|
|
imageGenTools,
|
|
actionDelimiter,
|
|
ImageVisionTool,
|
|
openapiToFunction,
|
|
validateAndParseOpenAPISpec,
|
|
} = require('librechat-data-provider');
|
|
const { processFileURL, uploadImageBuffer } = require('~/server/services/Files/process');
|
|
const { loadActionSets, createActionTool, domainParser } = require('./ActionService');
|
|
const { recordUsage } = require('~/server/services/Threads');
|
|
const { loadTools } = require('~/app/clients/tools/util');
|
|
const { redactMessage } = require('~/config/parsers');
|
|
const { sleep } = require('~/server/utils');
|
|
const { logger } = require('~/config');
|
|
|
|
/**
|
|
* Loads and formats tools from the specified tool directory.
|
|
*
|
|
* The directory is scanned for JavaScript files, excluding any files in the filter set.
|
|
* For each file, it attempts to load the file as a module and instantiate a class, if it's a subclass of `StructuredTool`.
|
|
* Each tool instance is then formatted to be compatible with the OpenAI Assistant.
|
|
* Additionally, instances of LangChain Tools are included in the result.
|
|
*
|
|
* @param {object} params - The parameters for the function.
|
|
* @param {string} params.directory - The directory path where the tools are located.
|
|
* @param {Array<string>} [params.adminFilter=[]] - Array of admin-defined tool keys to exclude from loading.
|
|
* @param {Array<string>} [params.adminIncluded=[]] - Array of admin-defined tool keys to include from loading.
|
|
* @returns {Record<string, FunctionTool>} An object mapping each tool's plugin key to its instance.
|
|
*/
|
|
function loadAndFormatTools({ directory, adminFilter = [], adminIncluded = [] }) {
|
|
const filter = new Set([...adminFilter]);
|
|
const included = new Set(adminIncluded);
|
|
const tools = [];
|
|
/* Structured Tools Directory */
|
|
const files = fs.readdirSync(directory);
|
|
|
|
if (included.size > 0 && adminFilter.length > 0) {
|
|
logger.warn(
|
|
'Both `includedTools` and `filteredTools` are defined; `filteredTools` will be ignored.',
|
|
);
|
|
}
|
|
|
|
for (const file of files) {
|
|
const filePath = path.join(directory, file);
|
|
if (!file.endsWith('.js') || (filter.has(file) && included.size === 0)) {
|
|
continue;
|
|
}
|
|
|
|
let ToolClass = null;
|
|
try {
|
|
ToolClass = require(filePath);
|
|
} catch (error) {
|
|
logger.error(`[loadAndFormatTools] Error loading tool from ${filePath}:`, error);
|
|
continue;
|
|
}
|
|
|
|
if (!ToolClass || !(ToolClass.prototype instanceof Tool)) {
|
|
continue;
|
|
}
|
|
|
|
let toolInstance = null;
|
|
try {
|
|
toolInstance = new ToolClass({ override: true });
|
|
} catch (error) {
|
|
logger.error(
|
|
`[loadAndFormatTools] Error initializing \`${file}\` tool; if it requires authentication, is the \`override\` field configured?`,
|
|
error,
|
|
);
|
|
continue;
|
|
}
|
|
|
|
if (!toolInstance) {
|
|
continue;
|
|
}
|
|
|
|
if (filter.has(toolInstance.name) && included.size === 0) {
|
|
continue;
|
|
}
|
|
|
|
if (included.size > 0 && !included.has(file) && !included.has(toolInstance.name)) {
|
|
continue;
|
|
}
|
|
|
|
const formattedTool = formatToOpenAIAssistantTool(toolInstance);
|
|
tools.push(formattedTool);
|
|
}
|
|
|
|
/** Basic Tools; schema: { input: string } */
|
|
const basicToolInstances = [new Calculator()];
|
|
for (const toolInstance of basicToolInstances) {
|
|
const formattedTool = formatToOpenAIAssistantTool(toolInstance);
|
|
tools.push(formattedTool);
|
|
}
|
|
|
|
tools.push(ImageVisionTool);
|
|
|
|
return tools.reduce((map, tool) => {
|
|
map[tool.function.name] = tool;
|
|
return map;
|
|
}, {});
|
|
}
|
|
|
|
/**
|
|
* Formats a `StructuredTool` instance into a format that is compatible
|
|
* with OpenAI's ChatCompletionFunctions. It uses the `zodToJsonSchema`
|
|
* function to convert the schema of the `StructuredTool` into a JSON
|
|
* schema, which is then used as the parameters for the OpenAI function.
|
|
*
|
|
* @param {StructuredTool} tool - The StructuredTool to format.
|
|
* @returns {FunctionTool} The OpenAI Assistant Tool.
|
|
*/
|
|
function formatToOpenAIAssistantTool(tool) {
|
|
return {
|
|
type: Tools.function,
|
|
[Tools.function]: {
|
|
name: tool.name,
|
|
description: tool.description,
|
|
parameters: zodToJsonSchema(tool.schema),
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Processes the required actions by calling the appropriate tools and returning the outputs.
|
|
* @param {OpenAIClient} client - OpenAI or StreamRunManager Client.
|
|
* @param {RequiredAction} requiredActions - The current required action.
|
|
* @returns {Promise<ToolOutput>} The outputs of the tools.
|
|
*/
|
|
const processVisionRequest = async (client, currentAction) => {
|
|
if (!client.visionPromise) {
|
|
return {
|
|
tool_call_id: currentAction.toolCallId,
|
|
output: 'No image details found.',
|
|
};
|
|
}
|
|
|
|
/** @type {ChatCompletion | undefined} */
|
|
const completion = await client.visionPromise;
|
|
if (completion && completion.usage) {
|
|
recordUsage({
|
|
user: client.req.user.id,
|
|
model: client.req.body.model,
|
|
conversationId: (client.responseMessage ?? client.finalMessage).conversationId,
|
|
...completion.usage,
|
|
});
|
|
}
|
|
const output = completion?.choices?.[0]?.message?.content ?? 'No image details found.';
|
|
return {
|
|
tool_call_id: currentAction.toolCallId,
|
|
output,
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Processes return required actions from run.
|
|
* @param {OpenAIClient | StreamRunManager} client - OpenAI (legacy) or StreamRunManager Client.
|
|
* @param {RequiredAction[]} requiredActions - The required actions to submit outputs for.
|
|
* @returns {Promise<ToolOutputs>} The outputs of the tools.
|
|
*/
|
|
async function processRequiredActions(client, requiredActions) {
|
|
logger.debug(
|
|
`[required actions] user: ${client.req.user.id} | thread_id: ${requiredActions[0].thread_id} | run_id: ${requiredActions[0].run_id}`,
|
|
requiredActions,
|
|
);
|
|
const tools = requiredActions.map((action) => action.tool);
|
|
const { loadedTools } = await loadTools({
|
|
user: client.req.user.id,
|
|
model: client.req.body.model ?? 'gpt-4o-mini',
|
|
tools,
|
|
functions: true,
|
|
options: {
|
|
processFileURL,
|
|
req: client.req,
|
|
uploadImageBuffer,
|
|
openAIApiKey: client.apiKey,
|
|
fileStrategy: client.req.app.locals.fileStrategy,
|
|
returnMetadata: true,
|
|
},
|
|
});
|
|
|
|
const ToolMap = loadedTools.reduce((map, tool) => {
|
|
map[tool.name] = tool;
|
|
return map;
|
|
}, {});
|
|
|
|
const promises = [];
|
|
|
|
/** @type {Action[]} */
|
|
let actionSets = [];
|
|
let isActionTool = false;
|
|
const ActionToolMap = {};
|
|
const ActionBuildersMap = {};
|
|
|
|
for (let i = 0; i < requiredActions.length; i++) {
|
|
const currentAction = requiredActions[i];
|
|
if (currentAction.tool === ImageVisionTool.function.name) {
|
|
promises.push(processVisionRequest(client, currentAction));
|
|
continue;
|
|
}
|
|
let tool = ToolMap[currentAction.tool] ?? ActionToolMap[currentAction.tool];
|
|
|
|
const handleToolOutput = async (output) => {
|
|
requiredActions[i].output = output;
|
|
|
|
/** @type {FunctionToolCall & PartMetadata} */
|
|
const toolCall = {
|
|
function: {
|
|
name: currentAction.tool,
|
|
arguments: JSON.stringify(currentAction.toolInput),
|
|
output,
|
|
},
|
|
id: currentAction.toolCallId,
|
|
type: 'function',
|
|
progress: 1,
|
|
action: isActionTool,
|
|
};
|
|
|
|
const toolCallIndex = client.mappedOrder.get(toolCall.id);
|
|
|
|
if (imageGenTools.has(currentAction.tool)) {
|
|
const imageOutput = output;
|
|
toolCall.function.output = `${currentAction.tool} displayed an image. All generated images are already plainly visible, so don't repeat the descriptions in detail. Do not list download links as they are available in the UI already. The user may download the images by clicking on them, but do not mention anything about downloading to the user.`;
|
|
|
|
// Streams the "Finished" state of the tool call in the UI
|
|
client.addContentData({
|
|
[ContentTypes.TOOL_CALL]: toolCall,
|
|
index: toolCallIndex,
|
|
type: ContentTypes.TOOL_CALL,
|
|
});
|
|
|
|
await sleep(500);
|
|
|
|
/** @type {ImageFile} */
|
|
const imageDetails = {
|
|
...imageOutput,
|
|
...currentAction.toolInput,
|
|
};
|
|
|
|
const image_file = {
|
|
[ContentTypes.IMAGE_FILE]: imageDetails,
|
|
type: ContentTypes.IMAGE_FILE,
|
|
// Replace the tool call output with Image file
|
|
index: toolCallIndex,
|
|
};
|
|
|
|
client.addContentData(image_file);
|
|
|
|
// Update the stored tool call
|
|
client.seenToolCalls && client.seenToolCalls.set(toolCall.id, toolCall);
|
|
|
|
return {
|
|
tool_call_id: currentAction.toolCallId,
|
|
output: toolCall.function.output,
|
|
};
|
|
}
|
|
|
|
client.seenToolCalls && client.seenToolCalls.set(toolCall.id, toolCall);
|
|
client.addContentData({
|
|
[ContentTypes.TOOL_CALL]: toolCall,
|
|
index: toolCallIndex,
|
|
type: ContentTypes.TOOL_CALL,
|
|
// TODO: to append tool properties to stream, pass metadata rest to addContentData
|
|
// result: tool.result,
|
|
});
|
|
|
|
return {
|
|
tool_call_id: currentAction.toolCallId,
|
|
output,
|
|
};
|
|
};
|
|
|
|
if (!tool) {
|
|
// throw new Error(`Tool ${currentAction.tool} not found.`);
|
|
|
|
if (!actionSets.length) {
|
|
actionSets =
|
|
(await loadActionSets({
|
|
assistant_id: client.req.body.assistant_id,
|
|
})) ?? [];
|
|
}
|
|
|
|
let actionSet = null;
|
|
let currentDomain = '';
|
|
for (let action of actionSets) {
|
|
const domain = await domainParser(client.req, action.metadata.domain, true);
|
|
if (currentAction.tool.includes(domain)) {
|
|
currentDomain = domain;
|
|
actionSet = action;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!actionSet) {
|
|
// TODO: try `function` if no action set is found
|
|
// throw new Error(`Tool ${currentAction.tool} not found.`);
|
|
continue;
|
|
}
|
|
|
|
let builders = ActionBuildersMap[actionSet.metadata.domain];
|
|
|
|
if (!builders) {
|
|
const validationResult = validateAndParseOpenAPISpec(actionSet.metadata.raw_spec);
|
|
if (!validationResult.spec) {
|
|
throw new Error(
|
|
`Invalid spec: user: ${client.req.user.id} | thread_id: ${requiredActions[0].thread_id} | run_id: ${requiredActions[0].run_id}`,
|
|
);
|
|
}
|
|
const { requestBuilders } = openapiToFunction(validationResult.spec);
|
|
ActionToolMap[actionSet.metadata.domain] = requestBuilders;
|
|
builders = requestBuilders;
|
|
}
|
|
|
|
const functionName = currentAction.tool.replace(`${actionDelimiter}${currentDomain}`, '');
|
|
|
|
const requestBuilder = builders[functionName];
|
|
|
|
if (!requestBuilder) {
|
|
// throw new Error(`Tool ${currentAction.tool} not found.`);
|
|
continue;
|
|
}
|
|
|
|
tool = await createActionTool({ action: actionSet, requestBuilder });
|
|
isActionTool = !!tool;
|
|
ActionToolMap[currentAction.tool] = tool;
|
|
}
|
|
|
|
if (currentAction.tool === 'calculator') {
|
|
currentAction.toolInput = currentAction.toolInput.input;
|
|
}
|
|
|
|
const handleToolError = (error) => {
|
|
logger.error(
|
|
`tool_call_id: ${currentAction.toolCallId} | Error processing tool ${currentAction.tool}`,
|
|
error,
|
|
);
|
|
return {
|
|
tool_call_id: currentAction.toolCallId,
|
|
output: `Error processing tool ${currentAction.tool}: ${redactMessage(error.message, 256)}`,
|
|
};
|
|
};
|
|
|
|
try {
|
|
const promise = tool
|
|
._call(currentAction.toolInput)
|
|
.then(handleToolOutput)
|
|
.catch(handleToolError);
|
|
promises.push(promise);
|
|
} catch (error) {
|
|
const toolOutputError = handleToolError(error);
|
|
promises.push(Promise.resolve(toolOutputError));
|
|
}
|
|
}
|
|
|
|
return {
|
|
tool_outputs: await Promise.all(promises),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Processes the runtime tool calls and returns the tool classes.
|
|
* @param {Object} params - Run params containing user and request information.
|
|
* @param {ServerRequest} params.req - The request object.
|
|
* @param {string} params.agent_id - The agent ID.
|
|
* @param {Agent['tools']} params.tools - The agent's available tools.
|
|
* @param {Agent['tool_resources']} params.tool_resources - The agent's available tool resources.
|
|
* @param {string | undefined} [params.openAIApiKey] - The OpenAI API key.
|
|
* @returns {Promise<{ tools?: StructuredTool[] }>} The agent tools.
|
|
*/
|
|
async function loadAgentTools({ req, agent_id, tools, tool_resources, openAIApiKey }) {
|
|
if (!tools || tools.length === 0) {
|
|
return {};
|
|
}
|
|
const { loadedTools, toolContextMap } = await loadTools({
|
|
user: req.user.id,
|
|
// model: req.body.model ?? 'gpt-4o-mini',
|
|
tools,
|
|
functions: true,
|
|
isAgent: agent_id != null,
|
|
options: {
|
|
req,
|
|
openAIApiKey,
|
|
tool_resources,
|
|
processFileURL,
|
|
uploadImageBuffer,
|
|
returnMetadata: true,
|
|
fileStrategy: req.app.locals.fileStrategy,
|
|
},
|
|
});
|
|
|
|
const agentTools = [];
|
|
for (let i = 0; i < loadedTools.length; i++) {
|
|
const tool = loadedTools[i];
|
|
if (tool.name && (tool.name === Tools.execute_code || tool.name === Tools.file_search)) {
|
|
agentTools.push(tool);
|
|
continue;
|
|
}
|
|
|
|
const toolDefinition = {
|
|
name: tool.name,
|
|
schema: tool.schema,
|
|
description: tool.description,
|
|
};
|
|
|
|
if (imageGenTools.has(tool.name)) {
|
|
toolDefinition.responseFormat = 'content_and_artifact';
|
|
}
|
|
|
|
const toolInstance = toolFn(async (...args) => {
|
|
return tool['_call'](...args);
|
|
}, toolDefinition);
|
|
|
|
agentTools.push(toolInstance);
|
|
}
|
|
|
|
const ToolMap = loadedTools.reduce((map, tool) => {
|
|
map[tool.name] = tool;
|
|
return map;
|
|
}, {});
|
|
|
|
let actionSets = [];
|
|
const ActionToolMap = {};
|
|
|
|
for (const toolName of tools) {
|
|
if (!ToolMap[toolName]) {
|
|
if (!actionSets.length) {
|
|
actionSets = (await loadActionSets({ agent_id })) ?? [];
|
|
}
|
|
|
|
let actionSet = null;
|
|
let currentDomain = '';
|
|
for (let action of actionSets) {
|
|
const domain = await domainParser(req, action.metadata.domain, true);
|
|
if (toolName.includes(domain)) {
|
|
currentDomain = domain;
|
|
actionSet = action;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (actionSet) {
|
|
const validationResult = validateAndParseOpenAPISpec(actionSet.metadata.raw_spec);
|
|
if (validationResult.spec) {
|
|
const { requestBuilders, functionSignatures, zodSchemas } = openapiToFunction(
|
|
validationResult.spec,
|
|
true,
|
|
);
|
|
const functionName = toolName.replace(`${actionDelimiter}${currentDomain}`, '');
|
|
const functionSig = functionSignatures.find((sig) => sig.name === functionName);
|
|
const requestBuilder = requestBuilders[functionName];
|
|
const zodSchema = zodSchemas[functionName];
|
|
|
|
if (requestBuilder) {
|
|
const tool = await createActionTool({
|
|
action: actionSet,
|
|
requestBuilder,
|
|
zodSchema,
|
|
name: toolName,
|
|
description: functionSig.description,
|
|
});
|
|
agentTools.push(tool);
|
|
ActionToolMap[toolName] = tool;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (tools.length > 0 && agentTools.length === 0) {
|
|
throw new Error('No tools found for the specified tool calls.');
|
|
}
|
|
|
|
return {
|
|
tools: agentTools,
|
|
toolContextMap,
|
|
};
|
|
}
|
|
|
|
module.exports = {
|
|
loadAgentTools,
|
|
loadAndFormatTools,
|
|
processRequiredActions,
|
|
formatToOpenAIAssistantTool,
|
|
};
|