UNPKG

@aj-archipelago/cortex

Version:

Cortex is a GraphQL API for AI. It provides a simple, extensible interface for using AI services from OpenAI, Azure and others.

241 lines (214 loc) 10.1 kB
// sys_tool_writefile.js // Entity tool that writes content to a file and uploads it to cloud storage import logger from '../../../../lib/logger.js'; import { uploadFileToCloud, addFileToCollection, getMimeTypeFromFilename, isTextMimeType } from '../../../../lib/fileUtils.js'; // Maximum file size for writing (50MB) - prevents memory issues const MAX_WRITABLE_FILE_SIZE = 50 * 1024 * 1024; // Helper function to format file size function formatFileSize(bytes) { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]; } export default { prompt: [], timeout: 60, toolDefinition: { type: "function", icon: "✍️", function: { name: "WriteFile", description: "Write content to a file and upload it to cloud storage. The file will be added to your file collection for future reference. Use this to save text, code, data, or any content you generate to a file.", parameters: { type: "object", properties: { content: { type: "string", description: "The content to write to the file" }, filename: { type: "string", description: "The filename for the file (e.g., 'output.txt', 'data.json', 'script.py'). Include the file extension." }, tags: { type: "array", items: { type: "string" }, description: "Optional: Array of tags to categorize the file (e.g., ['code', 'output', 'data'])" }, notes: { type: "string", description: "Optional: Notes or description about the file" }, userMessage: { type: "string", description: "A user-friendly message that describes what you're doing with this tool" } }, required: ["content", "filename", "userMessage"] } } }, executePathway: async ({args, runAllPrompts, resolver}) => { const { content, filename, tags = [], notes = '', contextId, contextKey, chatId } = args; // Validate inputs and return JSON error if invalid if (content === undefined || content === null) { const errorResult = { success: false, filename: filename || 'unknown', error: "content is required and cannot be null or undefined" }; resolver.tool = JSON.stringify({ toolUsed: "WriteFile" }); return JSON.stringify(errorResult); } if (typeof content !== 'string') { const errorResult = { success: false, filename: filename || 'unknown', error: `content must be a string, got ${typeof content}` }; resolver.tool = JSON.stringify({ toolUsed: "WriteFile" }); return JSON.stringify(errorResult); } if (!filename || typeof filename !== 'string') { const errorResult = { success: false, filename: 'unknown', error: "filename is required and must be a non-empty string" }; resolver.tool = JSON.stringify({ toolUsed: "WriteFile" }); return JSON.stringify(errorResult); } // Validate filename doesn't contain invalid characters if (filename.trim().length === 0) { const errorResult = { success: false, filename: filename, error: "filename cannot be empty or whitespace only" }; resolver.tool = JSON.stringify({ toolUsed: "WriteFile" }); return JSON.stringify(errorResult); } // Validate tags if provided if (tags !== undefined && !Array.isArray(tags)) { const errorResult = { success: false, filename: filename, error: "tags must be an array if provided" }; resolver.tool = JSON.stringify({ toolUsed: "WriteFile" }); return JSON.stringify(errorResult); } // Validate notes if provided if (notes !== undefined && typeof notes !== 'string') { const errorResult = { success: false, filename: filename, error: "notes must be a string if provided" }; resolver.tool = JSON.stringify({ toolUsed: "WriteFile" }); return JSON.stringify(errorResult); } try { // Convert content to buffer const fileBuffer = Buffer.from(content, 'utf8'); // Check file size to prevent memory issues if (fileBuffer.length > MAX_WRITABLE_FILE_SIZE) { const errorResult = { success: false, filename: filename, error: `Content too large to write (${formatFileSize(fileBuffer.length)}). Maximum file size is ${formatFileSize(MAX_WRITABLE_FILE_SIZE)}.` }; resolver.tool = JSON.stringify({ toolUsed: "WriteFile" }); return JSON.stringify(errorResult); } logger.info(`Prepared content buffer for file: ${filename} (${fileBuffer.length} bytes)`); // Determine MIME type from filename using utility function let mimeType = getMimeTypeFromFilename(filename, 'text/plain'); // Add charset=utf-8 for text-based MIME types to ensure proper encoding if (isTextMimeType(mimeType)) { mimeType = `${mimeType}; charset=utf-8`; } // Upload file to cloud storage (this will compute hash and check for duplicates) const uploadResult = await uploadFileToCloud( fileBuffer, mimeType, filename, resolver, contextId ); if (!uploadResult || !uploadResult.url) { throw new Error('Failed to upload file to cloud storage'); } // Add to file collection if contextId is provided let fileEntry = null; if (contextId) { try { fileEntry = await addFileToCollection( contextId, contextKey || null, uploadResult.url, uploadResult.gcs || null, filename, tags, notes, uploadResult.hash || null, null, // fileUrl - not needed since we already uploaded resolver, true, // permanent => retention=permanent chatId || null ); } catch (collectionError) { // Log but don't fail - file collection is optional logger.warn(`Failed to add file to collection: ${collectionError.message}`); } } const fileSize = Buffer.byteLength(content, 'utf8'); // Create explicit success message that clearly indicates completion and file collection status // This format matches image generation tools to prevent agent loops let message; if (fileEntry) { // File was added to collection - provide explicit completion message with reference info const fileRef = fileEntry.hash || fileEntry.id || filename; message = `File creation completed successfully. The file "${filename}" (${formatFileSize(fileSize)}) has been written, uploaded to cloud storage, and added to your file collection. The file is now available in your file collection and can be referenced by its hash (${fileEntry.hash || 'N/A'}), filename (${filename}), fileId (${fileEntry.id || 'N/A'}), or URL in future tool calls.`; } else { // File was uploaded but not added to collection (no contextId provided) message = `File "${filename}" written and uploaded successfully (${formatFileSize(fileSize)}). File URL: ${uploadResult.url}`; } const result = { success: true, filename: filename, url: uploadResult.url, gcs: uploadResult.gcs || null, hash: uploadResult.hash || null, fileId: fileEntry?.id || null, size: fileSize, sizeFormatted: formatFileSize(fileSize), message: message }; resolver.tool = JSON.stringify({ toolUsed: "WriteFile" }); return JSON.stringify(result); } catch (error) { let errorMsg; if (error?.message) { errorMsg = error.message; } else if (error?.errors && Array.isArray(error.errors)) { // Handle AggregateError errorMsg = error.errors.map(e => e?.message || String(e)).join('; '); } else { errorMsg = String(error); } logger.error(`Error writing file ${filename}: ${errorMsg}`); const errorResult = { success: false, filename: filename, error: errorMsg }; resolver.tool = JSON.stringify({ toolUsed: "WriteFile" }); return JSON.stringify(errorResult); } } };