Files
anything-llm/server/utils/agents/aibitat/plugins/create-files/xlsx/create-excel-file.js
Timothy Carambat 7aaea7f514 File creation agent skills (#5280)
* Powerpoint File Creation (#5278)

* wip

* download card

* UI for downloading

* move to fs system with endpoint to pull files

* refactor UI

* final-pass

* remove save-file-browser skill and refactor

* remove fileDownload event

* reset

* reset file

* reset timeout

* persist toggle

* Txt creation (#5279)

* wip

* download card

* UI for downloading

* move to fs system with endpoint to pull files

* refactor UI

* final-pass

* remove save-file-browser skill and refactor

* remove fileDownload event

* reset

* reset file

* reset timeout

* wip

* persist toggle

* add arbitrary text creation file

* Add PDF document generation with markdown formatting (#5283)

add support for branding in bottom right corner
refactor core utils and frontend rendering

* Xlsx document creation (#5284)

add Excel doc & sheet creation

* Basic docx creation (#5285)

* Basic docx creation

* add test theme support + styling and title pages

* simplify skill selection

* handle TG attachments

* send documents over tg

* lazy import

* pin deps

* fix lock

* i18n for file creation (#5286)

i18n for file-creation
connect #5280

* theme overhaul

* Add PPTX subagent for better results

* forgot files

* Add PPTX subagent for better results (#5287)

* Add PPTX subagent for better results

* forgot files

* make sub-agent use proper tool calling if it can and better UI hints
2026-03-30 15:13:39 -07:00

363 lines
13 KiB
JavaScript

const createFilesLib = require("../lib.js");
const {
parseCSV,
validateCSVData,
detectDelimiter,
inferCellType,
applyBranding,
autoFitColumns,
applyHeaderStyle,
applyZebraStriping,
freezePanes,
} = require("./utils.js");
module.exports.CreateExcelFile = {
name: "create-excel-file",
plugin: function () {
return {
name: "create-excel-file",
setup(aibitat) {
aibitat.function({
super: aibitat,
name: this.name,
description:
"Create an Excel spreadsheet (.xlsx) from CSV data. " +
"Supports multiple sheets, automatic type detection (numbers, dates, booleans), " +
"header styling, column auto-fit, zebra striping, and frozen panes. " +
"Provide data in CSV format with comma, semicolon, tab, or pipe delimiters.",
examples: [
{
prompt: "Create an Excel file with sales data",
call: JSON.stringify({
filename: "sales-report.xlsx",
sheets: [
{
name: "Q1 Sales",
csvData:
"Product,Region,Sales,Date\nWidget A,North,1250.50,2024-01-15\nWidget B,South,980.00,2024-01-20\nWidget C,East,1100.25,2024-02-01",
options: {
headerStyle: true,
autoFit: true,
freezeHeader: true,
},
},
],
}),
},
{
prompt: "Create a multi-sheet Excel workbook with employee data",
call: JSON.stringify({
filename: "employee-directory.xlsx",
sheets: [
{
name: "Employees",
csvData:
"ID,Name,Department,Salary,Start Date\n1,John Smith,Engineering,85000,2022-03-15\n2,Jane Doe,Marketing,72000,2021-08-01\n3,Bob Wilson,Sales,68000,2023-01-10",
options: {
headerStyle: true,
zebraStripes: true,
freezeHeader: true,
},
},
{
name: "Departments",
csvData:
"Department,Head,Budget\nEngineering,Alice Brown,500000\nMarketing,Carol White,250000\nSales,Dan Green,300000",
options: { headerStyle: true, autoFit: true },
},
],
}),
},
{
prompt: "Create a simple spreadsheet from CSV data",
call: JSON.stringify({
filename: "data-export.xlsx",
csvData:
"Name,Email,Status\nAlice,alice@example.com,Active\nBob,bob@example.com,Pending\nCharlie,charlie@example.com,Active",
}),
},
],
parameters: {
$schema: "http://json-schema.org/draft-07/schema#",
type: "object",
properties: {
filename: {
type: "string",
description:
"The filename for the Excel file. The .xlsx extension will be added automatically if not provided.",
},
csvData: {
type: "string",
description:
"CSV data for a single-sheet workbook. Use comma, semicolon, tab, or pipe as delimiter. " +
"For multiple sheets, use the 'sheets' parameter instead.",
},
sheets: {
type: "array",
description:
"Array of sheet definitions for multi-sheet workbooks. Each sheet has a name, csvData, and optional styling options.",
items: {
type: "object",
properties: {
name: {
type: "string",
description:
"The name of the worksheet (max 31 characters).",
},
csvData: {
type: "string",
description: "The CSV data for this sheet.",
},
options: {
type: "object",
description: "Optional styling options for this sheet.",
properties: {
headerStyle: {
type: "boolean",
description:
"Apply styling to the header row (bold, colored background).",
},
autoFit: {
type: "boolean",
description:
"Auto-fit column widths based on content.",
},
freezeHeader: {
type: "boolean",
description:
"Freeze the header row so it stays visible when scrolling.",
},
zebraStripes: {
type: "boolean",
description:
"Apply alternating row colors for better readability.",
},
delimiter: {
type: "string",
description:
"Override auto-detected delimiter. One of: comma, semicolon, tab, pipe.",
},
},
},
},
required: ["name", "csvData"],
},
},
options: {
type: "object",
description:
"Default styling options applied to all sheets (can be overridden per-sheet).",
properties: {
headerStyle: { type: "boolean" },
autoFit: { type: "boolean" },
freezeHeader: { type: "boolean" },
zebraStripes: { type: "boolean" },
},
},
},
required: ["filename"],
additionalProperties: false,
},
handler: async function ({
filename = "spreadsheet.xlsx",
csvData = null,
sheets = null,
options = {},
}) {
try {
this.super.handlerProps.log(`Using the create-excel-file tool.`);
const hasExtension = /\.xlsx$/i.test(filename);
if (!hasExtension) filename = `${filename}.xlsx`;
if (!csvData && (!sheets || sheets.length === 0)) {
return "Error: You must provide either 'csvData' for a single sheet or 'sheets' array for multiple sheets.";
}
const sheetDefinitions = sheets
? sheets
: [
{
name: "Sheet1",
csvData,
options: {},
},
];
for (const sheet of sheetDefinitions) {
if (!sheet.csvData || sheet.csvData.trim() === "") {
return `Error: Sheet "${sheet.name || "unnamed"}" has no CSV data.`;
}
}
const sheetCount = sheetDefinitions.length;
this.super.introspect(
`${this.caller}: Creating Excel file "${filename}" with ${sheetCount} sheet(s)`
);
if (this.super.requestToolApproval) {
const approval = await this.super.requestToolApproval({
skillName: this.name,
payload: {
filename,
sheetCount,
sheetNames: sheetDefinitions.map((s) => s.name),
},
description: `Create Excel spreadsheet "${filename}" with ${sheetCount} sheet(s)`,
});
if (!approval.approved) {
this.super.introspect(
`${this.caller}: User rejected the ${this.name} request.`
);
return approval.message;
}
}
const ExcelJS = await import("exceljs");
const workbook = new ExcelJS.default.Workbook();
workbook.creator = "AnythingLLM";
workbook.created = new Date();
workbook.modified = new Date();
const allWarnings = [];
for (const sheetDef of sheetDefinitions) {
let sheetName = (sheetDef.name || "Sheet").substring(0, 31);
sheetName = sheetName.replace(/[*?:\\/[\]]/g, "_");
const sheetOptions = {
...options,
...(sheetDef.options || {}),
};
const delimiterMap = {
comma: ",",
semicolon: ";",
tab: "\t",
pipe: "|",
};
const delimiter = sheetOptions.delimiter
? delimiterMap[sheetOptions.delimiter] ||
sheetOptions.delimiter
: detectDelimiter(sheetDef.csvData);
const parsedData = parseCSV(sheetDef.csvData, delimiter);
const validation = validateCSVData(parsedData);
if (!validation.valid) {
return `Error in sheet "${sheetName}": ${validation.error}`;
}
if (validation.warnings) {
allWarnings.push(
...validation.warnings.map((w) => `${sheetName}: ${w}`)
);
}
const worksheet = workbook.addWorksheet(sheetName);
for (
let rowIndex = 0;
rowIndex < parsedData.length;
rowIndex++
) {
const rowData = parsedData[rowIndex];
const row = worksheet.getRow(rowIndex + 1);
for (
let colIndex = 0;
colIndex < rowData.length;
colIndex++
) {
const cellValue = rowData[colIndex];
const cell = row.getCell(colIndex + 1);
const typedValue =
rowIndex === 0 ? cellValue : inferCellType(cellValue);
cell.value = typedValue;
if (typedValue instanceof Date) {
cell.numFmt = "yyyy-mm-dd";
} else if (
typeof typedValue === "number" &&
cellValue.includes("%")
) {
cell.numFmt = "0.00%";
}
}
row.commit();
}
if (sheetOptions.autoFit !== false) {
autoFitColumns(worksheet);
}
if (sheetOptions.headerStyle !== false) {
applyHeaderStyle(worksheet);
}
if (sheetOptions.zebraStripes) {
applyZebraStriping(worksheet);
}
if (sheetOptions.freezeHeader !== false) {
freezePanes(worksheet, 1, 0);
}
}
applyBranding(workbook);
const buffer = await workbook.xlsx.writeBuffer();
const bufferSizeKB = (buffer.length / 1024).toFixed(2);
const displayFilename = filename.split("/").pop();
this.super.handlerProps.log(
`create-excel-file: Generated buffer - size: ${bufferSizeKB}KB, sheets: ${sheetDefinitions.length}`
);
const savedFile = await createFilesLib.saveGeneratedFile({
fileType: "xlsx",
extension: "xlsx",
buffer: Buffer.from(buffer),
displayFilename,
});
this.super.socket.send("fileDownloadCard", {
filename: savedFile.displayFilename,
storageFilename: savedFile.filename,
fileSize: savedFile.fileSize,
});
createFilesLib.registerOutput(this.super, "ExcelFileDownload", {
filename: savedFile.displayFilename,
storageFilename: savedFile.filename,
fileSize: savedFile.fileSize,
});
this.super.introspect(
`${this.caller}: Successfully created Excel file "${displayFilename}"`
);
let result = `Successfully created Excel spreadsheet "${displayFilename}" (${bufferSizeKB}KB) with ${sheetDefinitions.length} sheet(s).`;
if (allWarnings.length > 0) {
result += `\n\nWarnings:\n${allWarnings.map((w) => `- ${w}`).join("\n")}`;
}
return result;
} catch (e) {
this.super.handlerProps.log(
`create-excel-file error: ${e.message}`
);
this.super.introspect(`Error: ${e.message}`);
return `Error creating Excel file: ${e.message}`;
}
},
});
},
};
},
};