@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
JavaScript
// 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);
}
}
};