File size: 10,305 Bytes
f0743f4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 | import { logger } from '@librechat/data-schemas';
import { EModelEndpoint, EToolResources, AgentCapabilities } from 'librechat-data-provider';
import type { AgentToolResources, TFile, AgentBaseResource } from 'librechat-data-provider';
import type { IMongoFile, AppConfig, IUser } from '@librechat/data-schemas';
import type { FilterQuery, QueryOptions, ProjectionType } from 'mongoose';
import type { Request as ServerRequest } from 'express';
/**
* Function type for retrieving files from the database
* @param filter - MongoDB filter query for files
* @param _sortOptions - Sorting options (currently unused)
* @param selectFields - Field selection options
* @param options - Additional options including userId and agentId for access control
* @returns Promise resolving to array of files
*/
export type TGetFiles = (
filter: FilterQuery<IMongoFile>,
_sortOptions: ProjectionType<IMongoFile> | null | undefined,
selectFields: QueryOptions<IMongoFile> | null | undefined,
options?: { userId?: string; agentId?: string },
) => Promise<Array<TFile>>;
/**
* Helper function to add a file to a specific tool resource category
* Prevents duplicate files within the same resource category
* @param params - Parameters object
* @param params.file - The file to add to the resource
* @param params.resourceType - The type of tool resource (e.g., execute_code, file_search, image_edit)
* @param params.tool_resources - The agent's tool resources object to update
* @param params.processedResourceFiles - Set tracking processed files per resource type
*/
const addFileToResource = ({
file,
resourceType,
tool_resources,
processedResourceFiles,
}: {
file: TFile;
resourceType: EToolResources;
tool_resources: AgentToolResources;
processedResourceFiles: Set<string>;
}): void => {
if (!file.file_id) {
return;
}
const resourceKey = `${resourceType}:${file.file_id}`;
if (processedResourceFiles.has(resourceKey)) {
return;
}
const resource = tool_resources[resourceType as keyof AgentToolResources] ?? {};
if (!resource.files) {
(tool_resources[resourceType as keyof AgentToolResources] as AgentBaseResource) = {
...resource,
files: [],
};
}
// Check if already exists in the files array
const resourceFiles = tool_resources[resourceType as keyof AgentToolResources]?.files;
const alreadyExists = resourceFiles?.some((f: TFile) => f.file_id === file.file_id);
if (!alreadyExists) {
resourceFiles?.push(file);
processedResourceFiles.add(resourceKey);
}
};
/**
* Categorizes a file into the appropriate tool resource based on its properties
* Files are categorized as:
* - execute_code: Files with fileIdentifier metadata
* - file_search: Files marked as embedded
* - image_edit: Image files in the request file set with dimensions
* @param params - Parameters object
* @param params.file - The file to categorize
* @param params.tool_resources - The agent's tool resources to update
* @param params.requestFileSet - Set of file IDs from the current request
* @param params.processedResourceFiles - Set tracking processed files per resource type
*/
const categorizeFileForToolResources = ({
file,
tool_resources,
requestFileSet,
processedResourceFiles,
}: {
file: TFile;
tool_resources: AgentToolResources;
requestFileSet: Set<string>;
processedResourceFiles: Set<string>;
}): void => {
if (file.metadata?.fileIdentifier) {
addFileToResource({
file,
resourceType: EToolResources.execute_code,
tool_resources,
processedResourceFiles,
});
return;
}
if (file.embedded === true) {
addFileToResource({
file,
resourceType: EToolResources.file_search,
tool_resources,
processedResourceFiles,
});
return;
}
if (
requestFileSet.has(file.file_id) &&
file.type.startsWith('image') &&
file.height &&
file.width
) {
addFileToResource({
file,
resourceType: EToolResources.image_edit,
tool_resources,
processedResourceFiles,
});
}
};
/**
* Primes resources for agent execution by processing attachments and tool resources
* This function:
* 1. Fetches OCR files if OCR is enabled
* 2. Processes attachment files
* 3. Categorizes files into appropriate tool resources
* 4. Prevents duplicate files across all sources
*
* @param params - Parameters object
* @param params.req - Express request object
* @param params.appConfig - Application configuration object
* @param params.getFiles - Function to retrieve files from database
* @param params.requestFileSet - Set of file IDs from the current request
* @param params.attachments - Promise resolving to array of attachment files
* @param params.tool_resources - Existing tool resources for the agent
* @returns Promise resolving to processed attachments and updated tool resources
*/
export const primeResources = async ({
req,
appConfig,
getFiles,
requestFileSet,
attachments: _attachments,
tool_resources: _tool_resources,
agentId,
}: {
req: ServerRequest & { user?: IUser };
appConfig: AppConfig;
requestFileSet: Set<string>;
attachments: Promise<Array<TFile | null>> | undefined;
tool_resources: AgentToolResources | undefined;
getFiles: TGetFiles;
agentId?: string;
}): Promise<{
attachments: Array<TFile | undefined> | undefined;
tool_resources: AgentToolResources | undefined;
}> => {
try {
/**
* Array to collect all unique files that will be returned as attachments
* Files are added from OCR results and attachment promises, with duplicates prevented
*/
const attachments: Array<TFile> = [];
/**
* Set of file IDs already added to the attachments array
* Used to prevent duplicate files from being added multiple times
* Pre-populated with files from non-OCR tool_resources to prevent re-adding them
*/
const attachmentFileIds = new Set<string>();
/**
* Set tracking which files have been added to specific tool resource categories
* Format: "resourceType:fileId" (e.g., "execute_code:file123")
* Prevents the same file from being added multiple times to the same resource
*/
const processedResourceFiles = new Set<string>();
/**
* The agent's tool resources object that will be updated with categorized files
* Create a shallow copy first to avoid mutating the original
*/
const tool_resources: AgentToolResources = { ...(_tool_resources ?? {}) };
// Deep copy each resource to avoid mutating nested objects/arrays
for (const [resourceType, resource] of Object.entries(tool_resources)) {
if (!resource) {
continue;
}
// Deep copy the resource to avoid mutations
tool_resources[resourceType as keyof AgentToolResources] = {
...resource,
// Deep copy arrays to prevent mutations
...(resource.files && { files: [...resource.files] }),
...(resource.file_ids && { file_ids: [...resource.file_ids] }),
...(resource.vector_store_ids && { vector_store_ids: [...resource.vector_store_ids] }),
} as AgentBaseResource;
// Now track existing files
if (resource.files && Array.isArray(resource.files)) {
for (const file of resource.files) {
if (file?.file_id) {
processedResourceFiles.add(`${resourceType}:${file.file_id}`);
// Files from non-context resources should not be added to attachments from _attachments
if (resourceType !== EToolResources.context && resourceType !== EToolResources.ocr) {
attachmentFileIds.add(file.file_id);
}
}
}
}
}
const isContextEnabled = (
appConfig?.endpoints?.[EModelEndpoint.agents]?.capabilities ?? []
).includes(AgentCapabilities.context);
const fileIds = tool_resources[EToolResources.context]?.file_ids ?? [];
const ocrFileIds = tool_resources[EToolResources.ocr]?.file_ids;
if (ocrFileIds != null) {
fileIds.push(...ocrFileIds);
delete tool_resources[EToolResources.ocr];
}
if (fileIds.length > 0 && isContextEnabled) {
delete tool_resources[EToolResources.context];
const context = await getFiles(
{
file_id: { $in: fileIds },
},
{},
{},
{ userId: req.user?.id, agentId },
);
for (const file of context) {
if (!file?.file_id) {
continue;
}
// Clear from attachmentFileIds if it was pre-added
attachmentFileIds.delete(file.file_id);
// Add to attachments
attachments.push(file);
attachmentFileIds.add(file.file_id);
// Categorize for tool resources
categorizeFileForToolResources({
file,
tool_resources,
requestFileSet,
processedResourceFiles,
});
}
}
if (!_attachments) {
return { attachments: attachments.length > 0 ? attachments : undefined, tool_resources };
}
const files = await _attachments;
for (const file of files) {
if (!file) {
continue;
}
categorizeFileForToolResources({
file,
tool_resources,
requestFileSet,
processedResourceFiles,
});
if (file.file_id && attachmentFileIds.has(file.file_id)) {
continue;
}
attachments.push(file);
if (file.file_id) {
attachmentFileIds.add(file.file_id);
}
}
return { attachments: attachments.length > 0 ? attachments : [], tool_resources };
} catch (error) {
logger.error('Error priming resources', error);
// Safely try to get attachments without rethrowing
let safeAttachments: Array<TFile | undefined> = [];
if (_attachments) {
try {
const attachmentFiles = await _attachments;
safeAttachments = (attachmentFiles?.filter((file) => !!file) ?? []) as Array<TFile>;
} catch (attachmentError) {
// If attachments promise is also rejected, just use empty array
logger.error('Error resolving attachments in catch block', attachmentError);
safeAttachments = [];
}
}
return {
attachments: safeAttachments,
tool_resources: _tool_resources,
};
}
};
|