diff --git a/data/completed_orders.csv b/data/completed_orders.csv new file mode 100644 index 0000000..829144d --- /dev/null +++ b/data/completed_orders.csv @@ -0,0 +1,6 @@ +order_id,customer_name,product,quantity,price,order_date,status +1001,John Smith,Laptop,1,999.99,2024-01-15,completed +1002,Jane Doe,Mouse,2,24.99,2024-01-16,completed +1004,Alice Brown,Monitor,2,299.99,2024-01-17,completed +1007,Frank Miller,USB Hub,2,19.99,2024-01-19,completed +1009,Henry Chen,External SSD,1,149.99,2024-01-20,completed \ No newline at end of file diff --git a/data/order_summary.json b/data/order_summary.json new file mode 100644 index 0000000..1dbcc1b --- /dev/null +++ b/data/order_summary.json @@ -0,0 +1,124 @@ +{ + "totalOrders": 10, + "totalRevenue": 2254.84, + "averageOrderValue": 225.48, + "statusBreakdown": { + "completed": 5, + "processing": 3, + "shipped": 2 + }, + "topProducts": [ + { + "product": "Headphones", + "totalQuantity": 3 + }, + { + "product": "Mouse", + "totalQuantity": 2 + }, + { + "product": "Monitor", + "totalQuantity": 2 + }, + { + "product": "USB Hub", + "totalQuantity": 2 + }, + { + "product": "Wireless Charger", + "totalQuantity": 2 + } + ], + "orderDetails": [ + { + "order_id": "1001", + "customer_name": "John Smith", + "product": "Laptop", + "quantity": "1", + "price": "999.99", + "order_date": "2024-01-15", + "status": "completed" + }, + { + "order_id": "1002", + "customer_name": "Jane Doe", + "product": "Mouse", + "quantity": "2", + "price": "24.99", + "order_date": "2024-01-16", + "status": "completed" + }, + { + "order_id": "1003", + "customer_name": "Bob Johnson", + "product": "Keyboard", + "quantity": "1", + "price": "79.99", + "order_date": "2024-01-17", + "status": "processing" + }, + { + "order_id": "1004", + "customer_name": "Alice Brown", + "product": "Monitor", + "quantity": "2", + "price": "299.99", + "order_date": "2024-01-17", + "status": "completed" + }, + { + "order_id": "1005", + "customer_name": "Charlie Wilson", + "product": "Headphones", + "quantity": "3", + "price": "49.99", + "order_date": "2024-01-18", + "status": "shipped" + }, + { + "order_id": "1006", + "customer_name": "Emma Davis", + "product": "Webcam", + "quantity": "1", + "price": "89.99", + "order_date": "2024-01-19", + "status": "processing" + }, + { + "order_id": "1007", + "customer_name": "Frank Miller", + "product": "USB Hub", + "quantity": "2", + "price": "19.99", + "order_date": "2024-01-19", + "status": "completed" + }, + { + "order_id": "1008", + "customer_name": "Grace Lee", + "product": "Desk Lamp", + "quantity": "1", + "price": "34.99", + "order_date": "2024-01-20", + "status": "shipped" + }, + { + "order_id": "1009", + "customer_name": "Henry Chen", + "product": "External SSD", + "quantity": "1", + "price": "149.99", + "order_date": "2024-01-20", + "status": "completed" + }, + { + "order_id": "1010", + "customer_name": "Isabel Garcia", + "product": "Wireless Charger", + "quantity": "2", + "price": "29.99", + "order_date": "2024-01-21", + "status": "processing" + } + ] +} \ No newline at end of file diff --git a/data/orders.csv b/data/orders.csv new file mode 100644 index 0000000..1fac55e --- /dev/null +++ b/data/orders.csv @@ -0,0 +1,11 @@ +order_id,customer_name,product,quantity,price,order_date,status +1001,John Smith,Laptop,1,999.99,2024-01-15,completed +1002,Jane Doe,Mouse,2,24.99,2024-01-16,completed +1003,Bob Johnson,Keyboard,1,79.99,2024-01-17,processing +1004,Alice Brown,Monitor,2,299.99,2024-01-17,completed +1005,Charlie Wilson,Headphones,3,49.99,2024-01-18,shipped +1006,Emma Davis,Webcam,1,89.99,2024-01-19,processing +1007,Frank Miller,USB Hub,2,19.99,2024-01-19,completed +1008,Grace Lee,Desk Lamp,1,34.99,2024-01-20,shipped +1009,Henry Chen,External SSD,1,149.99,2024-01-20,completed +1010,Isabel Garcia,Wireless Charger,2,29.99,2024-01-21,processing \ No newline at end of file diff --git a/data/report.json b/data/report.json new file mode 100644 index 0000000..0e14823 --- /dev/null +++ b/data/report.json @@ -0,0 +1,11 @@ +{ + "generatedAt": "2025-09-09T17:40:24.911Z", + "summary": { + "orders": 10, + "revenue": 2254.84, + "topProduct": { + "product": "Headphones", + "totalQuantity": 3 + } + } +} \ No newline at end of file diff --git a/examples/file_io.ts b/examples/file_io.ts new file mode 100644 index 0000000..f6e4cd7 --- /dev/null +++ b/examples/file_io.ts @@ -0,0 +1,177 @@ +/** + * File I/O Tool Example + * Demonstrates reading CSV files and writing JSON files using the core File I/O tool + */ + +import { fileIOTool, readCSVFile, writeJSONFile } from '../src/tools/fileIOTool.js'; +import * as path from 'path'; + +interface OrderSummary { + totalOrders: number; + totalRevenue: number; + averageOrderValue: number; + statusBreakdown: Record; + topProducts: Array<{ product: string; totalQuantity: number }>; + orderDetails: any[]; +} + +async function demonstrateFileIO() { + console.log('=== File I/O Tool Demonstration ===\n'); + + try { + // 1. Read CSV file using the tool directly + console.log('1. Reading CSV file using fileIOTool...'); + const csvPath = path.join(process.cwd(), 'data', 'orders.csv'); + + const readResult = await fileIOTool.execute({ + action: 'read', + filepath: csvPath, + format: 'csv' + }, {} as any); // Context would be provided by the agent framework + + // Check if result is a ToolResult + let orders: any[]; + if (typeof readResult === 'string') { + throw new Error('Unexpected string result'); + } else if (readResult.status === 'success') { + orders = readResult.data.data as any[]; + } else { + throw new Error(readResult.message || 'Failed to read CSV'); + } + console.log(`✓ Successfully read ${orders.length} orders from CSV\n`); + + // Display first few orders + console.log('First 3 orders:'); + orders.slice(0, 3).forEach(order => { + console.log(` Order #${order.order_id}: ${order.customer_name} - ${order.product} (${order.status})`); + }); + console.log(); + + // 2. Process the data + console.log('2. Processing order data...'); + + // Calculate summary statistics + const totalRevenue = orders.reduce((sum, order) => { + return sum + (parseFloat(order.price) * parseInt(order.quantity)); + }, 0); + + const statusBreakdown: Record = {}; + orders.forEach(order => { + statusBreakdown[order.status] = (statusBreakdown[order.status] || 0) + 1; + }); + + // Find top products by quantity + const productQuantities: Record = {}; + orders.forEach(order => { + const qty = parseInt(order.quantity); + productQuantities[order.product] = (productQuantities[order.product] || 0) + qty; + }); + + const topProducts = Object.entries(productQuantities) + .map(([product, totalQuantity]) => ({ product, totalQuantity })) + .sort((a, b) => b.totalQuantity - a.totalQuantity) + .slice(0, 5); + + const summary: OrderSummary = { + totalOrders: orders.length, + totalRevenue: Math.round(totalRevenue * 100) / 100, + averageOrderValue: Math.round((totalRevenue / orders.length) * 100) / 100, + statusBreakdown, + topProducts, + orderDetails: orders + }; + + console.log('✓ Data processing complete\n'); + console.log('Summary:'); + console.log(` Total Orders: ${summary.totalOrders}`); + console.log(` Total Revenue: $${summary.totalRevenue}`); + console.log(` Average Order Value: $${summary.averageOrderValue}`); + console.log(` Status Breakdown:`, summary.statusBreakdown); + console.log(); + + // 3. Write JSON file using the tool + console.log('3. Writing summary to JSON file...'); + const jsonPath = path.join(process.cwd(), 'data', 'order_summary.json'); + + const writeResult = await fileIOTool.execute({ + action: 'write', + filepath: jsonPath, + content: summary, + format: 'json' + }, {} as any); + + if (typeof writeResult !== 'string' && writeResult.status !== 'success') { + throw new Error(writeResult.message || 'Failed to write JSON'); + } + + console.log(`✓ Successfully wrote summary to ${jsonPath}\n`); + + // 4. Demonstrate convenience functions + console.log('4. Using convenience functions...'); + + // Read CSV using convenience function + const ordersAlt = await readCSVFile(csvPath); + console.log(`✓ readCSVFile: Read ${ordersAlt.length} orders`); + + // Write JSON using convenience function + const reportPath = path.join(process.cwd(), 'data', 'report.json'); + await writeJSONFile(reportPath, { + generatedAt: new Date().toISOString(), + summary: { + orders: summary.totalOrders, + revenue: summary.totalRevenue + } + }); + console.log(`✓ writeJSONFile: Created report at ${reportPath}\n`); + + // 5. Read the written JSON file to verify + console.log('5. Verifying written JSON file...'); + const verifyResult = await fileIOTool.execute({ + action: 'read', + filepath: jsonPath, + format: 'json' + }, {} as any); + + if (typeof verifyResult !== 'string' && verifyResult.status === 'success') { + const verifiedData = verifyResult.data.data as OrderSummary; + console.log(`✓ Verified JSON file contains ${verifiedData.totalOrders} orders`); + console.log(`✓ Revenue matches: $${verifiedData.totalRevenue}\n`); + } + + // 6. Demonstrate CSV writing + console.log('6. Writing filtered data to new CSV...'); + const completedOrders = orders.filter(order => order.status === 'completed'); + + const csvWriteResult = await fileIOTool.execute({ + action: 'write', + filepath: path.join(process.cwd(), 'data', 'completed_orders.csv'), + content: completedOrders, + format: 'csv' + }, {} as any); + + if (typeof csvWriteResult !== 'string' && csvWriteResult.status !== 'success') { + throw new Error(csvWriteResult.message || 'Failed to write CSV'); + } + + console.log(`✓ Wrote ${completedOrders.length} completed orders to new CSV file\n`); + + console.log('=== File I/O Tool Demonstration Complete ==='); + console.log('\nThe fileIOTool provides:'); + console.log(' • Core Tool interface compatible with JAF framework'); + console.log(' • Read/write support for text, JSON, and CSV files'); + console.log(' • Automatic format detection based on file extension'); + console.log(' • CSV parsing with configurable delimiter and headers'); + console.log(' • Type-safe parameters using Zod schemas'); + console.log(' • Proper error handling with ToolResult types'); + console.log(' • Convenience functions for common operations'); + + } catch (error) { + console.error('Error during file I/O demonstration:', error); + process.exit(1); + } +} + +// Run the demonstration +if (import.meta.url === `file://${process.argv[1]}`) { + demonstrateFileIO().catch(console.error); +} \ No newline at end of file diff --git a/src/tools/fileIOTool.ts b/src/tools/fileIOTool.ts new file mode 100644 index 0000000..f41b069 --- /dev/null +++ b/src/tools/fileIOTool.ts @@ -0,0 +1,267 @@ +/** + * File I/O Tool - Read and write text, JSON, and CSV files + * Core utility tool for file operations + */ + +import * as fs from 'node:fs/promises'; +import * as path from 'node:path'; +import { z } from 'zod'; +import { Tool } from '../core/types.js'; +import { success, error, ToolResult } from '../core/tool-results.js'; + +// ========== CSV Utilities ========== + +/** + * Parse CSV content into array of objects + */ +const parseCSV = (content: string, options: { delimiter?: string; headers?: boolean } = {}): any[] => { + const { delimiter = ',', headers = true } = options; + const lines = content.trim().split('\n'); + + if (lines.length === 0) return []; + + if (!headers) { + return lines.map(line => line.split(delimiter).map(cell => cell.trim())); + } + + const headerLine = lines[0]; + const headerFields = headerLine.split(delimiter).map(h => h.trim()); + const dataLines = lines.slice(1); + + return dataLines.map(line => { + const values = line.split(delimiter).map(v => v.trim()); + const obj: Record = {}; + headerFields.forEach((header, index) => { + obj[header] = values[index] || ''; + }); + return obj; + }); +}; + +/** + * Convert array of objects to CSV string + */ +const toCSV = (data: any[], options: { delimiter?: string; headers?: boolean } = {}): string => { + const { delimiter = ',', headers = true } = options; + + if (data.length === 0) return ''; + + // If data is array of arrays + if (Array.isArray(data[0])) { + return data.map(row => row.join(delimiter)).join('\n'); + } + + // If data is array of objects + const keys = Object.keys(data[0]); + const lines: string[] = []; + + if (headers) { + lines.push(keys.join(delimiter)); + } + + data.forEach(obj => { + const values = keys.map(key => String(obj[key] || '')); + lines.push(values.join(delimiter)); + }); + + return lines.join('\n'); +}; + +/** + * Detect file format from extension + */ +const detectFormat = (filepath: string): 'text' | 'json' | 'csv' => { + const ext = path.extname(filepath).toLowerCase(); + switch (ext) { + case '.json': + return 'json'; + case '.csv': + return 'csv'; + default: + return 'text'; + } +}; + +// ========== Schema Definitions ========== + +const FileIOSchema = z.discriminatedUnion('action', [ + z.object({ + action: z.literal('read'), + filepath: z.string().describe('Path to the file to read'), + format: z.enum(['text', 'json', 'csv']).optional().describe('File format (auto-detected if not specified)'), + encoding: z.string().optional().default('utf8').describe('File encoding'), + csvOptions: z.object({ + delimiter: z.string().optional().default(','), + headers: z.boolean().optional().default(true) + }).optional() + }), + z.object({ + action: z.literal('write'), + filepath: z.string().describe('Path to the file to write'), + content: z.any().describe('Content to write to the file'), + format: z.enum(['text', 'json', 'csv']).optional().describe('File format (auto-detected if not specified)'), + encoding: z.string().optional().default('utf8').describe('File encoding'), + csvOptions: z.object({ + delimiter: z.string().optional().default(','), + headers: z.boolean().optional().default(true) + }).optional() + }) +]); + +type FileIOParams = z.infer; + +// ========== Tool Implementation ========== + +/** + * File I/O Tool - Read and write text, JSON, and CSV files + */ +export function createFileIOTool(): Tool { + return { + schema: { + name: 'fileIO', + description: 'Read and write text, JSON, and CSV files. Supports automatic format detection.', + parameters: FileIOSchema + }, + + execute: async (params: FileIOParams): Promise => { + const { action, filepath, encoding = 'utf8' } = params; + let format = params.format; + + // Auto-detect format if not specified + if (!format) { + format = detectFormat(filepath); + } + + try { + if (action === 'read') { + // Read file + const fileContent = await fs.readFile(filepath, encoding); + + let data: string | Record | any[]; + + switch (format) { + case 'json': + data = JSON.parse(fileContent); + break; + case 'csv': + data = parseCSV(fileContent, params.csvOptions); + break; + default: + data = fileContent; + } + + return success({ + data, + format, + filepath, + message: `Successfully read ${format} file: ${filepath}` + }); + + } else { + // Write file + const { content } = params; + if (content === undefined || content === null) { + return error('Content is required for write action'); + } + + let fileContent: string; + + switch (format) { + case 'json': + fileContent = JSON.stringify(content, null, 2); + break; + case 'csv': + if (!Array.isArray(content)) { + return error('CSV content must be an array'); + } + fileContent = toCSV(content, params.csvOptions); + break; + default: + fileContent = typeof content === 'string' ? content : String(content); + } + + // Ensure directory exists + const dir = path.dirname(filepath); + await fs.mkdir(dir, { recursive: true }); + + // Write file + await fs.writeFile(filepath, fileContent, encoding); + + return success({ + filepath, + format, + message: `Successfully wrote ${format} file: ${filepath}` + }); + } + + } catch (err) { + const errorMessage = err instanceof Error ? err.message : String(err); + return error(`File I/O operation failed: ${errorMessage}`); + } + } + }; +} + +// ========== Convenience Functions ========== + +/** + * Read a text file + */ +export async function readTextFile(filepath: string): Promise { + const content = await fs.readFile(filepath, 'utf8'); + return content; +} + +/** + * Read a JSON file + */ +export async function readJSONFile(filepath: string): Promise { + const content = await fs.readFile(filepath, 'utf8'); + return JSON.parse(content); +} + +/** + * Read a CSV file + */ +export async function readCSVFile( + filepath: string, + options?: { delimiter?: string; headers?: boolean } +): Promise { + const content = await fs.readFile(filepath, 'utf8'); + return parseCSV(content, options); +} + +/** + * Write a text file + */ +export async function writeTextFile(filepath: string, content: string): Promise { + const dir = path.dirname(filepath); + await fs.mkdir(dir, { recursive: true }); + await fs.writeFile(filepath, content, 'utf8'); +} + +/** + * Write a JSON file + */ +export async function writeJSONFile(filepath: string, content: any): Promise { + const dir = path.dirname(filepath); + await fs.mkdir(dir, { recursive: true }); + await fs.writeFile(filepath, JSON.stringify(content, null, 2), 'utf8'); +} + +/** + * Write a CSV file + */ +export async function writeCSVFile( + filepath: string, + content: any[], + options?: { delimiter?: string; headers?: boolean } +): Promise { + const dir = path.dirname(filepath); + await fs.mkdir(dir, { recursive: true }); + const csvContent = toCSV(content, options); + await fs.writeFile(filepath, csvContent, 'utf8'); +} + +// Export the default tool instance +export const fileIOTool = createFileIOTool(); \ No newline at end of file diff --git a/test-file-io.mjs b/test-file-io.mjs new file mode 100644 index 0000000..aa1ee4b --- /dev/null +++ b/test-file-io.mjs @@ -0,0 +1,170 @@ +#!/usr/bin/env node + +/** + * Standalone test for File I/O Tool + * This file directly tests the file I/O functionality without compilation + */ + +import * as fs from 'node:fs/promises'; +import * as path from 'node:path'; + +// Inline the File I/O Tool implementation for testing +const parseCSV = (content, options = {}) => { + const { delimiter = ',', headers = true } = options; + const lines = content.trim().split('\n'); + + if (lines.length === 0) return []; + + if (!headers) { + return lines.map(line => line.split(delimiter).map(cell => cell.trim())); + } + + const headerLine = lines[0]; + const headerFields = headerLine.split(delimiter).map(h => h.trim()); + const dataLines = lines.slice(1); + + return dataLines.map(line => { + const values = line.split(delimiter).map(v => v.trim()); + const obj = {}; + headerFields.forEach((header, index) => { + obj[header] = values[index] || ''; + }); + return obj; + }); +}; + +const toCSV = (data, options = {}) => { + const { delimiter = ',', headers = true } = options; + + if (data.length === 0) return ''; + + if (Array.isArray(data[0])) { + return data.map(row => row.join(delimiter)).join('\n'); + } + + const keys = Object.keys(data[0]); + const lines = []; + + if (headers) { + lines.push(keys.join(delimiter)); + } + + data.forEach(obj => { + const values = keys.map(key => String(obj[key] || '')); + lines.push(values.join(delimiter)); + }); + + return lines.join('\n'); +}; + +async function testFileIO() { + console.log('=== File I/O Tool Test ===\n'); + + try { + // 1. Read CSV file + console.log('1. Reading CSV file...'); + const csvPath = path.join(process.cwd(), 'data', 'orders.csv'); + const csvContent = await fs.readFile(csvPath, 'utf8'); + const orders = parseCSV(csvContent); + console.log(`✓ Successfully read ${orders.length} orders from CSV\n`); + + // Display first few orders + console.log('First 3 orders:'); + orders.slice(0, 3).forEach(order => { + console.log(` Order #${order.order_id}: ${order.customer_name} - ${order.product} (${order.status})`); + }); + console.log(); + + // 2. Process the data + console.log('2. Processing order data...'); + + const totalRevenue = orders.reduce((sum, order) => { + return sum + (parseFloat(order.price) * parseInt(order.quantity)); + }, 0); + + const statusBreakdown = {}; + orders.forEach(order => { + statusBreakdown[order.status] = (statusBreakdown[order.status] || 0) + 1; + }); + + const productQuantities = {}; + orders.forEach(order => { + const qty = parseInt(order.quantity); + productQuantities[order.product] = (productQuantities[order.product] || 0) + qty; + }); + + const topProducts = Object.entries(productQuantities) + .map(([product, totalQuantity]) => ({ product, totalQuantity })) + .sort((a, b) => b.totalQuantity - a.totalQuantity) + .slice(0, 5); + + const summary = { + totalOrders: orders.length, + totalRevenue: Math.round(totalRevenue * 100) / 100, + averageOrderValue: Math.round((totalRevenue / orders.length) * 100) / 100, + statusBreakdown, + topProducts, + orderDetails: orders + }; + + console.log('✓ Data processing complete\n'); + console.log('Summary:'); + console.log(` Total Orders: ${summary.totalOrders}`); + console.log(` Total Revenue: $${summary.totalRevenue}`); + console.log(` Average Order Value: $${summary.averageOrderValue}`); + console.log(` Status Breakdown:`, summary.statusBreakdown); + console.log(` Top Products:`, topProducts.slice(0, 3).map(p => `${p.product} (${p.totalQuantity})`).join(', ')); + console.log(); + + // 3. Write JSON file + console.log('3. Writing summary to JSON file...'); + const jsonPath = path.join(process.cwd(), 'data', 'order_summary.json'); + await fs.mkdir(path.dirname(jsonPath), { recursive: true }); + await fs.writeFile(jsonPath, JSON.stringify(summary, null, 2), 'utf8'); + console.log(`✓ Successfully wrote summary to ${jsonPath}\n`); + + // 4. Verify the written JSON file + console.log('4. Verifying written JSON file...'); + const verifyContent = await fs.readFile(jsonPath, 'utf8'); + const verifiedData = JSON.parse(verifyContent); + console.log(`✓ Verified JSON file contains ${verifiedData.totalOrders} orders`); + console.log(`✓ Revenue matches: $${verifiedData.totalRevenue}\n`); + + // 5. Write filtered CSV + console.log('5. Writing filtered data to new CSV...'); + const completedOrders = orders.filter(order => order.status === 'completed'); + const csvOutput = toCSV(completedOrders); + const csvOutputPath = path.join(process.cwd(), 'data', 'completed_orders.csv'); + await fs.writeFile(csvOutputPath, csvOutput, 'utf8'); + console.log(`✓ Wrote ${completedOrders.length} completed orders to ${csvOutputPath}\n`); + + // 6. Create a simple report + console.log('6. Creating report file...'); + const report = { + generatedAt: new Date().toISOString(), + summary: { + orders: summary.totalOrders, + revenue: summary.totalRevenue, + topProduct: topProducts[0] + } + }; + const reportPath = path.join(process.cwd(), 'data', 'report.json'); + await fs.writeFile(reportPath, JSON.stringify(report, null, 2), 'utf8'); + console.log(`✓ Created report at ${reportPath}\n`); + + console.log('=== File I/O Tool Test Complete ==='); + console.log('\nThe fileIOTool.ts implementation provides:'); + console.log(' • Read/write support for text, JSON, and CSV files'); + console.log(' • Automatic format detection based on file extension'); + console.log(' • CSV parsing with header support'); + console.log(' • Convenient helper functions for common operations'); + console.log(' • Error handling and validation'); + + } catch (error) { + console.error('Error during file I/O test:', error); + process.exit(1); + } +} + +// Run the test +testFileIO().catch(console.error); \ No newline at end of file